在当今的 Web 应用开发中,JWT(JSON Web Token)由于其轻便性、跨平台兼容性以及易于集成的特点,已成为主流的身份认证与授权机制之一。然而,在 CTF 竞赛和安全渗透测试实践中,JWT 也经常成为攻击者突破系统防线的关键入口。一旦攻击者能够篡改 Token 内容并绕过服务器端的验证逻辑,就可能实现身份伪造,甚至获取管理员权限。本文将结合实际题目与常用工具,系统性地介绍 JWT 攻击的典型方法与实现路径,帮助读者深入理解其潜在风险及防御策略。
JWT 基础结构解析
本部分内容参考自《JSON Web Token 入门教程》,旨在为读者提供对 JWT 的基本认知。JWT 是目前应用最广泛的跨域身份验证方案之一。其核心思想是:用户通过身份验证后,服务器生成一个 JSON 格式的对象并发送给客户端;此后每次请求,客户端都需携带该对象,服务器则完全依赖此对象判断用户身份。为防止数据被篡改,服务器在生成该对象时会附加数字签名。
一个典型的 JWT 字符串由三部分组成,使用点号 . 分隔,分别是头部(Header)、负载(Payload)和签名(Signature),整体结构如下所示:
.
1. Header 头部信息
Header 部分通常包含两个关键字段:
{
"alg": "HS256",
"typ": "JWT"
}
alg表示所使用的签名算法,默认为 HMAC SHA256,简写为 HS256;typ表示令牌类型,对于 JWT 来说统一固定为
JWT
该部分经过 Base64Url 编码后构成 JWT 的第一段。
alg
typ
2. Payload 负载数据
Payload 用于承载实际传输的信息,包括标准定义的 7 个可选字段,也可以添加自定义的私有字段。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
这部分信息同样经过 Base64Url 编码,作为 JWT 的第二段内容。
3. Signature 签名生成
Signature 是对前两部分进行签名的结果,目的是确保数据完整性。服务器使用保存在本地的密钥(secret),结合 Header 中指定的算法(如 HS256),对拼接后的 Base64Url 编码头部和负载进行加密运算,生成最终签名。
三部分分别编码后,用点号连接形成完整的 JWT,并返回给用户。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTcyODU3MDg2MywiZXhwIjoxNzI4NTc4MDYzLCJuYmYiOjE3Mjg1NzA4NjMsInN1YiI6InVzZXIiLCJqdGkiOiI1NWQ3ZDBlNjI2YzFjMjk2NTY5MGEwZWFlYTk3ZjJlZSJ9.L7RdUb-HcLH4-MTea7n4S7iuwFhuAPnbCSQlpwtKFx0
常用攻击工具介绍
1. BurpSuite 插件:JWT Editor
在 JWT 相关的安全测试中,BurpSuite 的 JWT Editor 插件是非常实用的辅助工具。当拦截到含有 JWT 的 HTTP 请求时,插件能自动识别并以结构化形式展示 Header 与 Payload,允许测试人员直接修改字段内容、导入或管理密钥,并重新生成合法签名。后续实例中将结合具体场景演示其操作流程。插件安装方式不在本文详述,用户可自行查阅相关文档。
2. hashcat —— 密钥爆破工具
当目标系统使用对称加密算法(如 HS256)且密钥强度较弱时,可通过 hashcat 实现离线暴力破解。若成功恢复密钥,则可在 BurpSuite 中导入该密钥,对篡改后的 Token 进行重签,进而利用系统逻辑漏洞完成权限提升等操作。具体使用方法将在后续案例中说明。hashcat 的下载与配置过程不在此展开。
3. rockyou.txt —— 常用密码字典
在 CTF 或真实渗透场景中,rockyou.txt 是最为常用的密码字典之一。它源自历史上大规模泄露的真实账户数据,涵盖大量弱口令、生日组合及常见短语,适用于绝大多数低强度密钥的爆破任务。配合 hashcat 使用时,可显著提高破解成功率。该字典通常预装于 Kali Linux 系统中,路径一般位于:
/usr/share/wordlists/
实战案例一:Web 345 —— 签名未校验
查看源码提示访问特定目录:
/admin
尝试访问该路径时出现 302 重定向:
初步分析认为可能是 Cookie 中权限不足所致。虽然 JWT Editor 未能自动识别 Token 结构,但可通过手动 Base64 解码 Cookie 内容,发现系统采用 JWT 进行认证,但并未对签名进行有效性验证。
(alg:"None")
因此只需修改 Payload 内容为预期值:
sub:admin
再将其 Base64 编码并替换原 Cookie,重新发送请求即可成功访问目标页面:
index.php
最终成功获取 flag:
实战案例二:Web 346 —— 使用 None 算法绕过签名
前端代码提示信息如下:
/admin/
访问指定接口:
/admin
利用 JWT Editor 自动解析当前 Token,可见其签名算法为 HS256(HMAC + SHA-256):
若服务端接受 alg 设置为 "none" 的无签名 JWT,则可将 Header 中的算法字段修改为 none,移除签名部分。此时 Token 内容可任意篡改而不会被检测。
实施该攻击后,成功访问受限资源:
/admin
并获得 flag:
实战案例三:Web 347 —— 对称密钥爆破攻击
某些 JWT 实现使用对称签名算法,例如本题中的 HS256(HMAC + SHA-256)。这类算法的安全性高度依赖密钥复杂度。若密钥过于简单,攻击者可通过离线爆破手段还原密钥,从而实现对任意 Payload 的重签名。
题目前端仍给出相同提示:
/admin/
(注:后续步骤涉及使用 hashcat 结合 rockyou.txt 字典进行密钥爆破,导入 BurpSuite 完成重签,最终获取 flag。详细流程将在后续章节延续说明。)
在进行安全测试时,访问目标接口并使用抓包工具分析请求内容。通过观察可发现,JWT 使用的是 HS256 算法进行签名,因此可以考虑对密钥进行爆破尝试。
/admin/
利用 hashcat 工具对 JWT 的签名密钥展开字典攻击。首先指定对应的哈希类型,并提供待破解的 JWT 令牌,同时关联一个常用密码字典文件来进行暴力破解。
-a 0
-m 16500
HS256
rockyou.txt
根据爆破结果,成功获取到签名密钥为“123456”。接下来,使用 jwt-editor 插件生成可用于签名的密钥对象。
hashcat.exe -a 0 -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTcyODQzNzMwNiwiZXhwIjoxNzI4NDQ0NTA2LCJuYmYiOjE3Mjg0MzczMDYsInN1YiI6InVzZXIiLCJqdGkiOiJhMDZkYTg3ZWRkYWE4Zjg5MzgxYmVjMWFjMzU4NGNkYyJ9.cZ1VQX4r1Ymo_gZCs8FqfYPlOD5IriiQZjFnA2jOBzU rockyou.txt
在插件界面中选择创建新的对称密钥,点击相应选项后将密钥值替换为经 base64 编码后的“123456”,确认保存即可获得可用签名密钥。
JWT Editor
New Symmetic Key
generate
k
随后修改原始 JWT 中的用户身份信息(如将用户名或角色更改为 admin),然后在左下角点击签名按钮,选择之前配置好的密钥完成重签。
sub
admin
将重新签名后的 JWT 替换至请求中并发送,成功绕过权限验证,返回最终的 flag 内容。
/admin/
5. CTFshow Web 348 —— 对称式签名密钥爆破
本题与第 347 题操作流程基本一致,主要区别在于本次使用的密钥强度有所提升,无法通过极简字典快速命中。经过完整字典爆破后,得出实际密钥为:
aaab
用户可参照前一题的方法步骤自行完成破解过程。
6. CTFshow Web 349 —— 非对称式加密签名私钥泄露
本题暴露了部分前端 JavaScript 源码,其核心功能包含两个部分:
- 服务器从指定路径读取私钥文件
- 使用 RS256 非对称算法生成 JWT 并写入 cookie 字段
GET /
public/private.key
auth
当用户发起请求时,系统会校验该 JWT 是否合法,且要求 payload 中声明的身份为 admin 才返回 flag;否则提示 “you are not admin”。
POST /
admin
通过访问特定接口,我们可以下载到用于签名的私钥文件。
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var privateKey = fs.readFileSync(process.cwd()+'//public//private.key');
var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });
res.cookie('auth',token);
res.end('where is flag?');
});
router.post('/',function(req,res,next){
var flag="flag_here";
res.type('html');
var auth = req.cookies.auth;
var cert = fs.readFileSync(process.cwd()+'//public/public.key'); // get public key
jwt.verify(auth, cert, function(err, decoded) {
if(decoded.user==='admin'){
res.end(flag);
}else{
res.end('you are not admin');
}
});
});
https://xxx.challenge.ctf.show/private.key
RS256
利用 JWT Editor 插件新建一个 RSA 类型密钥,选择 PEM 格式并生成空白密钥对,随后将下载得到的私钥内容完整替换进去。
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDNioS2aSHtu6WIU88oWzpShhkb+r6QPBryJmdaR1a3ToD9sXDb
eni5WTsWVKrmzmCk7tu4iNtkmn/r9D/bFcadHGnXYqlTJItOdHZio3Bi1J2Elxg8
IEBKx9g6RggTOGXQFxSxlzLNMRzRC4d2PcA9mxjAbG1Naz58ibbtogeglQIDAQAB
AoGAE+mAc995fvt3zN45qnI0EzyUgCZpgbWg8qaPyqowl2+OhYVEJq8VtPcVB1PK
frOtnyzYsmbnwjZJgEVYTlQsum0zJBuTKoN4iDoV0Oq1Auwlcr6O0T35RGiijqAX
h7iFjNscfs/Dp/BnyKZuu60boXrcuyuZ8qXHz0exGkegjMECQQD1eP39cPhcwydM
cdEBOgkI/E/EDWmdjcwIoauczwiQEx56EjAwM88rgxUGCUF4R/hIW9JD1vlp62Qi
ST9LU4lxAkEA1lsfr9gF/9OdzAsPfuTLsl+l9zpo1jjzhXlwmHFgyCAn7gBKeWdv
ubocOClTTQ7Y4RqivomTmlNVtmcHda1XZQJAR0v0IZedW3wHPwnT1dJga261UFFA
+tUDjQJAERSE/SvAb143BtkVdCLniVBI5sGomIOq569Z0+zdsaOqsZs60QJAYqtJ
V7EReeQX8693r4pztSTQCZBKZ6mJdvwidxlhWl1q4+QgY+fYBt8DVFq5bHQUIvIW
zawYVGZdwvuD9IgY/QJAGCJbXA+Knw10B+g5tDZfVHsr6YYMY3Q24zVu4JXozWDV
x+G39IajrVKwuCPG2VezWfwfWpTeo2bDmQS0CWOPjA==
-----END RSA PRIVATE KEY-----
接着修改原始 token 中的用户身份字段为 admin,点击重签名并选择刚才导入的私钥完成签名更新。
由于源码提示需以 POST 方法提交请求才能触发 flag 返回逻辑,因此需要在 Burp Suite 中右键修改请求方式。
change request methond
最后发送修改后的请求,成功获取 flag。
7. CTFshow Web 350 —— 篡改加密签名算法
查看源码包中 routes/index.js 文件,整体逻辑与上一题相似,关键差异在于本次无法直接访问私钥文件路径,因此不能通过常规私钥重签方式突破验证。
private.key
但公钥文件仍处于可访问状态,这为我们提供了另一种攻击思路:将原本应使用 RS256 验签的 JWT,篡改为使用 HS256 算法签名,并利用已知的公钥作为 HMAC 密钥参与签名过程。
var express = require('express');
var router = express.Router();
var jwt = require('jsonwebtoken');
var fs = require('fs');
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var privateKey = fs.readFileSync(process.cwd()+'//routes/private.key');
var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });
res.cookie('auth',token);
res.end('where is flag?');
});
router.post('/',function(req,res,next){
var flag="flag_here";
res.type('html');
var auth = req.cookies.auth;
var cert = fs.readFileSync(process.cwd()+'//routes/public.key'); // get public key
jwt.verify(auth, cert,function(err, decoded) {
if(decoded.user==='admin'){
res.end(flag);
}else{
res.end('you are not admin'+err);
}
});
});
module.exports = router;
public.key
RS256
HS256
这种攻击成立的前提基于以下两点:
- 服务器在验证 JWT 时依赖 header 中的 alg 字段动态选择验证算法,若未严格限制允许的算法类型,则存在被篡改风险。
- HS256 是基于对称密钥的 HMAC 签名机制,而如果攻击者将 RSA 公钥整体当作 HMAC 的密钥输入,在服务端也使用相同公钥进行校验的情况下,签名依然会被认为合法。
alg
RS256
HS256
具体操作中,首先访问指定接口获取公钥内容。尝试在 Burp 的 JWT Editor 中新建对称密钥并粘贴 PEM 格式的公钥时,会出现格式错误或签名失败问题。这是因为手动处理过程中容易破坏原始换行、缺失头尾标记或编码不一致所致。
https://xxx.challenge.ctf.show/public.key
许多公开的解决方案采用 Node.js 脚本实现,其原理是通过 fs.readFileSync 直接读取整个 public.pem 文件字符串(包括 BEGIN/END 行和内部换行符),并将完整的文本内容作为 HMAC 密钥使用。只要客户端和服务端使用的字节序列完全一致,就能生成有效签名。
为避免格式偏差,推荐使用 Python 编写自动化脚本来生成正确的 token。生成后将其填入 Burp 请求中,并注意将请求方法改为 POST,最终成功获取 flag。
import jwt
# 原始 JWT 粘贴到这里
orig = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImlhdCI6MTc1NzQ4NDU1NH0.QKpMeBqmdq3D1i-3AXwpwxo1vJd7ZAoKUBD76pmOu8D9jhbXF8enHCECZ53218LPNyjbBG-h6xVycQ7kHi0vLzBJHM0P4zuqCJMd0CkDksNVf0vlznp4LcmxqWHhok38ohY5tNgR1uE5ULDa9rOVt2_T0juJPWDD-h_360-S0NA"
# 解析 JWT,获取头部和载荷
header = jwt.get_unverified_header(orig)
payload = jwt.decode(orig, options={"verify_signature": False})
print("Header:\n",header,"\n","Payload:\n",payload,"\n")
# 修改头部和载荷
header["alg"] = "HS256"
payload["user"] = "admin"
# 重新签名并输出新的 JWT
# 读取公钥全文
with open("public.key", "rb") as f:
key = f.read()
token = jwt.encode(payload, key, algorithm="HS256", headers=header)
print(token)

雷达卡


京公网安备 11010802022788号







