hacktricks/pentesting-web/race-condition.md

26 KiB

Condição de Corrida


Use Trickest para construir e automatizar fluxos de trabalho com facilidade, utilizando as ferramentas comunitárias mais avançadas do mundo.
Acesse hoje mesmo:

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

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

Explorando a Condição de Corrida

O principal problema de abusar da Condição de Corrida é que você precisa que as solicitações sejam processadas em paralelo com uma diferença de tempo muito curta (geralmente >1ms). Na seção a seguir, são propostas diferentes soluções para tornar isso possível.

Ataque de pacote único (HTTP/2) / Sincronização do último byte (HTTP/1.1)

O HTTP2 permite enviar 2 solicitações em uma única conexão TCP (enquanto no HTTP/1.1 elas devem ser sequenciais).
O uso de um único pacote TCP elimina completamente o efeito da variação de rede, portanto, isso claramente tem potencial para ataques de condição de corrida também. No entanto, duas solicitações não são suficientes para um ataque de corrida confiável devido à variação do lado do servidor - variações no tempo de processamento da solicitação do aplicativo causadas por variáveis incontroláveis, como contenção da CPU.

Mas, usando a técnica de 'sincronização do último byte' do HTTP/1.1, é possível pré-enviar a maior parte dos dados retendo um pequeno fragmento de cada solicitação e, em seguida, 'completar' 20-30 solicitações com um único pacote TCP.

Para pré-enviar a maior parte de cada solicitação:

  • Se a solicitação não tiver corpo, envie todos os cabeçalhos, mas não defina o sinalizador END_STREAM. Retenha um quadro de dados vazio com o sinalizador END_STREAM definido.
  • Se a solicitação tiver um corpo, envie os cabeçalhos e todos os dados do corpo, exceto o último byte. Retenha um quadro de dados contendo o último byte.

Em seguida, prepare-se para enviar os quadros finais:

  • Aguarde 100ms para garantir que os quadros iniciais tenham sido enviados.
  • Verifique se o TCP_NODELAY está desativado - é crucial que o algoritmo de Nagle agrupe os quadros finais.
  • Envie um pacote de ping para aquecer a conexão local. Se você não fizer isso, a pilha de rede do sistema operacional colocará o primeiro quadro final em um pacote separado.

Por fim, envie os quadros retidos. Você deve ser capaz de verificar que eles chegaram em um único pacote usando o Wireshark.

{% hint style="info" %} Observe que isso não funciona para arquivos estáticos em determinados servidores, mas como arquivos estáticos não são relevantes para ataques de condição de corrida. No entanto, arquivos estáticos são irrelevantes para ataques de CC. {% endhint %}

Usando essa técnica, você pode fazer com que 20-30 solicitações cheguem ao servidor simultaneamente - independentemente da variação de rede:

Adaptando à arquitetura do alvo

Vale ressaltar que muitos aplicativos estão atrás de um servidor de front-end, e esses servidores podem decidir encaminhar algumas solicitações por conexões existentes para o back-end e criar novas conexões para outras.

Portanto, é importante não atribuir o tempo inconsistente de solicitação ao comportamento do aplicativo, como mecanismos de bloqueio que permitem apenas que uma única thread acesse um recurso de cada vez. Além disso, o roteamento de solicitações de front-end geralmente é feito por conexão, portanto, você pode suavizar o tempo de solicitação realizando o aquecimento da conexão do lado do servidor - enviando algumas solicitações inconsequentes pela sua conexão antes de realizar o ataque (isso significa enviar várias solicitações antes de iniciar o ataque real)).

Mecanismos de bloqueio baseados em sessão

Alguns frameworks tentam evitar a corrupção acidental de dados usando algum tipo de bloqueio de solicitação. Por exemplo, o módulo de manipulador de sessão nativo do PHP processa apenas uma solicitação por sessão de cada vez.

É extremamente importante identificar esse tipo de comportamento, pois ele pode mascarar vulnerabilidades facilmente exploráveis. Se você perceber que todas as suas solicitações estão sendo processadas sequencialmente, tente enviá-las usando um token de sessão diferente para cada uma delas.

Abusando dos limites de taxa ou recursos

Se o aquecimento da conexão não faz diferença, existem várias soluções para esse problema.

Usando o Turbo Intruder, você pode introduzir um pequeno atraso no lado do cliente. No entanto, como isso envolve dividir suas solicitações de ataque reais em vários pacotes TCP, você não poderá usar a técnica de ataque de um único pacote. Como resultado, em alvos com alta variação de latência, o ataque provavelmente não funcionará de forma confiável, independentemente do atraso definido.

Em vez disso, você pode resolver esse problema abusando de um recurso de segurança comum.

Servidores web frequentemente atrasam o processamento de solicitações se muitas forem enviadas rapidamente. Ao enviar um grande número de solicitações falsas para acionar intencionalmente o limite de taxa ou recurso, você pode causar um atraso adequado no lado do servidor. Isso torna o ataque de um único pacote viável mesmo quando a execução atrasada é necessária.

{% hint style="warning" %} Para obter mais informações sobre essa técnica, consulte o relatório original em https://portswigger.net/research/smashing-the-state-machine {% endhint %}

Exemplos de Ataque

  • Tubo Intruder - Ataque de um único pacote HTTP2 (1 ponto de extremidade): Você pode enviar a solicitação para o Turbo Intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), você pode alterar na solicitação o valor que deseja forçar por %s como em csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s e, em seguida, selecione o examples/race-single-packer-attack.py no menu suspenso:

Se você for enviar valores diferentes, você pode modificar o código com este que usa uma lista de palavras da área de transferência:

passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')

{% hint style="warning" %} Se o site não suportar HTTP2 (apenas HTTP1.1), use Engine.THREADED ou Engine.BURP em vez de Engine.BURP2. {% endhint %}

  • Tubo Intruder - Ataque de pacote único HTTP2 (Vários endpoints): Caso você precise enviar uma solicitação para 1 endpoint e depois várias para outros endpoints para acionar o RCE, você pode alterar o script race-single-packet-attack.py para 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 'Enviar grupo em paralelo' no Burp Suite.
  • Para limit-overrun, você pode simplesmente adicionar a mesma solicitação 50 vezes no grupo.
  • Para aquecimento de conexão, você pode adicionar no início do grupo algumas solicitações para alguma parte não estática do servidor web.
  • Para atrasar o processo entre o processamento de uma solicitação e outra em etapas de 2 subestados, você pode adicionar solicitações extras entre ambas as solicitações.
  • Para um RC de vários endpoints, você pode começar enviando a solicitação que vai para o estado oculto e, em seguida, 50 solicitações logo após isso que exploram o estado oculto.

Raw BF

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

  • Repeater: Verifique os exemplos da seção anterior.
  • Intruder: Envie a solicitação para o Intruder, defina o número de threads para 30 no menu Opções e selecione como payload Cargas úteis nulas 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

A biblioteca asyncio do Python é uma biblioteca assíncrona que permite escrever código concorrente usando a sintaxe async/await. Ela é especialmente útil para lidar com operações de I/O não bloqueantes, como chamadas de rede, e pode ser usada para criar aplicativos de alto desempenho e escaláveis.

A principal vantagem do asyncio é a capacidade de lidar com tarefas concorrentes sem a necessidade de threads adicionais. Em vez disso, ele usa um único loop de eventos para gerenciar várias tarefas de forma eficiente.

Para aproveitar ao máximo o asyncio, é importante entender os conceitos básicos, como corrotinas, tarefas e o loop de eventos. As corrotinas são funções assíncronas que podem ser pausadas e retomadas posteriormente. As tarefas são objetos que representam corrotinas em execução. O loop de eventos é responsável por executar as tarefas e gerenciar os eventos de I/O.

Ao usar o asyncio, é possível lidar com problemas comuns, como condições de corrida. Uma condição de corrida ocorre quando duas ou mais tarefas tentam acessar ou modificar um recurso compartilhado ao mesmo tempo, resultando em comportamento indefinido ou incorreto.

Para evitar condições de corrida, é possível usar mecanismos de sincronização, como semáforos ou bloqueios. Esses mecanismos garantem que apenas uma tarefa possa acessar o recurso compartilhado de cada vez, evitando assim problemas de concorrência.

No entanto, é importante lembrar que o asyncio não é uma solução mágica para todos os problemas de concorrência. É necessário projetar cuidadosamente o código para evitar condições de corrida e garantir a consistência dos dados.

Com o conhecimento adequado do asyncio e das técnicas de sincronização, é possível escrever aplicativos assíncronos eficientes e robustos em 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 de RC

Limite de estouro / TOCTOU

Este é o tipo mais básico de condição de corrida, onde vulnerabilidades que aparecem em lugares que limitam o número de vezes que você pode executar uma ação. Como usar o mesmo código de desconto várias vezes em uma loja online. Um exemplo muito fácil pode ser encontrado neste relatório ou neste bug.

Existem muitas variações desse tipo de ataque, incluindo:

  • Resgatar um vale-presente várias vezes
  • Avaliar um produto várias vezes
  • Sacar ou transferir dinheiro além do saldo da sua conta
  • Reutilizar uma solução CAPTCHA única
  • Bypass de um limite de taxa anti-brute-force

Subestados ocultos

Outra condição de corrida mais complicada explorará subestados no estado da máquina que poderiam permitir que um invasor abusasse de estados aos quais ele nunca deveria ter acesso, mas há uma pequena janela para o invasor acessá-lo.

  1. Prever subestados ocultos e interessantes potenciais

O primeiro passo é identificar todos os pontos finais que gravam nele ou lêem dados dele e, em seguida, usam esses dados para algo importante. Por exemplo, os usuários podem ser armazenados em uma tabela de banco de dados que é modificada pelo registro, edição de perfil, iniciação de redefinição de senha e conclusão da redefinição de senha.

Podemos usar três perguntas-chave para descartar pontos finais que provavelmente não causarão colisões. Para cada objeto e os pontos finais associados, pergunte:

  • Como o estado é armazenado?

Dados armazenados em uma estrutura de dados persistente do lado do servidor são ideais para exploração. Alguns pontos finais armazenam seu estado inteiramente no lado do cliente, como redefinições de senha que funcionam enviando um JWT por e-mail - esses podem ser ignorados com segurança.

As aplicações costumam armazenar algum estado na sessão do usuário. Esses estados geralmente são um tanto protegidos contra subestados - mais sobre isso depois.

  • Estamos editando ou anexando?

Operações que editam dados existentes (como alterar o endereço de e-mail principal de uma conta) têm grande potencial de colisão, enquanto ações que simplesmente anexam a dados existentes (como adicionar um endereço de e-mail adicional) provavelmente não serão vulneráveis a nada além de ataques de limite de estouro.

  • Em que a operação é baseada?

A maioria dos pontos finais opera em um registro específico, que é procurado usando uma 'chave', como um nome de usuário, token de redefinição de senha ou nome de arquivo. Para um ataque bem-sucedido, precisamos de duas operações que usem a mesma chave. Por exemplo, imagine duas implementações plausíveis de redefinição de senha:

  1. Investigar pistas

Neste ponto, é hora de lançar alguns ataques de RC nos pontos finais potencialmente interessantes para tentar encontrar resultados inesperados em comparação com os regulares. Qualquer desvio da resposta esperada, como uma mudança em uma ou mais respostas, ou um efeito de segunda ordem, como conteúdos de e-mail diferentes ou uma mudança visível em sua sessão, pode ser uma pista indicando que algo está errado.

  1. Provar o conceito

A etapa final é provar o conceito e transformá-lo em um ataque viável.

Quando você envia um lote de solicitações, pode descobrir que um par de solicitações iniciais aciona um estado final vulnerável, mas solicitações posteriores sobrescrevem/invalidam esse estado e o estado final não pode ser explorado. Nesse cenário, você desejará eliminar todas as solicitações desnecessárias - duas devem ser suficientes para explorar a maioria das vulnerabilidades. No entanto, reduzir para duas solicitações tornará o ataque mais sensível ao tempo, portanto, talvez seja necessário tentar o ataque várias vezes ou automatizá-lo.

Ataques Sensíveis ao Tempo

Às vezes, você pode não encontrar condições de corrida, mas as técnicas para enviar solicitações com tempo preciso ainda podem revelar a presença de outras vulnerabilidades.

Um exemplo disso é quando carimbos de data/hora de alta resolução são usados em vez de strings aleatórias criptograficamente seguras para gerar tokens de segurança.

Considere um token de redefinição de senha que é apenas randomizado usando um carimbo de data/hora. Nesse caso, pode ser possível acionar duas redefinições de senha para dois usuários diferentes, que ambos usam o mesmo token. Tudo o que você precisa fazer é cronometrar as solicitações para que elas gerem o mesmo carimbo de data/hora.

{% hint style="warning" %} Para confirmar, por exemplo, a situação anterior, você pode simplesmente solicitar 2 tokens de redefinição de senha ao mesmo tempo (usando um ataque de pacote único) e verificar se eles são iguais. {% endhint %}

Verifique o exemplo neste laboratório.

Estudos de caso de subestados ocultos

Pagar e adicionar um item

Verifique este laboratório para ver como pagar em uma loja e adicionar um item extra que você não precisará pagar.

Confirmar outros e-mails

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

De acordo com este artigo, o Gitlab estava vulnerável a uma invasão dessa maneira porque poderia enviar o token de verificação de e-mail de um e-mail para o outro e-mail.

Você também pode verificar este laboratório para aprender sobre isso.

Estados ocultos do banco de dados / Bypass de confirmação

Se 2 gravações diferentes são usadas para adicionar informações dentro de um banco de dados, há uma pequena parte do tempo em que apenas os primeiros dados foram gravados no banco de dados. Por exemplo, ao criar um usuário, o nome de usuário e a senha podem ser gravados e, em seguida, o token para confirmar a conta recém-criada é gravado. Isso significa que por um curto período de tempo, o token para confirmar uma conta é nulo.

Portanto, registrar uma conta e enviar várias solicitações com um token vazio (token= ou token[]= ou qualquer outra variação) para confirmar a conta imediatamente pode permitir confirmar uma conta em que você não controla o e-mail.

Verifique este laboratório para ver um exemplo.

Bypassar 2FA

O seguinte pseudo-código demonstra como um site pode ser vulnerável a uma variação de corrida desse ataque:

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

Como você pode ver, isso é de fato uma sequência de vários passos dentro de uma única solicitação. Mais importante ainda, ela passa por um subestado em que o usuário temporariamente possui uma sessão válida logada, mas a MFA ainda não está sendo aplicada. Um atacante poderia potencialmente explorar isso enviando uma solicitação de login juntamente com uma solicitação a um endpoint autenticado sensível.

Persistência eterna do OAuth2

Existem vários provedores de OAuth. Esses serviços permitirão que você crie um aplicativo e autentique usuários registrados pelo provedor. Para fazer isso, o cliente precisará permitir que seu aplicativo acesse alguns de seus dados dentro do provedor de OAuth.
Então, até aqui, apenas um login comum com Google/LinkedIn/GitHub... onde você é solicitado em uma página dizendo: "O aplicativo <InserirNomeLegal> deseja acessar suas informações, você deseja permitir?"

Condição de corrida em authorization_code

O problema ocorre quando você aceita e automaticamente envia um authorization_code para o aplicativo malicioso. Em seguida, esse aplicativo abusa de uma Condição de Corrida no provedor de serviços de OAuth para gerar mais de um AT/RT (Authentication Token/Refresh Token) a partir do authorization_code para sua conta. Basicamente, ele abusará do fato de você ter aceitado o aplicativo para acessar seus dados e criará várias contas. Então, se você parar de permitir que o aplicativo acesse seus dados, um par de AT/RT será excluído, mas os outros ainda serão válidos.

Condição de corrida em Refresh Token

Depois de obter um RT válido, você pode tentar abusar dele para gerar vários AT/RT e, mesmo se o usuário cancelar as permissões para o aplicativo malicioso acessar seus dados, vários RTs ainda serão válidos.

CC em WebSockets

No WS_RaceCondition_PoC, você pode encontrar um PoC em Java para enviar mensagens de websocket em paralelo para abusar das Condições de Corrida também em WebSockets.

Referências

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


Use Trickest para criar e automatizar fluxos de trabalho com facilidade, usando as ferramentas comunitárias mais avançadas do mundo.
Acesse hoje mesmo:

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