条件竞争
Last updated
Last updated
条件竞争是一种常见的漏洞类型,与业务逻辑缺陷密切相关。当网站在没有足够保护措施的情况下并发处理请求时,就会出现条件竞争。它可能导致多个不同的线程同时与相同的数据交互,产生冲突,从而引发应用程序出现非预期的行为。条件竞争攻击会使用精心计时的请求来引发故意的冲突,并利用这种非预期的行为来达到恶意目的。
冲突可能发生的时间段称为竞争窗口。例如两次与数据库交互之间的几分之一秒。
与其他逻辑缺陷一样,条件竞争的影响在很大程度上取决于应用程序以及发生竞争的特定功能。
在本节中,你将学习如何识别和利用不同类型的条件竞争。我们会教你如何使用Burp Suite内置的工具来克服执行经典攻击时遇到的挑战,以及一种久经考验的方法,使你能够在隐藏的多步骤过程中检测出新的条件竞争种类。这些远远超出你可能已经熟悉的限制溢出。
PortSwigger Research
像往常一样,我们还提供了一些故意易受攻击的实验,你可以利用它们针对现实目标安全地练习所学知识。其中许多实验是基于PortSwigger的原创研究,最早在Black Hat USA 2023上首次展示。
有关更多详细信息,请查阅随附的白皮书:《粉碎状态机:Web条件竞争的真正潜力》
最广为人知的条件竞争类型是让你可以超出应用程序业务逻辑规定的某种限制。
例如,考虑一个在线商店,允许你在结账时输入促销码,以便在订单上获得一次性折扣。为了应用此折扣,应用程序可能会执行以下高级步骤:
检查你是否已经使用过此促销码。
将折扣计入订单总额。
更新数据库中的记录,以反映你已经使用了此促销码。
如果你稍后试图重用这个促销码,流程开始时执行的初始检查应该会阻止你这样做:
现在考虑一下,如果一个之前从未使用过该折扣码的用户在几乎完全相同的时间内尝试两次使用它会发生什么:
如你所见,应用程序在请求处理完成之前会经历一个临时子状态,即在请求处理完成之前它会进入并再次退出的状态。在本例中,子状态始于服务器开始处理第一个请求时,到更新数据库以表明你已使用过该折扣码时结束。这样就引入了一个小的竞争窗口,在此期间,你可以随心所欲地重复索取折扣。
这种攻击的许多变种,包括:
多次兑换礼品卡
多次评价产品
超出账户余额的提取或转账
重复使用单个验证码解决方案
绕过反暴力破解速率限制
限制溢出是检查时间到使用时间(TOCTOU)缺陷的一个子类型。在本主题的后面,我们将查看一些不属于这两类条件竞争漏洞的例子。
检测和利用限制溢出条件竞争的过程相对简单。从高层面来说,你只需执行以下操作:
识别出具有某种安全影响或其他有用目的的单次使用或速率限制的端点。
向该端点连续发出多个请求,查看是否可以超出此限制。
主要的挑战在于如何把握请求的时间,以便至少有两个竞争窗口同时出现,从而导致冲突。这个窗口通常只有几毫秒,甚至更短。
即使你在完全相同的时间发送所有请求,在实际操作中,也会存在各种不可控和不可预测的外部因素影响服务器处理每个请求的时间和顺序。
Burp Suite 2023.9为Burp Repeater添加了强大的新功能,新功能使你能够轻松发送一组并行请求,并能够显著地减少网络抖动的影响。Burp会自动调整所使用的技术,以适应服务器所支持的HTTP版本:
对于HTTP/1,它使用经典的最后一个字节同步技术。
对于HTTP/2,它使用单包攻击技术,该技术首次由PortSwigger Research于Black Hat USA 2023上演示。
单包攻击使你能够通过使用单个TCP数据包同时完成20-30个请求,从而完全抵消网络抖动的干扰。
尽管通常只需要两个请求来触发利用,但发送大量这样的请求有助于减少内部延迟,这也称为服务器端抖动。在初始发现阶段,尤其有用。我们将在下面更详细地介绍这套方法论。
阅读更多
有关如何使用Burp Repeater的新功能并行发送多个请求的详细信息,请参阅Sending requests in parallel。
有关单包攻击底层机制的技术见解以及更详细的方法论,请查阅随附的白皮书:《粉碎状态机:Web条件竞争的真正潜力》
LAB
除了在Burp Repeater中对单包攻击提供本地支持外,我们还增强了Turbo Intruder扩展以支持这种技术。你可以从BApp Store下载最新版本。
Turbo Intruder需要一定的Python熟练程度,但它适用于更复杂的攻击,如需要多次重试、请求时间交错或请求数量极大的攻击。
要在Turbo Intruder中使用单包攻击:
确保目标支持HTTP/2。单包攻击与HTTP/1不兼容。
为请求引擎设置engine=Engine.BURP2
和concurrentConnections=1
配置选项。
在对请求进行排队时,可使用engine.queue()
方法的gate
参数将请求分配给一个命名的“门”,从而对请求进行分组。
要发送指定组中的所有请求,请使用engine.openGate()
方法打开相应的“门”。
有关详细信息,请参阅Turbo Intruder默认示例目录中提供的race-single-packet-attack.py
模板。
LAB
在实践中,一个单独的请求在幕后可能会启动整个多步骤序列,通过多个隐藏状态过渡应用程序,而这些状态在请求处理完成之前会进入并再次退出。我们将这些状态称为子状态。
如果能识别出一个或多个与相同数据交互的HTTP请求,你就有可能滥用这些子状态,来暴露多步骤工作流程中常见的逻辑缺陷的时间敏感变化。这样就可以使条件竞争利用远远超出限制溢出的范围。
例如,你也许对存在缺陷的多因素认证(MFA)工作流程并不陌生,该工作流程允许你使用已知凭证执行登录的第一步,然后通过强制浏览直接进入应用程序,从而完全绕过MFA。
注意
如果你不熟悉这种利用,请查看我们其他主题中的如下实验:
以下伪码演示了一个网站如何易受到这种攻击的条件竞争变种攻击:
正如你所见,实际上这是一个在单个请求中的多步骤序列。最为重要的是,它会经历一个子状态,在这个子状态中,用户会暂时拥有一个有效的登录会话,不过MFA还尚未执行。攻击者可以同时发送一个登录请求和一个请求到敏感的、需经过认证的端点来利用这一点。
稍后,我们将看到隐藏多步骤序列的更多示例,你还可以在交互式实验中练习利用它们。不过,由于这些漏洞与特定的应用程序密切相关,因此在高效地识别它们之前,首先理解你需要应用的更广泛的方法论是很重要的,无论是在实验中还是在现实环境中。
要检测和利用隐藏的多步骤序列,我们建议采用以下方法,这些方法总结于PortSwigger Research的白皮书《粉碎状态机:Web条件竞争的真正潜力》。
测试每一个端点是不切实际的。在正常绘制目标站点地图后,你可以通过以下问题来减少需要测试的端点数量:
这个端点的安全性关键吗? 许多端点并不涉及关键功能,因此不值得测试。
是否存在任何冲突的可能性? 要成功实现冲突,通常需要两个或两个以上的请求触发对同一记录的操作。例如,考虑如下密码重置实现的变种:
在第一个示例中,为两个不同用户并行请求密码重置不太可能导致冲突,因为这样会对两个不同的记录进行更改。但第二种实现方式可以让你能够通过为两个不同的用户请求来编辑同一记录。
要识别线索,首先需要基准测试端点在正常情况下的行为。你可以在Burp Repeater中将所有请求分组,并使用Send group in sequence (separate connections)
选项来做到这一点。更多信息,请参阅Send group in sequence。
接着,使用单包攻击(如果不支持HTTP/2,则使用最后一个字节同步)一次发送同一组请求,以减少网络抖动。你可以在Burp Repeater中选择Send group in parallel
选项来完成此操作。更多信息,请参阅Send group in parallel。或者,你也可以使用Turbo Intruder扩展,该扩展可从BApp Store获得。
任何事物都可能成为线索。只需寻找与基准测试期间观察到的情况有所偏离的某种形式。包括一个或多个响应的变化,也不要忽略二阶效应,如不同的电子邮件内容或应用程序行为之后的明显变化。
试图理解发生了什么,删除多余的请求,并确保你仍可以复制效果。
高级条件竞争可能会导致异常和独特的基元,因此达到最大影响的途径并不总是显而易见的。将每种条件竞争视为结构性弱点,而不是孤立的漏洞,可能会有所帮助。
PortSwigger Research
如需了解更详细的方法论,请查阅完整的白皮书:《粉碎状态机:Web条件竞争的真正潜力》
这种条件竞争最直观的表现形式可能就是同时向多个端点发送请求。
考虑一下在线商店中的经典逻辑缺陷,你先将商品添加到购物篮或购物车中,付款,然后在强制浏览订单确认页面之前,又将更多商品添加到购物车中。
注意
如果你不熟悉这种利用,请查看我们其他主题中的如下实验:
在处理单个请求的期间进行付款验证和订单确认时,可能会出现这种漏洞的变种。订单状态的状态机可能如下所示:
在这种情况下,你或许可以在付款验证和订单最终确认之间的竞争窗口内,向购物篮中添加更多商品。
在测试多端点条件竞争时,即使使用单包技术将所有请求都同时发送,你也可能会遇到试图为每个请求排列竞争窗口的问题。
这一常见问题主要是由以下两个因素导致的:
网络架构引入的延迟 - 例如,前端服务器与后端建立新连接时可能会出现延迟。所使用的协议也可能会产生重大影响。
特定端点的处理引入的延迟 - 不同的端点在其处理时间上存在固有的差异,有时差异还很大,具体取决于它们触发的操作。
幸运的是,如上两个问题都有可能得到解决。
后端连接延迟通常不会干扰条件竞争攻击,因为它们通常会平均地延迟并行请求,从而使请求保持同步。
必须将这些延迟与特定端点导致的延迟区分开来。一种方法是通过一个或多个无关紧要的请求来预热连接,看看这样是否能使剩余的处理时间更加平稳,在Burp Repeater中,你可以尝试在标签组的开头添加一个主页的GET
请求,然后使用Send group in sequence (single connection)
选项。
如果第一个请求处理的时间仍较长,而其余请求现在都能在很短的窗口内得到处理,则可以忽略明显的延迟,并继续正常测试。
LAB
即使使用的是单包技术,但仍然在单个端点上看到不一致的响应时间,这表明后端延迟干扰了你的攻击。你或许可以尝试在主攻击请求之前,使用Turbo Intruder发送一些连接预热请求来解决此问题。
如果连接预热没有产生任何差异,那也有各种办法解决这个问题。
使用Turbo Intruder,你可以引入一个短暂的客户端延迟。不过,由于这需要将实际攻击请求分拆成多个TCP包中,所以你将无法使用单包攻击技术。因此,在高抖动目标上,无论设置了何种延迟,攻击都不太可能能可靠地进行。
相反,你可以通过滥用一种常见的安全功能来解决此问题。
Web服务器通常会在请求发送过多过快的情况下延迟处理请求。通过发送大量虚假请求来故意触发速率或资源限制,就可以导致适当的服务器端延迟。这样,即使在需要延迟执行的情况下,单包攻击也是可行的。
向单个端点发送具有不同值的并行请求有时可能触发强大的条件竞争。
考虑一个密码重置机制,该机制将用户ID和重置令牌存储在用户的会话中。
在这种情况下,从同一会话中发送两个并行的密码重置请求,但使用的是两个不同的用户名,可能会导致以下冲突:
请注意,当所有操作都完成时的最终状态:
session['reset-user'] = victim
session['reset-token'] = 1234
会话现在包含了受害者的用户ID,但有效的重置令牌被发送给了攻击者。
注意
要使这种攻击奏效,每个进程执行的不同操作必须以恰到好处的顺序发生。通常需要多次尝试或一些运气才能达到预期效果。
电子邮件地址确认或任何基于电子邮件的操作通常是单端点条件竞争的合适目标。电子邮件通常是在服务器向客户端发出HTTP响应后在后台线程中发送的,因此这会使得条件竞争更有可能发生。
LAB
一些框架试图通过使用某种形式的请求锁定来防止意外的数据损坏。如PHP的本地会话处理模块一次只处理一个会话请求。
发现这种行为非常重要,否则它可能会掩盖那些易于被利用的漏洞。如果你注意到所有请求都按顺序处理的,请尝试使用不同的会话令牌发送每个请求。
许多应用程序在多个步骤中创建对象,这可能会引入一个临时的中间状态,在该状态对象是可以被利用的。
例如,在注册新用户时,应用程序可能会在数据库中创建用户,并使用两条单独的SQL语句设置其API密钥。在用户存在但其API密钥尚未初始化的情况下留下了一个微小的窗口。
这种行为为利用铺平了道路,你可以注入一个返回与未初始化数据库值相匹配的输入值,例如空字符串或JSON中的null
,然后将其作为安全控制的一部分进行比较。
框架通常允许使用非标准语法传递数组和其他非字符串数据结构。在PHP中:
param[]=foo
等同于param = ['foo']
param[]=foo¶m[]=bar
等同于param = ['foo', 'bar']
param[]
等同于param = []
Ruby on Rails还允许你通过提供一个查询或带键但不带值的POST
参数来实现类似的功能。换句话说,param[key]
会产生以下服务器端对象:
在上面的示例中,意味着在竞争窗口期间,你有可能发出以下需经过认证的API请求:
注意
使用密码而非API密钥也有可能导致类似的部分构造冲突。但是由于密码已经被哈希,意味着你需要注入一个值,使哈希摘要与未初始化的值匹配。
LAB
有时候你可能无法找到条件竞争,但以精确时序发送请求的技术仍然可以揭示其他漏洞的存在。
其中一个例子就是使用高分辨率时间戳来代替加密安全随机字符串生成安全令牌。
考虑一个仅使用时间戳进行随机化的密码重置令牌。在这种情况下,可能触发两个不同用户的两次密码重置,而这两次密码重置都使用相同的令牌。你所需要做的就是计时请求,使它们生成相同的时间戳。
LAB
当一个单独的请求就可以通过不可见的子状态过渡应用程序时,理解和预测其行为就变得极其困难。这使得防御变得不切实际。要确保应用程序的安全,我们建议采用以下策略消除所有敏感端点的子状态:
避免混合来自不同存储位置的数据。
使用数据存储的并发功能,确保敏感端点的状态更改是原子性的。例如,使用单个数据库事务来检查付款与购物车值是否相匹配,并确认订单。
作为一种深度防御措施,利用数据存储的完整性和一致性特性,如列唯一性约束。
不要试图使用一个数据存储层来保护另一个数据存储层。例如,会话不适合用于防止对数据库的限制溢出攻击。
确保会话处理框架保持内部一致。逐个更新会话变量而不是批量更新可能是一种诱人的优化,但极其危险。这一点也适用于ORM,通过隐藏事务等概念,它们对其负有全部责任。
在某些架构中,完全避免服务器端状态可能是合适的。相反,你可以使用加密来推送客户端状态,如使用JWT。请注意,这也有其风险,我们在JWT攻击主题中对此进行了广泛的介绍。