如果你也在刷漏洞赏金大概遇到过这种情况一个功能看起来完全正常单发每一个请求都没问题可一旦把几个请求同时打过去业务状态就乱了。比如优惠券被重复领取、一次支付被扣成负数、一个邀请码绑定了十个人。这些十有八九是竞态条件漏洞。竞态条件漏洞也叫竞争条件、Race Condition本质是并发场景下多个请求同时读写同一份共享数据时产生的逻辑错乱。它对漏洞赏金猎人来说价值很高扫描器基本扫不出来厂商文档里写得也少而且一旦挖到往往直接就是高危甚至严重等级。这篇文章我按自己的实战路径把竞态条件的原理、工具选型、挖掘技巧、报告写法以及我在各平台踩过的坑一次讲透。适合刚接触漏洞赏金的同学也适合做渗透测试但没系统整理过并发类漏洞的人。1. 竞态条件漏洞的核心原理与赏金价值1.1 从一次“余额双花”说起到底什么是竞态条件先不急着背定义我用一个生活场景来说明。想象一个只允许一个人进入的房间门口保安的逻辑是“先看你有没有票有票就开门然后立刻把票作废”。正常情况没问题。但如果两个人同时把票递到他面前他在一瞬间看到两张“还没作废”的票于是两个人一起进去了。这就是典型的“检查后使用”时间窗口Time-of-Check to Time-of-Use简称 TOCTOU。安全检查和使用资源之间存在一个可被穿插的间隙攻击者利用这个间隙让两个请求都通过了校验。放到 Web 业务里更直观。比如转账接口的后端逻辑是先读取用户余额判断是否够扣然后执行扣款。在单请求场景下100 块扣 60 块余额变成 40完全正常。但攻击者同时发出两个扣款请求两个请求都读到余额 100都通过了“余额充足”的校验最后余额变成 -20。这就是余额双花资金类系统最怕的问题之一。竞态条件的本质是系统对“共享资源”的更新不是原子的。多个执行流交错执行后读的数据覆盖了先写的结果或者校验阶段和更新阶段之间插入了别的请求。在 Web 漏洞里常见触发位置大多是先查再更新select-then-update、先查再插入select-then-insert、先判断状态再变更状态check-then-act。这些模式在代码里很难看出一眼的问题但并发一上来就露馅。1.2 为什么赏金猎人热衷挖它价值分析我一开始对竞态条件并不上心觉得报上去也就是个 Low 级别的逻辑 bug。后来在一个众测项目里靠一个礼品卡重复兑换的竞态拿到了一笔不错的奖金才意识到自己之前错过了一个多好的方向。梳理下来它至少有四个点特别吸引赏金猎人。第一影响面大。凡是涉及计数、余额、状态流转的业务都可能存在竞态。优惠券、积分、签到、抽奖、转账、订单、邀请奖励、绑定手机号邮箱哪个平台没有这些功能这类接口数量庞大挖到哪里都可能有戏。第二自动扫描器几乎无效。常规漏洞扫描器靠发送畸形 Payload、检测响应差异来工作但竞态条件没有固定的请求特征它考验的是“你能不能理解业务的数据流”。因此这个方向竞争者少很多漏洞赏金新手甚至老手都会自动跳过“多发几次请求”这种简单思路。第三影响评级容易拿高。如果竞态导致资金损失、库存超卖、账号绑定被劫持对应到 Bug Bounty 平台通常直接是 High 或 Critical。相比费劲挖一条存储型 XSS它的利用链往往更短说服力更强。第四复现门槛相对低。很多注入类漏洞需要绕过 WAF、构造复杂 Payload竞态条件只要有并发工具配合一点点业务理解就能稳定复现。尤其是 HTTP/2 普及后多个请求几乎可以同时到达服务器触发概率比过去高了不少。我对比过一组数据同一个兑换接口用 HTTP/1.1 并发成功率在 30% 到 60% 之间浮动换成 HTTP/2 多路复用后很多时候能接近 80% 以上。下面这个对比表能更清楚地看出扫描器和人工思路的差异维度扫描器视角赏金猎人视角发现依据请求特征、签名、已知指纹业务资源更新方式、并发时序核心能力快速遍历参数与 Payload读懂接口逻辑识别共享状态复现思路单请求对比响应多请求并发制造时间窗口结果判定响应内容差异业务数据变化、重复资源分配上手难度配置完就能跑需要写少量脚本和理解逻辑1.3 竞态条件和附近几个漏洞概念的边界刚开始接触的人容易把竞态条件和越权、幂等性缺失、拒绝服务混淆我实际写报告时也被厂商来回问过这里把边界说清楚能省不少来回扯皮的功夫。越权漏洞的核心是“访问控制失效”比如普通用户调了管理员接口。竞态条件有时会造成越权效果但根因不是权限校验缺失而是时间窗口内状态未被正确锁定。两者修复方式不同一个改鉴权一个改并发控制。幂等性缺失指的是同一个请求重放多次业务上应该只生效一次结果却生效了多次。竞态条件可以看作“幂等性在并发场景下的失效”。很多重复领取类漏洞厂商会辩解“我们已经加了幂等逻辑”如果你能证明并发下幂等形同虚设那报告的含金量会更高。至于死锁、性能瓶颈这类并发问题安全上一般不关心性能本身但可以从响应时间、慢查询日志里反向推断后端是否存在加锁或串行化这在“复现率不稳定”时很有用。2. 挖掘前的准备与工具选型2.1 先读懂目标业务再动手三个关键问题直接拿 Burp 对着目标乱发一百个请求大概率什么都触发不了。竞态条件挖掘的第一步不是“发请求”而是“定位共享资源”。我自己会先问三个问题第一个问题哪个操作在更新同一份资源比如一个优惠券兑换接口它读的是这个券的状态一个转账接口它读的是账号余额一个绑定手机号接口它读的是用户和手机号的关联关系。找出“同一份资源”是关键后续所有并发请求都围绕它展开。第二个问题有没有一次性的标识或限制接口参数里有 token、code、inviteCode、orderId或者业务上写着“每人限领一次”“每笔订单只能用一张券”这些字眼都在暗示存在“检查一次、使用一次”的状态转换竞态常常就藏在这里。第三个问题检查与更新之间隔了多远我用 Burp 抓包时会特别看重两个时间点服务端校验参数到真正写库之间的业务逻辑。外部请求很难直接看到服务端代码但可以通过响应延迟来猜测。如果接口返回时间在 50ms 到 200ms 之间说明中间可能做了不少业务处理时间窗口相对充足。有的接口响应飞快比如 1ms 就返回那并发请求的到达顺序稍有偏差窗口就过了需要用更精细的工具。有条件做白盒审计的话直接搜代码里的 read-then-update 模式尤其注意没有加锁的查询、没有唯一约束的插入。一次我在代码里看到一个函数先查用户是否已领取再 insert 一条领取记录中间没有事务也没有锁我当时就意识到这里能打最后果然稳定复现重复领取。2.2 工具链怎么选Burp 为主脚本为辅工具不在多关键是用对。我在不同阶段分别用过 Burp Repeater、Intruder、Turbo Intruder 和自写 Python 脚本这里把各自的定位说清楚。Burp Repeater 只适合做单请求调试不适合当竞态利用工具。它的“发送”按钮是顺序发送即使你手动快速点两次两次请求之间也隔了人的操作时间根本不算并发。如果只是验证接口字段可以用它但验证竞态必须换工具。Burp Intruder 是一个相对容易上手的并发工具。方法也很简单选中目标请求把 payload 位置留在无关的参数上比如把请求里的任意值替换成 §x§使用一个空 payload 列表在“Resource Pool”里设置并发线程数然后跑一次。它能在短时间里打出几十个并发请求对很多场景已经够用。缺点是控制粒度不够细无法精确控制“同一时刻开门”也没法做多阶段请求的复杂编排。Turbo Intruder 是真正干这活的利器。它是 Burp 扩展用 Python 脚本控制请求队列可以做到“先把一堆请求排队再统一放行”也能玩 HTTP/2 流水线。我在实际测试里遇到普通 Intruder 复现不稳定的场景换成 Turbo Intruder 后成功率明显提升。原因在于它能控制并发连接数、每连接请求数还能用 gate 机制同步发送时机时间窗口不会被网络抖动打散。Python 脚本适合需要自定义逻辑的时候比如先登录、再获取 token、再并发打目标接口或者需要循环多轮统计成功率。requests 库加 ThreadPoolExecutor 就能完成大部分任务但如果要精细控制连接创建推荐 httpx 或直接用底层 http.client这样能复用连接池。自己做 Agent 式测试时脚本能输出更详细的响应日志便于后续写报告。工具并发强度控制粒度适用场景Burp Repeater极弱手动顺序接口调试、参数分析Burp Intruder中等线程数、无精确定时快速验证、简单重复Turbo Intruder强请求队列、流水线、gate 开关精确并发、高频率复现Python 脚本强完全可控复杂流程、多阶段利用、批量统计2.3 搭建可控靶场验证思路本地先跑通在任何目标上测竞态之前我都建议先在本地搭一个最小靶场把原理和脚本跑通再上授权目标验证。一方面能确认自己的请求构造没问题另一方面也避免在没有充分理解的时候拿真实业务当试验田被平台判定为滥用测试。下面是一个用 Flask 写的简化版“领取优惠券”接口逻辑就是典型的 check-then-act。它先检查 user 是否已经领取没领取就进入一个耗时操作然后发放奖励。并发请求打进来时多个线程都能通过检查。from flask import Flask, request, jsonify import time app Flask(__name__) claimed_users {} app.route(/claim, methods[POST]) def claim(): user request.args.get(user) if user in claimed_users: return jsonify(code1, msgalready claimed) # 模拟真实业务中的复杂校验与耗时 time.sleep(0.05) claimed_users[user] True return jsonify(code0, msgclaim success) if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)在这个接口上用 Python 脚本并发打 20 个请求理论上如果竞态存在成功领取的次数会大于 1。注意一点上面的代码用了全局字典实际生产环境大多是数据库层面的竞争本地验证时最好把claimed_users换成 SQLite 或 PostgreSQL否则 Python 的全局解释器锁可能让并发效果不明显。# 快速验证用 xargs 并发发 20 个请求 seq 1 20 | xargs -P 20 -I {} curl -s -X POST http://127.0.0.1:5000/claim?usertest正常情况只会有一个 “claim success”其余都是 “already claimed”。如果出现多个 “claim success”恭喜这就是一个可以在授权目标上寻找同类模式的好思路。3. 实战挖掘技巧与利用链分析3.1 高发场景一优惠券、礼品卡、签到奖励被“薅秃”这是我个人遇到最多的竞态类型几乎每个有营销活动的平台都可能有。接口通常长这样前端点击“领取”后端收到一个优惠券编码或用户 ID然后校验这个券是否未使用、这个用户是否已领过最后把券标记为已使用并发放到用户账户。请求看起来像这样POST /api/v1/coupon/redeem HTTP/1.1 Host: target.com Content-Type: application/json {code: SUMMER2024, userId: 10001}如果这个接口在校验“code 是否已被使用”和“更新 code 状态为已使用”之间没有原子保护那么并发发送多个相同的兑换请求就可能出现多个人都兑换成功或者同一个人多次领取同一张券。实操时我的经验是用 10 到 50 个并发请求做一轮测试而不是一上来就发 1000 个。请求太多容易被服务端的限流拦截而且也会给对方系统制造不必要的压力影响平台对你的评价。发完之后重点看几个地方是否出现多个成功响应、响应中的业务码是否都是 0、优惠券列表里是否出现多张相同编码的券、数据中心是否产生了多条领取流水。有一个很关键的细节有时候响应码全是 200但只有第一条是“兑换成功”后面全是“已兑换”这种情况不算漏洞因为后端幂等逻辑生效了。真正的问题是“响应标记为已兑换但用户却收到了多次发放”或者“多条请求都返回兑换成功”。判断依据永远是业务结果不能只看 HTTP 状态码。这类漏洞的修复通常要求在数据库层面做唯一约束或者在更新语句里加上条件例如UPDATE coupons SET statusused WHERE statusunused同时判断影响行数。但作为挖洞的人我们关心的是怎么稳定触发以及怎么把影响写清楚。3.2 高发场景二转账、支付、限额绕过资金类接口的竞态危害远高于优惠券。它的典型模式是先读取账户余额校验是否足够支付或是否超过单笔限额然后执行扣款和加款。由于扣款和加款往往涉及到多个账户、多张表甚至有独立的账务服务从校验到最终落库的时间跨度会更大窗口也更容易被利用。最常见的利用有两个方向。第一个是重复提现或重复支付账户里有 100 块同时发起 10 笔 50 块的提现正常逻辑下只有 2 笔能成功但竞态条件下可能 10 笔全部成功账户变成 -400。第二个是限额绕过系统规定单笔转账上限 1000攻击者同时发起 5 笔 1000 的转账每一笔校验时都“看起来”没有超过累计限额最后总额 5000 被转出。测试这类接口时首先要确认收款方参数可以改变还是固定为当前登录用户。如果是固定为当前用户并发转账时反复向自己的账户打钱可能没有实际利益但不代表没有风险有些系统会把“给自己转账”和“给他人转账”用同一个逻辑校验部分可以复用。其次要注意后端可能已经做了悲观锁比如SELECT ... FOR UPDATE这种情况下并发请求会变成串行执行导致你发再多请求也只有一笔成功。如果遇到这种场景不必死磕记下“存在锁防护”作为结论即可。但有时候开发者的锁只加在了账务核心服务上外层的优惠、积分、流水接口却没加。我遇到过转账限额被绕过的情况是因为限额校验在网关层做而实际扣款在另一个服务两个服务之间的数据同步存在延迟攻击者在延迟窗口内多发请求一样能绕过。这种隐蔽的分布式竞态才是真正考验功力的地方。写报告时资金类竞态的文案要特别强调“直接资金损失”或“库存超卖”这是评级的关键。比如攻击者可在短时间内并发提交多笔提现请求导致同一笔余额被扣减多次账户余额可变为负数配合提现流程可造成平台资金损失。3.3 高发场景三账户操作与身份绑定逻辑账户类接口的竞态表面上没有资金类那么直观但影响往往更严重。常见位置包括邮箱换绑、手机号换绑、密码重置、邀请码绑定、注册流程中的身份校验等。这类操作通常是多步骤的先发起验证验证通过后更新绑定关系更新过程中可能还涉及旧会话的失效逻辑。以邮箱换绑为例正常流程是用户提交新邮箱收到验证邮件点击验证链接后服务端把用户的主邮箱字段改成新地址。攻击者可以准备两个邮箱同时提交两个换绑请求。如果服务端在验证通过后没有对“用户当前绑定关系的版本号”做并发控制两个请求可能先后覆盖最终用户的邮箱被绑定到攻击者控制的地址而用户本人收不到任何提醒。请求构造上我需要同时准备两条不同的请求体分别携带不同的新邮箱和对应的验证 token。这比单一接口重复发送要复杂一些不能再用简单的空 payload Intruder 解决。我的做法是先用 Python 脚本分别完成两个邮箱的验证链接获取再在两个请求之间几乎同时发送最终更新请求。邀请奖励的场景也很常见。新人注册时填入邀请码后端会检查这个新人是否已经绑定过邀请人如果没绑定就把新人标记为某个邀请人的下线。并发提交两个不同的邀请码可能导致一个新人同时成为两个人的下线或者同一个邀请码被绑定了多次。这类漏洞同样属于竞态但对厂商来说他们更关心的是“一个新人只能被一个邀请人获取奖励”的业务规则被绕过。判断这类漏洞是否成立除了看响应更关键的是去用户中心看最终绑定状态。比如绑定的邀请码是否是最后请求的那个、是否出现两个邀请人都收到了奖励通知。影响描述建议往“账号绑定被劫持”“推广奖励刷取”方向写。3.4 从“偶然复现”到“稳定利用”写 PoC 的四个要点很多竞态漏洞的复现率不稳定关键在于时间窗口没控制好。我总结了一套提高稳定性的实操方法写 PoC 时基本按这四个点走。第一尽量使用单连接多请求。传统的多线程并发从不同连接发出请求每个连接都要经历 TCP 握手到达时间差异大。如果能使用同一个 TCP 连接内的 HTTP 流水线或者使用 HTTP/2 多路复用多个请求几乎可以在同一帧序列里到达服务器窗口里的请求顺序被网络抖动的干扰最小。Burp 的 Turbo Intruder 可以直接配置requestsPerConnection和pipeline参数Python 里可以用h2库但日常用 Turbo Intruder 就够了。第二循环重试并记录成功率。不要发一轮就下结论。我通常写一个脚本循环跑 10 轮每轮 30 个并发请求统计每轮成功次数。如果成功率从 0% 变成 30%说明窗口确实存在只是不一定每次都踩中。如果一轮成功、一轮不成功多试几轮能帮你在报告里写得更有说服力。import httpx from concurrent.futures import ThreadPoolExecutor url https://target.com/api/v1/coupon/redeem def send_one(i): resp httpx.post(url, json{code: SUMMER2024, userId: 10001}) return resp.json() for round_no in range(10): with ThreadPoolExecutor(max_workers20) as executor: results list(executor.map(send_one, range(30))) success [r for r in results if r.get(code) 0] print(fround {round_no}: success{len(success)}/30)第三PoC 要证明业务状态被破坏而不是只贴 HTTP 响应。截图里除了 Burp 或脚本的请求响应还要包含优惠券列表、账户流水、数据库查询结果或前端页面的“已领取数量”。很多新手报告贴了一堆 200 OK厂商完全不知道影响是什么自然不愿意给高评级。第四注意影响最小化。不要真的在目标环境里把几百张券刷完也不要用大量流量轰炸对方接口。测试前先看目标平台规则尽量使用测试账号在规定的测速范围内操作。你是在证明漏洞不是在薅羊毛控制请求量是安全测试的基本素养。4. 常见问题与排查技巧实录4.1 “怎么发都发不成功”排查并发没有真正并发这是新手最容易卡住的地方。我一开始用 Burp Repeater 手动快速点发送复现了整整一个下午都没成功后来才发现请求根本不是同时到达的。排查时可以按下面三个方向检查。第一个方向确认工具是否真的在并发。Burp Repeater 的发送是顺序的没有并发能力。如果用的是 Intruder检查线程数是否设置正确以及是否选择了同一个“Resource Pool”。不同连接池会让请求从不同 TCP 连接发出配合网络抖动顺序就可能被拉开。第二个方向确认服务端是否串行执行。有些接口用了数据库行锁、分布式锁或事务隔离导致即使请求同时到达也会被排队执行。这种情况下无论你并发多少本质上都变成了顺序处理业务不会错乱。你可以通过观察响应时间来判断如果 20 个请求的响应时间差得很大可能是锁等待如果响应全部集中在同一时间说明窗口存在且请求确实并发到达。第三个方向使用 HTTP/2 或单连接流水线提高同时性。如果请求每次都“刚好错过”窗口可以试试 Turbo Intruder 的pipelineTrue让多个请求在同一连接里背靠背发出。很多现代 Web 服务在 HTTP/2 下对同连接多请求的处理更趋近于并发这常常是压死骆驼的最后一根稻草。我本人还遇到过一种隐蔽情况目标前面有多个负载均衡节点请求被分发到了不同后端实例每个实例各自有独立的缓存状态导致并发请求在不同节点上各自判定成功。这种问题在报告里比较难复现但反过来也给厂商提了个醒说明他们的缓存同步和分布式锁设计存在缺陷。4.2 怎么排除“假阳性”与误判竞态条件的误报比例不低尤其是刚入门时容易把正常功能当成漏洞。最常见的误判场景是并发发出 50 个请求所有请求都返回 200但只发了一次优惠券。这是因为服务端可能用了唯一索引、幂等表或缓存重复请求被“静默吞掉”响应码虽然是 200业务上却没有重复发放。这种情况下不构成漏洞或者说虽然缺少接口幂等但对业务没有实际危害厂商一般不会按漏洞接收。另一个容易误判的场景和“网络超时重试”有关。App 或前端在网络抖动时会对同一个请求自动重试如果服务端没有幂等设计用户可能看到两次下单成功这本质上确实是幂等缺失但它属于客户端行为导致和竞态不完全一样。写报告的时候要把“重复提交”和“并发竞态”分开描述结论会更严谨。我在本地靶场验证时会特意看最终数据状态而不是响应。一个稳妥的判据是并发请求后数据库里对应资源的最终状态是否违反业务规则。比如优惠券表里同一个 code 是否出现在多个用户账下、订单表里是否生成了多个相同订单号但不同资源记录、账户余额流水是否出现了多笔扣减。以“数据不一致”为准绳才能少交误报。还要留意一种“假阴性”厂商修复后接口看起来正常了但只加了应用层锁绕过的可能性依然存在。比如用 Redis 锁但锁没有设置过期时间或者锁的 key 只包含了部分参数攻击者换一个参数组合就能绕过。后面测试时我一般会多换几组参数再确认一遍。4.3 漏洞赏金平台与报告写法注意事项在真正提交报告之前有几条关于平台规则和沟通方式的建议一定要提前知道。第一明确授权范围。不是所有目标都允许并发测试某些 Bug Bounty 项目明确禁止 DoS 或高速率请求连 100 个并发都可能被判定为违规。动手之前先读一遍项目条款如果规则里允许逻辑漏洞测试那么合理控制请求量是默认要求。竞态条件本身的测试特性决定了你必须要用并发所以更要严格限制在“验证”层面不要跑大规模压力测试。第二报告标题要直接说明“竞态条件导致什么后果”比如“Race Condition: Coupon Redemption Can Be Reused Multiple Times”。标题里出现“Race Condition”和具体业务后果比只写“逻辑漏洞”更容易被审核人员重视。复现步骤按时间线分步写包括前置条件、发包内容、并发数量、观察结果并附上屏幕录像或流量截图。影响评级部分不要只写“可能导致重复领取”最好量化同一优惠码可被并发兑换 20 次涉及资金面额度 X 元。第三主动给修复建议。厂商收到报告后往往会问“怎么修”如果你能直接建议使用唯一约束、乐观锁版本号、原子更新或分布式锁比单纯报漏洞更容易获得信任。我也见过不少猎手因为修复建议写得专业被厂商邀请参与后续众测或获得额外奖励。平台之间的评级标准有差异大致可以参考下面的经验值场景影响常见等级优惠券/礼品卡重复兑换薅羊毛、营销费用损失Medium 到 High余额重复提现/转账限额绕过资金损失High 到 Critical邮箱/手机号换绑被并发覆盖账号绑定劫持High 到 Critical邀请码/推广奖励刷取风控规则绕过Medium 到 High投票/点赞/签到计数重复业务数据被污染Low 到 Medium4.4 修复后的绕过测试与二次验证厂商修复漏洞之后很多人就不管了但我会习惯性地再做一轮“修复验证”。不是不信任厂商而是并发场景下的修复很容易只堵住一条路留下另一条。比如为了阻止重复领取开发者在用户维度加了唯一索引但攻击者如果改用两个不同用户的身份去兑换同一个 code可能仍然绕过因为唯一约束建错了字段。又比如用 Redis 锁解决了更新冲突但锁的 key 只用了订单号攻击者更换订单号后又可以重复利用同一张券。二次验证时我会先复跑原始 PoC看请求是否还能制造重复数据之后再用参数变体测试比如改变用户、改变邀请码、改变设备标识、改变请求顺序确认修复覆盖了整个状态流转链路。如果发现绕过报告里就写得更加扎实原始漏洞修复不完整攻击者可通过 X 方式继续利用。还需要注意一点修复后服务端可能增加了限流或风控原来的并发频率被拦截这不代表漏洞真的修好了只是触发条件变苛刻了。我会尝试降低并发数、拉长请求间隔或者使用不同源 IP 重测以确认到底是“并发逻辑修复”还是“风控把攻击挡住”。从厂商角度来说最稳妥的修复其实是两条腿走路数据库唯一约束兜底业务层逻辑锁防止重复校验同时把“读取-校验-更新”放进同一个事务或原子更新语句里。赏金报告里能把这个方案写清楚审核人员对你的专业度会有非常直观的认可。聊到最后说一点我做竞态条件测试的真实体会。这个方向不像注入类漏洞那样需要很深的知识积累它更考验耐心和对业务的理解力。每次拿到一个目标我都会把“领取、兑换、绑定、确认、重置”这类词穷举一遍看到“一次性”“限领一次”“每个用户仅限”的地方就多留个心眼。本地靶场跑通工具链备好脚本循环跑上几轮反反复复对比数据和响应。多积累几个典型模式后再遇到类似功能基本一眼就能看出哪儿有窗口。如果你还没试过挖竞态条件建议先从自己搭的靶场开始把并发工具跑熟再去授权目标上实战。我一直觉得这个方向的产出与投入比在漏洞赏金的所有类型里是排得上号的。