CorpGate
题目概述
本题是一条精心设计的三段式攻击链:原型链污染触发 JWT 签名密钥轮换 → 伪造 Admin Token → 通过合法 Admin 接口获取一次性诊断引用 → 执行命令读取 Flag。题目综合考察了 Node.js 原型链污染、JWT 安全机制以及代码审计能力。
解题思路总览
1 | 普通用户注册/登录 |
关键源码分析
1. 设置接口 — 原型链污染入口 (routes/settings.js)
1 | // routes/settings.js(关键片段) |
利用点:sanitizeKey 只过滤了 .,没有过滤 constructor 和 prototype。当传入 {"constructor":{"prototype":{"pending":"EVIL_KEY"}}} 时,deepMerge 会将 EVIL_KEY 写入 Object.prototype.pending,从而污染所有对象的原型链。
2. 健康检查接口 — 密钥轮换触发器 (routes/system.js)
1 | // routes/system.js(关键片段) |
利用点:{}.pending 在正常情况下为 undefined,但由于我们在上一步中污染了 Object.prototype.pending,此处取到了我们注入的恶意密钥 EVIL_KEY,并将其赋值给 config.jwtSecret。这是整条攻击链的核心枢纽。
3. 报告执行接口 — Flag 读取端点 (routes/diagnostic.js)
1 | // routes/diagnostic.js(关键片段) |
利用点:该接口需要 authMiddleware + adminMiddleware 双重鉴权,且需要一个有效的 reference。注意 config.diagnosticStore 是通过 Object.create(null) 创建的(经实测原型链污染对其无效),因此不能通过污染注入 entry,必须找到合法创建途径。
4. Admin 面板 — 合法创建 Diagnostic Entry (routes/admin.js)
1 | // routes/admin.js(完整源码) |
利用点:这是唯一合法创建 diagnostic entry 的端点。访问 GET /admin 时,服务端自动生成 tokenId 并写入 diagnosticStore,同时在渲染的 HTML 页面中通过 stats.reference 泄露该值。我们只需用伪造的 Admin JWT 访问此页面,提取 reference,即可在 120 秒内用于 /api/reports/execute。
完整 Exp
1 | import jwt, json, ssl, socket, re, time |
攻击链总结
| 步骤 | 接口 | 漏洞/机制 | 作用 |
|---|---|---|---|
| ① | POST /api/settings |
sanitizeKey未过滤 constructor.prototype |
污染 Object.prototype.pending |
| ② | GET /api/system/healthcheck |
{}.pending读取被污染的原型属性 |
将 jwtSecret替换为攻击者控制的密钥 |
| ③ | 本地 | PyJWT + 已知密钥 | 伪造 role:admin的 JWT Token |
| ④ | GET /admin |
合法创建 diagnosticStoreentry 并在 HTML 中泄露 reference |
获取一次性有效引用 |
| ⑤ | POST /api/reports/execute |
持有效 reference + Admin 权限 | 触发 execSync('/readflag')拿到 Flag |
踩坑记录
- 原型链污染对
**Object.create(null)**无效:config.diagnosticStore使用Object.create(null)创建,没有原型链,因此无法通过污染直接注入 entry。这是本题最大的陷阱,必须通过代码审计找到/admin这个合法写入端点。 **sanitizeKey**** 的绕过**:仅移除.而不过滤constructor/prototype/__proto__是典型的原型链污染绕过场景。- Reference 的一次性与时效性:entry 有 120 秒 TTL 且只能使用一次,提取后必须立即调用 execute 接口。
TaxManager
本题考察 XML 外部实体注入(XXE)漏洞的利用。由于目标环境存在 WAF 且无回显(Blind XXE),无法直接在页面中读取文件内容。需要通过加载外部恶意 DTD 文件,结合 OASTify 等带外测试平台,将敏感文件(flag.txt)的内容通过 HTTP/DNS 请求外带到攻击者服务器上。
解题思路
- 绕过 WAF:直接将恶意实体定义写在本地 XML 中容易被拦截,因此采用“外部 DTD 引用”的方式,将核心 Payload 托管在 GitHub Gist Raw 链接上,实现 Payload 与触发代码分离。
- 解决换行符报错:初次尝试将 flag 拼接到子域名进行 DNS 外带时,遇到
Illegal character found in host: LF错误。原因是 Linux 下文本文件末尾自带换行符(\n),导致域名解析失败。 - 优化外带方式:将外带方式从“DNS 子域名拼接”改为“HTTP URL 参数传递”,使换行符被自动编码为
%0A,从而避免主机名非法字符问题,成功实现数据外带。
利用过程
Step 1: 准备外部恶意 DTD
在 GitHub Gist 创建 evil.dtd,初始版本尝试通过 DNS 子域名外带:
1 | <!ENTITY % file SYSTEM "file:///flag.txt"> |
⚠️ 踩坑记录:提交后靶场返回 Error parsing XML: Illegal character found in host: LF。因为 flag.txt 内容包含换行符,拼接到域名位置时破坏了 URL 结构。
Step 2: 修复 Payload 并更新 Gist
将外带位置从子域名调整为 URL 查询参数,修改 evil.dtd 如下:
1 | <!ENTITY % file SYSTEM "file:///flag.txt"> |
原理:作为 URL 参数时,换行符会被 HTTP 客户端自动编码为 %0A,不再影响主机名解析,同时 Burp Collaborator 仍能完整捕获请求内容。
Step 3: 构造纯净 XML 触发漏洞
使用 GitHub Raw 链接引用更新后的 DTD,向靶场提交以下 XML:
1 |
|
Step 4: 获取 Flag
提交后立即在 Burp Suite Collaborator 中点击 Poll now,成功捕获一条 HTTP 请求,Flag 出现在 ?f= 参数中(去除末尾的 %0A 即为最终答案)。
知识点总结
| 技术点 | 说明 |
|---|---|
| Blind XXE | 无回显场景下,必须依赖 OOB 通道外带数据 |
| 外部 DTD 绕过 WAF | 本地仅保留无害的引用语句,恶意逻辑托管在外部 CDN |
| LF 字符处理 | 文件读取结果含换行符时,禁止拼接到域名位置,应改用 URL Path/Query 或 POST Body 外带 |
| 参数实体嵌套 | %eval;中定义新实体时必须使用 %转义百分号,这是 DTD 语法强制要求 |
InkVerse
本题是一条精心设计的五段式攻击链:/api/tip 并发条件竞争突破声望阈值 → 晋升 Reviewer 权限 → 自审自批文章并导出获取 Feature-Token → 提交特色文章申请触发后台异步审批 → 刷新公告缓存暴露高权限 Flag。题目综合考察了并发条件竞争、权限链设计缺陷、异步任务状态轮询以及服务端缓存机制利用能力。
解题思路总览
1 | 普通用户注册/登录 (Balance=200, Reputation=0) |
关键源码分析
1. 打赏接口 — 条件竞争入口 (/api/tip)
1 | // 伪代码还原(基于行为推断) |
利用点:余额校验 (if (user.balance < amount)) 与实际扣款 (user.balance -= amount) 之间没有加锁或事务保护。当同时发送大量请求时,多个请求可能同时通过余额检查(此时余额仍为正),导致余额被扣成负数,但声望持续累加。初始 Balance=200,正常最多 Tip 20 次获得 40 声望,无法达到 Reviewer 所需的 50 声望门槛。通过并发竞争可绕过此限制。
2. 文章导出接口 — Token 泄露源 (/api/export)
1 | # 导出文件内容格式 (exports/export_{job_id}_{article_id}.txt) |
利用点:导出文件中包含 Feature-Token 字段,该值是服务端为每篇已发布文章生成的签名凭证。此 Token 并非用于解密或下载验证,而是作为 /api/review/feature 接口的 signature 参数,证明当前 Reviewer 确实有权对该文章发起特色申请。这是整条攻击链中凭证传递的关键环节。
3. 特色文章申请 — 异步处理端点 (/api/review/feature)
1 | // 伪代码还原(基于行为推断) |
利用点:该接口采用异步处理模式,提交后立即返回 “submitted”,实际审批由后台 Worker 完成。必须通过 /api/review/feature/status?article_id=N 轮询确认 status: approved 后,才能进行下一步。许多选手在此处误以为提交即生效,忽略了状态确认环节。
4. 公告缓存刷新 — Flag 暴露触发器 (/api/bulletin/refresh)
1 | <!-- /bulletin 页面中的隐藏公告(仅 featured_authors 可见) --> |
利用点:Bulletin 页面使用了服务端缓存机制。即使文章已被标记为 Featured,直接访问 /bulletin 仍然返回旧的缓存内容(不包含特邀作者公告)。必须显式调用 POST /api/bulletin/refresh 使缓存失效并重新渲染,新的 featured_authors 可见公告才会出现在响应中。这是获取 Flag 的最后一步,也是最容易被忽略的一步。
完整 Exploit(浏览器控制台操作)
Step 1: 注册并登录普通用户
正常注册账号并登录,进入 Dashboard 确认初始状态:
1 | Balance: 200 |
Step 2: 条件竞争提升声望
在浏览器控制台执行以下代码,同时发送 30 个打赏请求:
1 | Promise.allSettled( |
执行后刷新 /dashboard,确认角色升级:
1 | Balance: -80 |
Step 3: 发布并批准自己的文章
在首页提交一篇新文章,记录文章 ID(例如 10)。然后进入 /review 面板,点击该文章的 Approve 按钮,使其变为 published 状态。
也可直接在控制台调用:
1 | fetch('/api/review/single', { |
Step 4: 导出文章获取 Feature-Token
1 | // 提交导出任务 |
Step 5: 提交特色文章申请并轮询状态
1 | // 使用提取到的 Feature-Token 作为 signature |
Step 6: 刷新公告缓存并获取 Flag
1 | // 刷新缓存 |
最终在 /bulletin 页面中获得 flag
攻击链总结
| 步骤 | 接口 | 漏洞/机制 | 作用 |
|---|---|---|---|
| ① | POST /api/tip |
余额检查与扣款非原子操作 | 并发竞争使 Reputation≥50,晋升 Reviewer |
| ② | POST /api/review/single |
Reviewer 自审自批 | 将自己的文章变为 published 状态 |
| ③ | POST /api/export+ GET /exports/<file> |
导出文件泄露敏感凭证 | 获取 Feature-Token 作为签名参数 |
| ④ | POST /api/review/feature |
异步处理 + 签名验证 | 提交特色文章申请,等待后台审批通过 |
| ⑤ | POST /api/bulletin/refresh+ GET /bulletin |
缓存刷新暴露高权限内容 | 使 featured_authors可见公告生效,获取 Flag |
踩坑记录
- 条件竞争是前置必要条件:初始 Balance=200,单次 Tip 消耗 10 点、增加 2 声望,正常上限为 40 声望,永远无法达到 Reviewer 门槛 50。必须通过并发竞争打破这一数学限制。浏览器控制台
Promise.allSettled发送 30 个并发请求即可稳定触发。 - Feature-Token 不是下载密钥:导出文件中的
Feature-Token容易误导选手尝试用它解密文件或作为 Auth Header 使用。实际上它是/api/review/feature的signature参数,用于服务端验证请求合法性。 - 异步任务必须轮询状态:
/api/review/feature返回 “submitted for background processing” 仅代表入队成功,不代表审批完成。必须通过/api/review/feature/status?article_id=N轮询直到status: approved,否则刷新 Bulletin 时文章尚未被标记为 Featured,隐藏公告不会出现。 - Bulletin 缓存不会自动失效:即使文章已成功 Featured,直接访问
/bulletin仍返回旧缓存。必须显式调用POST /api/bulletin/refresh触发缓存重建。公告页面提示 “显示缓存结果。缓存后你的可见性可能发生了变化” 是关键线索。 - API 文档优先于逆向猜测:题目壳子提示了
/api/docs可查看完整接口文档。如果一开始就读取文档,可以直接获知/api/review/feature的参数格式和/api/export/status的存在,避免大量无效路径探测。