在一次安全测试中,目标网站的验证码发送接口引起了我的注意。网站整体防护看起来比较宽松,于是我决定重点审计其验证码机制。
正常的响应与防护
首先,查看一个正常的验证码请求响应。

图1:正常发送验证码请求的成功响应
当我们尝试短时间内重复发送验证码时,系统会进行拦截,提示“请勿重复发送,请稍后再试”。

图2:重复发送请求被后端防护逻辑拦截
尝试常规绕过手法
接下来,我尝试使用一些常规的绕过手法来突破这个频率限制。
1. 换行符 – 失败
在参数中添加换行符,尝试干扰后端解析,但请求依然被识别为重复。

2. 添加空格或加号 – 失败
尝试在手机号参数中添加空格或 + 号进行混淆。
body=contact%3D%2013888888888%20%26type%3Dgetverifycode%26cmd%3D-1&data=&yzm=&encrypt=false
body=contact%3D+13888888888%26type%3Dgetverifycode%26cmd%3D-1&data=&yzm=&encrypt=false
结果均显示“请勿重复发送”。

图3:添加空格的请求被拦截

图4:添加加号的请求同样被拦截
3. 添加分号或逗号 – 失败
尝试使用分号或逗号分割多个手机号,但后端直接返回了“手机号/邮箱格式错误”,说明参数校验在前。

图5:添加分号导致参数校验失败
4. 添加国际区号 – 失败
尝试在手机号前添加 +86 或 86,请求直接返回“验证码发送失败”。

突破点:利用数据库空白字符
常见思路都失败后,我思考是否可以利用数据库层面的空白字符(如NULL,即 %00)进行干扰。在Burp Suite的Intruder模块中,我在手机号参数后添加 %00 进行爆破测试。

图6:在Intruder中使用Payload 13888888888%00 进行测试
令人意外的是,这次测试竟然收到了多条验证码短信!

图7:测试手机收到了多个不同的验证码
为了进一步确认,我将攻击线程数提高到30并再次运行。

图8:将Intruder的线程数设置为30

图9:部分请求返回成功状态码(447),部分被拦截(408)
问题显现了。按照之前的逻辑,使用 %00 只是改变了参数值,服务器应将其视为同一个手机号的重复请求并进行拦截。但现在却出现了部分请求成功发送验证码的情况,这表明确实存在业务逻辑上的漏洞。
漏洞分析与定位:是条件竞争吗?
最初,我怀疑这是高并发导致的漏洞。查阅资料后,我发现典型的高并发漏洞通常涉及“检查”与“操作”两个步骤的非原子性。

图10:高并发漏洞常见于“先查询后写入”的非原子操作
随后,我与团队的经验丰富的安全工程师交流,他给出了关键提示:这很可能是一个条件竞争漏洞。虽然我过去更多地在文件上传场景中接触条件竞争,但验证码发送的逻辑同样可能存在“判断是否可发送”与“执行发送并标记”之间的时间窗口。如果这两个操作不是原子性的,在高并发请求下,多个线程可能同时通过“检查”,然后都执行了“发送”操作。
那么,如何验证这就是条件竞争?最直接的证明就是:不使用任何混淆技巧,直接重放同一个合法的验证码请求包,通过高并发线程进行轰炸。

图11:捕获一个正常的验证码请求包用于重放测试
我将重放攻击的线程数设置为50。


图12:大量并发重放请求,部分成功(447),部分被拦截(408)
最终,手机再次收到了多条验证码。

图13:通过纯重放攻击,同样触发了多次验证码发送
这成功验证了漏洞的存在。测试的目的是验证问题,因此无需将线程开得过大以致影响网站正常服务。
总结与思考
- 验证码机制同样存在条件竞争漏洞。本次实战表明,不仅是文件上传,任何包含“状态检查”与“状态更新”两步非原子操作的业务逻辑(如发送短信、发放优惠券、扣除余额等),在并发环境下都可能存在竞争条件。
- 漏洞挖掘需要理解底层逻辑。从尝试各种绕过手法到最终定位为条件竞争,整个过程要求测试者不仅要“知道怎么操作”,更要思考“为什么会这样”。理解服务器大致的处理流程(检查频率→发送短信→记录发送状态)是推断漏洞成因的关键。
- 关于漏洞名称的讨论:有观点认为,这种因高并发导致多个请求绕过限制的情况,本质上就是条件竞争(Race Condition)。因为多个线程(请求)在“竞争”通过检查的时间窗口,先通过检查的线程会执行操作并改变状态,从而影响后续线程的判断。

图14:与同行探讨漏洞原理,加深理解
这次经历拓宽了我对条件竞争漏洞应用场景的认识。安全测试永无止境,保持好奇与钻研精神才能不断进步。

图15:技术积累就像充电,需要一步步来
希望这个案例能为你带来启发。欢迎在云栈社区与更多安全爱好者交流技术心得。