imthiyas25/challenge2-jwt-verification
GitHub: imthiyas25/challenge2-jwt-verification
JWT 算法混淆漏洞修复验证工具,提供八种攻击向量的自动化测试和防篡改证据报告生成。
Stars: 0 | Forks: 0
# 挑战 2 — 修复验证报告
**JWT Algorithm Confusion | FIND-0087**
本仓库涵盖挑战 2 的所有五个部分:书面威胁分析、测试用例设计、AI 辅助工作流文档、实现说明以及系统设计。
## 文件夹结构
```
challenge2-jwt-verification/
├── verify_jwt.py ← Part D — working verification script
├── jwt_server.py ← local test server (fixed RS256 version)
├── config.json ← input config (target, strategies, key path)
├── Challenge2_Submission.docx ← full written report (Parts A–E)
└── evidence/
├── jwt_report_*.json ← auto-generated tamper-evident reports
└── screenshots/ ← terminal output screenshots
```
## 设置
```
# 安装依赖
pip3 install flask pyjwt cryptography requests --break-system-packages
# 生成 RSA 密钥对(如果尚未存在)
openssl genrsa -out server_private.pem 2048
openssl rsa -in server_private.pem -pubout -out server_public.pem
```
## 如何运行
### 针对本地测试服务器运行(显示 PASS 结果)
**终端 1 — 启动修复后的 JWT 服务器:**
```
python3 jwt_server.py
```
**终端 2 — 运行验证器:**
```
python3 verify_jwt.py config.json
```
### CLI 标志
```
python3 verify_jwt.py config.json # normal run with full output
python3 verify_jwt.py config.json --quiet # verdict only
python3 verify_jwt.py config.json --verbose # full token and response detail
```
### 退出码
| 代码 | 含义 |
|------|---------|
| `0` | REMEDIATION_VERIFIED |
| `1` | REMEDIATION_FAILED |
| `2` | INCONCLUSIVE |
## 示例输出
```
===== REMEDIATION VERIFICATION REPORT =====
Finding : jwt_algorithm_confusion
Target : https://httpbin.org/get
Timestamp: 2026-03-13T09:00:00Z
[TC-01] Strategy : alg_none
Status : 401 | Time: 0.31s | Sensitive: NO
Result : PASS
[TC-02] Strategy : hs256_with_pubkey
Status : 200 | Time: 0.28s | Sensitive: NO
Result : FAIL -- Server accepted manipulated token
===== VERDICT: REMEDIATION FAILED =====
Failed Tests: 1 / 4
Evidence saved : evidence/jwt_report_20260313T090000Z.json
Report hash : sha256:9f2c1a3b...
```
## A 部分 — 威胁建模分析 [25 分]
### Q1. 什么是算法混淆攻击,为什么它最初能成功?
JSON Web Token (JWT) 是一个由三部分组成的 Base64URL 编码结构:header(标头)、payload(载荷)和 signature(签名)。header 声明了签名算法(例如 alg: RS256),服务器必须仅使用该算法 —— 并根据受信任的密钥进行验证 —— 来认证令牌。RS256 是非对称的:服务器使用其私钥签名,并使用其公钥验证。根据设计,公钥是旨在被共享的。
算法混淆攻击利用了一个关键的错误假设:易受攻击的服务器信任客户端提供的 alg 标头字段来决定使用哪种算法,而不是强制执行服务器端的允许列表。获取了服务器公钥的攻击者可以伪造一个 alg: HS256 的新令牌,并使用 RS256 公钥作为 HMAC secret 对其进行签名。易受攻击的服务器读取 alg: HS256,将公钥视为共享的 HMAC secret,并验证签名 —— 验证成功,因为攻击者正是使用该密钥进行签名的。伪造的令牌因此被作为合法令牌接受。
**根本原因:** 原始代码调用了通用的 `jwt.verify(token, publicKey)` 而未指定 `algorithms: ['RS256']` —— 这一行的遗漏导致算法被降级为攻击者完全控制的对称方案。
### Q2. 修复仍不完整或可能被绕过的五种不同方式:
| # | 绕过向量 | 机制 |
|---|--------------|-----------|
| 1 | alg: none 被接受 | 如果服务器未明确拒绝,某些 JWT 库接受 'none' 作为有效算法,从而完全绕过签名验证。 |
| 2 | kid 标头注入 | 如果服务器使用 kid (Key ID) 标头从文件系统路径查找密钥,攻击者可以注入 kid: '../../dev/null' 并使用空的 HMAC secret 进行签名。 |
| 3 | 库级 Bug | 旧版本的 PyJWT (<2.4)、jsonwebtoken (<9.0) 和 golang-jwt 存在 Bug,导致算法强制执行对混合大小写的 alg 值(例如 'Hs256' 对 'HS256')失效。 |
| 4 | 回退代码路径 | 先于 RS256 补丁存在的旧版端点、调试路由或中间件层可能仍接受 HS256 令牌,从而在非主要路径上绕过修复。 |
| 5 | 响应缓存 | 如果负载均衡器或 CDN 缓存了修复部署前的 200 OK 响应,即使后端现在正确拒绝了令牌,攻击者仍能收到缓存的正常响应。 |
### Q3. 声明修复成功所需的三个可衡量条件:
1. **条件 1 — 算法强制执行已验证:** 对于每个提交了 alg: HS256、alg: none、alg: ''(空白)或任何非 RS256 算法的令牌,服务器均返回 HTTP 401,且需在所有端点上确认。
2. **条件 2 — 原始利用被拒绝:** 一个以 alg: HS256 伪造、使用实际 RS256 公钥作为 HMAC secret 签名的令牌(原始 CVE payload),返回 HTTP 401 且响应正文中无敏感数据。
3. **条件 3 — 合法 RS256 令牌被接受:** 一个正确签名的 RS256 令牌继续返回 HTTP 200,确认修复未破坏正常认证。在不破坏功能性的前提下修复安全性是验证的门槛。
### Q4. 24 小时的 JWT secret 轮换能否加强修复?
不能 —— secret 轮换与此特定漏洞无关。Secret 轮换对于对称方案(HS256/HS512)有意义,因为泄露的共享 secret 可以被轮换。然而,RS256 使用非对称密钥对。服务器的公钥不需要保密 —— 根据设计,它是公开的。轮换它并不能阻止攻击,因为攻击者只需要当前的公钥(可在攻击时从 JWKS 端点获取)即可伪造新的 HS256 令牌。根本问题在于服务器接受算法降级。在严格的算法强制执行到位之前,secret 轮换不提供额外保护。
## B 部分 — 测试用例设计 [25 分]
针对 FIND-0087 的最小可行测试套件。目标:`GET /api/v1/admin/users`。预期拒绝代码:401。
| 测试 ID | 类别 | Token 修改 | 预期(易受攻击) | 预期(已修复) | 通过条件 |
|---------|----------|-------------------|----------------------|-----------------|----------------|
| TC-01 | alg:none 攻击 | 设置 alg='none',去除签名 | 200 OK — 返回 admin 列表 | 401 Unauthorized | 状态码 401,body 中无敏感数据 |
| TC-02 | 原始利用 (client-claim) | alg='HS256',使用 RS256 公钥作为 HMAC secret 签名 | 200 OK — 返回 admin 列表 | 401 Unauthorized | 状态码 401;确认仅执行 RS256 |
| TC-03 | alg 标头移除 | 从标头中完全移除 alg 字段 | 200 OK 或 500 | 401 Unauthorized | 状态码 401,无 500 服务器错误 |
| TC-04 | kid 标头注入 | 注入 kid='../../dev/null',使用空 HMAC 签名 | 200 OK (null-key bypass) | 401 Unauthorized | 状态码 401,kid 字段被拒绝 |
| TC-05 | 过期 Token | 设置 exp=0, iat=0,保留原始 RS256 签名 | 200 OK (exp 未检查) | 401 Unauthorized | 状态码 401;确认有效期验证 |
| TC-06 | 篡改 Payload (角色提升) | 在 payload 中修改 role='admin';签名无效 | 200 OK (签名未验证) | 401 Unauthorized | 状态码 401;确认签名完整性强制执行 |
| TC-07 | 空白 alg (库级) | 设置 alg='' (空字符串);测试 PyJWT/jsonwebtoken 大小写折叠 Bug | 200 OK (库 Bug 绕过检查) | 401 Unauthorized | 状态码 401;在库级别拒绝空白 alg |
| TC-08 | RS256 密钥不匹配 | alg=RS256 正确但使用错误的私钥生成签名 | 200 OK (签名未验证) | 401 Unauthorized | 状态码 401;确认加密验证 |
## C 部分 — AI 辅助工作流 [20 分]
### 使用的 Prompt
### 原始 AI 输出(逐字)
```
import jwt, requests, base64, json, hmac, hashlib
def test_jwt_strategies(token, endpoint, strategies):
results = []
for strategy in strategies:
modified = apply_strategy(token, strategy)
resp = requests.get(endpoint, headers={'Authorization': f'Bearer {modified}'})
results.append({'strategy': strategy, 'status': resp.status_code,
'accepted': resp.status_code == 200})
return results
def apply_strategy(token, strategy):
parts = token.split('.')
header = json.loads(base64.b64decode(parts[0] + '=='))
payload = json.loads(base64.b64decode(parts[1] + '=='))
if strategy == 'alg_none':
header['alg'] = 'none'
return encode_token(header, payload, '')
elif strategy == 'hs256_with_pubkey':
header['alg'] = 'HS256'
key = open('public.pem').read()
return encode_token(header, payload, key)
```
### 评判 — 错误或不安全之处
1. `requests.get()` 没有设置 timeout。挂起的请求将无限期阻塞函数。修复:添加 `timeout=10`。
2. 硬编码 `'public.pem'` 路径。密钥路径必须是一个参数,而不是硬编码的假设。
3. 对 JWT 使用带 `'=='` 填充的 `base64.b64decode` 是错误的。JWT 使用省略了可变长度填充的 URL 安全 Base64。盲目添加 `'=='` 会导致解码错误。正确做法:将填充计算为 `4 - len(s)%4`。
4. HMAC 使用 `secret.encode()` 调用 —— 对于二进制 PEM 密钥会失败。RS256 公钥是二进制 PEM 数据,而不是 UTF-8 字符串。必须作为字节读取并直接传递。
5. 缺少错误处理。如果策略名称未知,`apply_strategy()` 静默返回 None,导致 `encode_token()` 崩溃。
6. `alg:none` 令牌获得了有效的 HMAC 签名。正确的 `alg:none` 令牌必须具有空的签名字段(仅有一个尾随点),而不是计算出的签名。
7. `tamper_role_to_admin` 使用空字符串 HMAC。应将签名保留为 `'invalidsignature'` 以测试服务器是否验证签名,而不是使用空的 secret 签名。
8. 没有检查响应正文中的敏感数据。200 状态并不是唯一的失败信号 —— 服务器可能返回 200 并携带泄露的数据。
### 修正版本
修正后的实现位于 `verify_jwt.py` (D 部分)。主要改进:带正确填充的 URL 安全 base64;公钥作为原始字节读取;每次请求设置超时;alg:none 产生空签名;篡改策略产生无效签名;敏感数据模式扫描;所有未知策略引发 ValueError;每个测试用例的完整结构化输出。
## D 部分 — 实现冲刺 [20 分]
工作脚本以 `verify_jwt.py` 形式提交。它接受 JSON 配置文件并支持所有四种必需策略以及四种额外策略(kid 注入、alg 标头移除、空白 alg、RS256 错误密钥)。异常检测涵盖状态码、响应时间(>3s)和敏感数据模式。
**已实现额外功能:** 证据 JSON 和 SHA-256 哈希在每次运行时自动保存到 `evidence/` 目录。
### 异常检测
| 信号 | 条件 |
|--------|-----------|
| BEHAVIORAL | 状态码 != 预期拒绝代码 |
| TEMPORAL | 响应时间 > 3 秒 |
| CONTENT | 在响应正文中发现敏感数据模式 |
## E 部分 — 极限系统设计 [10 分]
*字数:约 185 词(在 150-200 限制内)*
该管道应采用策略模式,并设置一个中央注册表,将发现类型映射到验证器类。核心引擎加载一条发现记录,读取其类型字段,查找已注册的策略,并将所有测试生成、执行和异常检测委托给该策略类。添加新的发现类型只需:(1) 创建一个继承自共享 BaseVerifier 接口的新策略类,(2) 用一行代码注册它。核心引擎永远不需要更改。
所有策略必须共享一个标准化的约定,涵盖五个维度:输入 schema(包含必填字段的发现记录)、测试用例结构(test_id, category, payload, expected_status)、异常信号(行为:状态码;时间:p95 超标;内容:body 哈希或金丝雀)、结果格式(PASS / FAIL / ERROR)以及证据 schema(带 SHA-256 哈希的 JSON 报告)。特定于发现的信号 —— SSRF 的 OOB 回调、反序列化的利用链触发器 —— 是策略内部的覆盖,对核心不可见。这种分离确保引擎无需修改即可扩展到任何漏洞类别。
## 提交清单
- [x] A 部分 — 威胁建模书面回答 (Q1, Q2, Q3, Q4)
- [x] B 部分 — 表格格式的 8 个测试用例
- [x] C 部分 — 使用的 AI Prompt + 原始输出 + 评判 + 修正代码
- [x] D 部分 — 可工作的 `verify_jwt.py` 及示例输出 + 额外证据哈希
- [x] E 部分 — 系统设计回答(约 185 词,在 150-200 限制内)
- [x] 额外功能 — 证据 JSON + SHA-256 哈希自动保存到 `evidence/`
- [x] 截图 — `evidence/screenshots/` 中的终端输出截图
标签:Flask, JWT安全, PyJWT, Python, RS256, Web安全, 威胁分析, 安全测试工具, 对称加密, 无后门, 漏洞修复验证, 算法混淆漏洞, 网络安全, 自动化侦查工具, 自动化审计, 蓝队分析, 逆向工具, 隐私保护