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安全, 威胁分析, 安全测试工具, 对称加密, 无后门, 漏洞修复验证, 算法混淆漏洞, 网络安全, 自动化侦查工具, 自动化审计, 蓝队分析, 逆向工具, 隐私保护