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

本文详细分析了TOCTOU竞态条件漏洞如何导致身份验证被破坏的技术原理,通过实际案例展示了在高并发场景下,认证检查与令牌发放不同步会导致多个错误密码获得有效令牌的安全风险。

Time-of-check Time-of-use (TOCTOU) 竞态条件导致身份验证破坏

发现过程

我在无聊时开始随机测试公共漏洞赏金程序。按照惯例,我首先进行子域名收集以缩小攻击面。我的快速漏洞赏金狩猎流程是:

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

然后将结果传递给aquatone:

1
cat subdomain.txt | aquatone

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

漏洞发现

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

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

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

在正常情况下,一切表现正常:错误密码返回401,正确密码返回200并发放令牌。

但在一天中的某些特定时间,网站会变得极其缓慢。在进行登录或注册时不是"有点慢",而是完全卡顿。然后我意识到是服务器过载导致的。

关键发现

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

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

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

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

技术原理

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

实际发生的情况

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

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

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

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

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

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

代码层面的原因

核心问题是非原子操作序列:

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

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

由于没有每个账户的锁或原子操作,多个并发请求都可以在状态更新之前通过检查步骤。

其他常见根本原因包括:

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

总结

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

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