TOCTOU竞争条件导致身份验证漏洞分析

本文详细分析了TOCTOU竞争条件漏洞如何导致身份验证绕过。作者通过实际案例展示了在高并发场景下,由于密码检查与令牌发放步骤不同步,攻击者可以使用错误密码获取有效令牌的技术细节。

Time-of-check Time-of-use (TOCTOU) 竞争条件导致身份验证漏洞

作者:Irsyad Muhammad Fawwaz

我是如何开始的

我感到无聊,开始随机测试公共漏洞赏金项目。像往常一样,我从子域名收集开始,以缩小攻击面。我的快速漏洞赏金狩猎流程是:

subfinder -d example.com --all >> subdomain.txt

然后通过aquatone进行处理:

cat subdomain.txt | aquatone

之后我找到了一个有趣的子域名,它立即重定向到了登录/注册页面。

发现过程

我花了四天时间希望找到漏洞,使用常规测试(如XSS、SQL注入、扫描、原型污染等)和暴力破解实验对网站进行测试。

在四天的测试中,我观察到异常行为:网站在某些时候极其缓慢且无响应,而在其他时候则完全正常。

看到这两种行为,我在网站缓慢期间和快速期间分别运行了登录暴力破解测试。

在正常时间,一切行为正常:错误密码返回401,正确密码返回200和令牌。

但在一天中的某些特定时间,网站会变得极其缓慢。在进行登录或注册时不是"有点慢",而是完全卡顿。

然后我意识到是服务器过载了。

所以我尝试在快速窗口和缓慢窗口期间再次进行暴力破解。

在快速窗口期间,结果很无聊且符合预期:除了正确密码外,所有请求都被拒绝。

在缓慢窗口期间,发生了奇怪的事情。我找到了五个不同的密码,它们都返回200并给了我有效令牌,但这些密码都不是真正的密码。

起初我以为我看错了,但我验证了这些令牌对受保护端点的访问,它们确实有效。

发现原因

发现问题后,我发现这不是魔法。这是一个TOCTOU竞争条件。简单来说,检查登录是否允许的函数和实际发放令牌的部分没有同步。如果认证函数比传入请求慢,多个请求可以在系统更新状态之前通过检查,因此几个不同的错误密码看起来都"有效",因为请求超过了函数的内部计时。

实际发生了什么

将登录流程想象成两条街道:

  • “检查"街道
  • “使用"街道

代码首先沿着检查街道走,说"好的,这个账户现在看起来被允许”,然后沿着使用街道走,说"好的,我将创建会话/令牌”。

如果这两个步骤连续发生,一切都能完美工作。

但如果检查步骤很慢(CPU负载重、数据库慢、I/O阻塞、服务器过载),多个传入请求会在它后面排队。它们都在令牌创建步骤完成之前通过了初始检查。结果是几个请求到达了令牌生成步骤,尽管密码验证本应失败。

简而言之:当认证函数比传入请求速率慢时,请求可以超越内部状态并产生不一致的结果。

代码层面的原因

核心问题是一个非原子序列:

  1. 代码检查某些条件(密码、速率限制、账户状态)
  2. 之后基于该条件发放令牌

但在步骤1和步骤2之间,条件可能发生变化。

由于没有每个账户的锁或原子操作,多个并发请求都可以在状态更新之前通过检查步骤。其他常见根本原因包括:

  • 在缓存中使用GET→SET模式(非原子)
  • 缺乏Redis原子操作/Lua脚本
  • 在没有同步的情况下将验证卸载到异步工作器
  • 在数据库事务中不使用SELECT … FOR UPDATE
  • 高并发下的陈旧读取

TL;DR: 数据库延迟在登录流程中创建了一个竞争窗口:密码检查比传入请求运行得慢,让多个错误密码到达令牌创建步骤。结果是为无效登录发放了真实令牌。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计