图1 封面
概括
当网站并发处理请求时,就会出现竞争条件漏洞。竞争条件通常发生在多线程或多进程环境中,即多个线程或进程竞争共享资源。竞争条件攻击依赖于事件的相对时间,攻击者可以操纵此时间,导致应用程序出现意外行为,从而执行某些恶意任务。
描述
我发现了一个竞争条件,利用这个条件我能够绕过登录面板上已实施的速率限制,并成功接管账户。在一个网站的登录面板上,速率限制被实施了,我无法进行暴力破解,因为速率限制阻止了我。但使用修改后的竞争条件脚本,我能够绕过速率限制机制,从而成功绕过身份验证并登录账户。
竞争条件漏洞剖析
竞争条件通常对时间非常敏感,攻击的成功取决于精确的时间操控。可能发生碰撞的时间段被称为“竞争窗口”。
什么是比赛窗口?
竞争窗口是指由于多个线程或进程并发执行而可能利用漏洞的时间范围。在此期间,程序或系统的结果取决于事件的顺序和时间,从而导致意外且不安全的行为。
便于理解的示例
场景:具有速率限制的帮助台登录门户
假设一家公司有一个登录门户的功能,员工可以输入他们的凭证。为了防止暴力攻击,系统设置了速率限制,即每分钟仅允许单个 IP 地址进行 5 次登录尝试。
正常行为(速率限制正常工作)
-
您输入了错误的密码 -
你再试一次 -
5 次尝试失败后,系统将阻止进一步的登录尝试 1 分钟
现在通常情况下,这可以防止攻击者快速尝试数千个密码。
图2:常速率限制机制流程
竞争条件如何绕过速率限制
您无需一次发送一个登录请求,而是使用精心设计的脚本:
- 在“门”后面同时排队 100 个登录请求
- 大门打开时同时发送所有 100 个请求
服务器逻辑上的缺陷:
- 处理每个登录请求后,速率限制计数器都会更新
- 由于所有 100 个请求同时到达,服务器不会立即阻止它们
- 服务器首先验证密码,然后增加速率限制计数器
图3 竞争条件——绕过速率限制
结果:
- 尽管每分钟只允许 5 次尝试,但您在计数器更新之前成功发送了 100 次密码猜测(为了演示,我在上图中仅显示了 6 次尝试,而不是 100 次)
- 如果其中一个密码正确,您就可以在速率限制阻止您之前访问该账户。
注意:在上图中,我展示了第 6 次尝试,因为第 5 次无效尝试后触发的速率限制不会在这里触发,因为请求已排队,并且一旦所有 6 个请求都启动,速率限制将仅增加 1,因为所有请求几乎同时到达登录面板。
我如何发现这个漏洞?
- 我进入目标网站的登录面板,拦截登录请求 > 右键单击 > 扩展 > Turbo Intruder > 发送到 turbo intruder
图4 登录请求 - 发送至 Turbo Intruder
2. 在 Turbo Intruder 中,我加载了修改后的脚本,并从记事本中选择了所有密码,右键单击并复制它(剪贴板副本),然后单击“攻击”
def queueRequests(target, wordlists):
print("Initializing RequestEngine...")
# as the target supports HTTP/2, use engine=Engine.BURP1 and concurrentConnections=1 for a single-packet attack
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2
)
print("RequestEngine initialized with endpoint: {}".format(target.endpoint))
# assign the list of candidate passwords from your clipboard
passwords = wordlists.clipboard
print("Loaded {} passwords from clipboard.".format(len(passwords)))
# queue a login request using each password from the wordlist
# the 'gate' argument withholds the final part of each request until engine.openGate() is invoked
for index, password in enumerate(passwords):
print("Queuing password {}/{}: {}".format(index + 1, len(passwords), password))
engine.queue(target.req, password, gate='1')
print("All requests have been queued.")
# once every request has been queued
# invoke engine.openGate() to send all requests in the given gate simultaneously
print("Opening gate '1' to send all queued requests...")
engine.openGate('1')
print("Gate '1' opened. All requests sent.")
def handleResponse(req, interesting):
print("Handling response for request: {}".format(req))
table.add(req)
为什么会发生这样的事?
在我看来,
之所以能绕过,是因为服务器先检查了登录凭证,然后再更新速率限制计数器,从而造成了时间差(竞争窗口)。当我同时向服务器发送多次登录尝试时,服务器会在将所有猜测的密码计为单独尝试之前处理掉它们。这使得我能够在不触发速率限制的情况下识别出正确的密码,因为服务器只将攻击计为“1次尝试”,而不是“100次尝试”。操作顺序错误和同步性差让我得以“超越”速率限制。
影响
该漏洞允许攻击者绕过速率限制并识别任何用户的凭证,从而导致凭证泄露和账户接管。
CVSS 评分及说明
矢量字符串:
CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N
评分: 3.7 低
解释:
攻击媒介(网络):攻击者可以通过网络访问该项目,因为它是一个 Web 应用程序。
攻击复杂性(高):竞争条件攻击需要精确的连续计时
所需权限(无):攻击者不需要任何权限即可执行此攻击
用户交互(无):攻击者无需额外的用户交互即可利用该漏洞。
范围(未改变):受影响的应用程序由同一安全机构维护。
保密性(低):继任时有效凭证会被曝光
完整性(无):攻击者无法修改此处的任何数据
可用性(无):整个网站仍然可访问
减轻
为了缓解此问题,建议实施以下修复
- 原子速率限制:在验证凭证之前增加速率限制计数器,而不是之后。
- 锁定共享资源:使用锁/互斥锁来防止对同一账户的登录尝试进行并行处理。
- 拒绝过载数据包:将 HTTP/2 多路复用请求视为多个单独的尝试,而不是一次。
- 添加服务器端延迟:在登录失败后引入限制(例如,2 秒延迟)以减缓暴力攻击。
原文始发于微信公众号(Rsec):0034. 竞争条件——账户接管的速率限制
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论