Web 应用的竞态条件(Race Condition)与 TOCTOU 漏洞测试指南。适用于测试一次性操作、高并发 HTTP 滥用、突破频率限制(Rate-limit bypass)、Turbo Intruder 闸门控制(gates)、HTTP/2 单数据包攻击(single-packet attacks)以及 CWE-362 类型的同步漏洞。
SKILL:竞态条件 — 测试与利用实战手册
AI 加载指令:请将竞态条件(Race Condition)视为权限/状态完整性问题:非原子的“先读后写”(read-then-write)操作会让多个并发请求读取到旧状态。优先测试一次性或余额类操作。将并行传输技术(HTTP/1.1 尾字节同步、HTTP/2 单数据包攻击、Turbo Intruder 闸门控制)与应用层证据(重复的成功响应、余额异常、账单重复落库)相结合。仅限授权测试。 路由提示:对于业务工作流、优惠券、库存或一次性奖励,优先加载本 Skill,并交叉加载
business-logic-vulnerabilities。
0. 快速上手 — 优先测试什么
重点针对**检查(check)与更新(update)**未在一个原子化数据库事务中完成的端点:
| 优先级 | 操作类型 | 示例路径 / 参数 |
|---|---|---|
| 1 | 一次性兑换 / 优惠券 / 奖励领取 | redeem, apply_coupon, claim_reward, voucher |
| 2 | 余额 / 额度 / 库存扣减 | transfer, purchase, reserve, inventory |
| 3 | 邀请 / 裂变 referral / 注册奖励 | invite_accept, referral_claim |
| 4 | 密码 / 邮箱 / MFA 验证 | verify_token, confirm_email, reset_password |
| 5 | 未使用强主键的伪幂等 API | 单个用户理论上只能成功一次的 POST 请求 |
第一步动作(概念性):
- 在代理工具中截获引发状态变更的请求。
- 在工具能力范围内,尽可能同步发送 20~100 个重复请求。
- 归类测试结果:预期仅 0/1 次成功 vs N 次成功 或 最终状态不一致。
1. 核心概念
1.1 TOCTOU(检查时间到使用时间间隔漏洞)
线程 A 线程 B
| |
+-- 检查 CHECK (资源有效) |
| +-- 检查 CHECK (资源有效) ← 双方均判定“有效”
+-- 使用/更新 USE / UPDATE |
| +-- 使用/更新 USE / UPDATE ← 造成双重/重复生效
TOCTOU 指的是**逻辑判断(检查)与数据变更(使用)**并非不可分割的单一原子步骤。
1.2 非原子的“先读后写”(Read-Then-Write)
典型的漏洞伪代码逻辑:
balance = SELECT balance FROM accounts WHERE id = ?
if balance >= amount:
UPDATE accounts SET balance = balance - ? WHERE id = ?
两个并发请求在任意一个 UPDATE 提交之前,均能顺利通过 if 校验。
1.3 数据库层与应用层的锁机制漏洞
| 层级 | 问题根源 |
|---|---|
| 应用层(Application) | 内存标志(In-memory flag)、缓存或 Session 仍显示“未使用”,而数据库其实已更新 — 反之亦然。 |
| ORM / 服务层 | 部署了多实例但缺少分布式锁;各实例均认为自己掌握了决策权。 |
| 数据库层(DB) | 缺少 SELECT … FOR UPDATE 显式锁、事务隔离级别错误,或业务逻辑散落在多个非事务语句中。 |
| API 网关层 | 基于 IP 的限流逻辑采用先检查后递增策略 — 并发突发请求绕过了重复检查。 |
提示:UNIQUE 唯一性约束与幂等键(Idempotency keys)通常能直接打掉整类漏洞 — 重点测试目标应用是否在核心热点路径上硬性校验了这些机制。
2. 攻击模式
2.1 突破额度上限(重复兑换 / 重复领取)
并行发送多个相同的已认证请求:
POST /api/v1/rewards/claim HTTP/1.1
Host: target.example
Authorization: Bearer <token>
Content-Type: application/json
{"reward_id":"welcome_bonus"}
成功特征:多次返回 HTTP 200/201、数据库产生多条账单明细记录,或账户余额超出了规则允许的上限。
2.2 利用并发绕过频率限制(Rate-Limit Bypass)
如果限流策略被实现为针对每个请求独立检查计数器,且递增操作非原子化:
POST /api/v1/login HTTP/1.1
Host: target.example
Content-Type: application/json
{"email":"victim@example.com","password":"wrong"}
在一个时间波次内并行发送 N 个请求;与 N 个串行请求的结果进行对比。
成功特征:成功提交的失败尝试次数超过了官方公布的封顶值,或者在短时间内高并发冲刷时未触发账号锁定。
2.3 多步骤逻辑利用(打破流水线节奏)
典型工作流:创建订单 → 支付 → 确认。若确认步骤在密码学上未与支付完成强绑定:
- 在同一个 Session/条目下同时开启两条并行流水线。
- 当通道 A 的支付仍处于处理中(in-flight)或被放弃时,在通道 B 上直接完成确认。
成功特征:在无对应支付记录的情况下,商品被标记为已支付/已发货,或流程状态发生异常回退。
3. HTTP/1.1 尾字节同步技术(Last-Byte Synchronization)
原理:挂起所有请求的 Socket,使其发送完除 Body 最后一个字节之外的所有数据;接着瞬间并发释放这最后一个字节,使服务器在极短的时间窗口内密集接收到全部请求。
客户端 1: [请求头 + Body - 1字节] ----挂起----+
客户端 2: [请求头 + Body - 1字节] ----挂起----+--> 一起冲刷释放最后一个字节 (flush)
客户端 N: [请求头 + Body - 1字节] ----挂起----+
优势:相比在 Repeater 中简单手动重放,能极大降低各个请求之间的网络抖动。
工具选型:自定义脚本、部分 Burp 插件,或使用 Turbo Intruder 的 gate 门控模式(详见第 5 节)作为同步释放的具体落地方案。
4. HTTP/2 单数据包攻击(Single-Packet Attack)
原理:在单个 HTTP/2 连接中复用(Multiplex)多个完整的 HTTP/2 流(Streams),并将它们的帧(Frames)合并在一起,确保所有请求的首字节能在同一个 TCP 报文段(Segment)(或极其微小的间隔)中挤出网卡。服务端接收调度器随后会以毫秒级别以下的时间间隔处理这些请求。
Burp Repeater(现代工作流):
- 打开多个标签页或选中多个请求。
- 使用右键功能 Send group (parallel) / single-packet attack(单包攻击模式)。
- 如果目标支持,优先强制走 HTTP/2 协议。
[ 请求 A 流 ]
[ 请求 B 流 ] --HTTP/2--> 单次突发 (one burst) --> 应用工作线程池
[ 请求 C 流 ]
相比 HTTP/1.1 尾字节技巧的优势:线缆层面的对齐更加紧密,大幅减少了对单连接序列化发送的依赖。
5. TURBO INTRUDER 脚本模板
开源仓库:PortSwigger/turbo-intruder(Burp Suite 扩展插件)。
5.1 模板 1 — 同一端点,闸门(Gate)同步释放
配置参数:concurrentConnections=30, requestsPerConnection=30,使用 gate(闸门)确保所有线程同时触发。
核心模式(压入 N 个请求后统一开闸):
for _ in range(N):
engine.queue(request, gate='race1')
engine.openGate('race1')
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=30,
pipeline=False,
engine=Engine.THREADED,
maxRetriesPerRequest=0
)
for i in range(30):
engine.queue(target.req, gate='race1')
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)
请求头要求(为每个队列中的副本打上唯一标记以供日志关联;Turbo Intruder 的 Payload 占位符):
x-request: %s
当搭配字典(或其它 Payload 来源)时,Turbo Intruder 会自动将每个请求中的 %s 替换掉 — 在将请求从 Repeater 发送到 Turbo Intruder 前,请在基础请求中保留该 Header。HTTP 协议下大小写不敏感;建议使用统一名称以便做日志 Grep 关联。
5.2 模板 2 — 跨多端点,同一闸门
测试模式:1 个针对 target-1 的 POST(引发状态变更)加上多个针对 target-2 的 GET(读取侧),在同一闸门下同时释放,以此拉大 TOCTOU 时间窗口以便观察。
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=30,
pipeline=False,
engine=Engine.THREADED,
maxRetriesPerRequest=0
)
engine.queue(post_to_target1, gate='race1')
for _ in range(30):
engine.queue(get_target2, gate='race1')
engine.openGate('race1')
如果涉及不同的 Host/路径,可通过实例化多个 RequestEngine 来调整端点(Turbo Intruder 支持多 Engine 架构 — 请查阅您使用的 Burp 版本对应的上游文档)。
6. CVE 案例参考 — CVE-2022-4037
CVE-2022-4037(GitLab CE/EE):竞态条件漏洞,导致已验证邮箱地址伪造;当该产品充当 **OAuth 身份提供方(OAuth IdP)**时,还会引发第三方账号关联绑定安全风险。漏洞归类:CWE-362。公开研究表明,通过 HTTP/2 单数据包攻击的时序控制技术,可以在极窄的时间窗口内成功碰撞出该漏洞。
对测试人员的启示:邮箱验证、OAuth 关联绑定以及“所有权确认”工作流都是极高价值的竞态攻击目标 — 远不止优惠券和余额扣减场景。
参考链接(官方 / 客观):
- NVD — CVE-2022-4037
- GitLab 安全公告及对应受影响版本的厂商 CVE JSON 文档
7. 工具箱
| 工具 | 作用 / 场景 |
|---|---|
| PortSwigger/turbo-intruder | Burp 中的高并发重放、**闸门控制(gates)**与 Python 脚本支持。 |
| JavanXD/Raceocat | 专为竞态测试设计的 HTTP 客户端模式(使用前请确认与自身技术栈的兼容性)。 |
| nxenon/h2spacex | HTTP/2 底层 / 单数据包风格攻击测试工具(请合规使用,仅限授权目标)。 |
| Burp Suite — Repeater | 内置 Send group (parallel) / single-packet attack 组合发送功能,实现多请求同步。 |
8. 测试决策树
起始:是否为引发状态变更的 API?
|
否 -----------+---------- 是
| |
终止测试 是否涉及一次性操作/余额/身份验证?
|
+-------------------------+-------------------------+
| | |
类优惠券场景 频率限制 (Rate Limit) 多步骤工作流
| | |
并发发送相同请求 并发 vs 串行对比 并发多条流水线
| | |
是否重复成功? 是否突破限制? 状态是否不匹配?
/ \ / \ / \
是 否 是 否 是 否
| | | | | |
输出报告 + 尝试 HTTP/2 输出报告 + 尝试 TI 输出报告 + 深入分析
收集证据 单数据包攻击 收集证据 闸门控制 收集证据 单步细节
| | | | | |
+----+----+ +----+----+ +----+----+
| | |
选工具 选工具 选工具
v v v
Burp 分组 / h2spacex TI 闸门 / Raceocat TI + 追踪 ID
如何确认漏洞(漏洞证据 Checklist):
- 高并发场景下可稳定复现重复成功,而非偶然的单次重试波动。
- 存在服务端落库证据:产生了两条记录、发送了两封邮件、赋予了两次权限,或最终余额数值异常。
- 结合
x-request(或类似)请求标记,或唯一的 Body 参数进行关联验证。






