TL; DR Este artigo descreve truques impopulares de condição de corrida que não são comumente usados nesse tipo de ataque. Com base nos resultados de nossa pesquisa, criamos nossa própria estrutura para ataques de corrida de cavalos.
Vasya quer transferir 100 dólares que ele tem em sua conta, Petya. Ele vai para a guia de transferências, leva o apelido de Petin para o campo com o número de fundos que precisam ser transferidos - o número 100. Em seguida, clica no botão de transferência. Dados para quem e quanto é enviado ao aplicativo Web. O que pode acontecer lá dentro? O que precisa ser feito pelo programador para que tudo funcione corretamente?
- Você precisa garantir que o valor esteja disponível para a Vasya para transferência.
É necessário obter o valor do saldo atual do usuário, se for menor que o valor que ele deseja transferir, informe-o. Dado o fato de que nosso site não concede empréstimos e não deve ter saldo negativo.
- Subtrair o valor a ser transferido do saldo do usuário
É necessário anotar o saldo do usuário atual com a dedução do valor transferido. Foi 100, tornou-se 100-100 = 0.
- Adicione ao saldo do usuário Petya o valor transferido.
Petya, pelo contrário, era 0, tornou-se 0 + 100 = 100.
- Mostre uma mensagem ao usuário que ele está bem feito!
Ao escrever programas, uma pessoa usa os algoritmos mais simples, que ele combina em um único gráfico, que será o script do programa. No nosso caso, a tarefa do programador é escrever a lógica das transferências de dinheiro (pontos, créditos) de uma pessoa para outra em um aplicativo da web. Guiado pela lógica, você pode criar um algoritmo que consiste em várias verificações. Imagine que acabamos de remover tudo o que é desnecessário e compilamos um pseudocódigo.
(. >= _) .=.-_ .=.+_ () ()
Mas tudo ficaria bem se tudo acontecesse por sua vez. Mas um site pode atender muitos usuários ao mesmo tempo, e isso não acontece em um único encadeamento, porque os aplicativos da Web modernos usam multiprocessamento e multithreading para processamento de dados paralelo. Com o advento do multithreading, os programas têm uma vulnerabilidade arquitetônica engraçada - condição de corrida (ou condição de corrida).
Agora imagine que nosso algoritmo funcione simultaneamente 3 vezes.
Vasya ainda tem 100 pontos em sua balança, mas de alguma forma ele se voltou para o aplicativo Web em três threads ao mesmo tempo (com um período mínimo de tempo entre solicitações). Todos os três fluxos verificam se o usuário é Petya e se Vasya possui saldo suficiente para a transferência. No momento em que o algoritmo verifica a balança, ele ainda é igual a 100. Quando a verificação é concluída, 100 são subtraídas da balança atual três vezes e Pete é adicionado.
O que nós temos? Vasya tem um saldo negativo em sua conta (100 - 300 = -200 pontos). Enquanto isso, Petya tem 300 pontos, embora na verdade deva ser 100. Este é um exemplo típico da exploração de uma condição de corrida. É comparável ao fato de várias pessoas passarem uma passagem por vez. Abaixo está uma captura de tela desta situação do
4lemon
A condição de corrida pode estar em aplicativos multithread, bem como nos bancos de dados em que eles trabalham. Não necessariamente em aplicativos da web, por exemplo, este é um critério comum para escalação de privilégios em sistemas operacionais. Embora os aplicativos da Web tenham suas próprias características para uma operação bem-sucedida, sobre o qual quero falar.
Operação típica de condição de corrida
Um hacker entra em uma sala de narguilé, em uma missão e em um bar, e para ele - você tem uma condição de corrida! Omar Ganiev
Na maioria dos casos, o software multithread é usado como um cliente para verificar / operar a condição de corrida. Por exemplo, Burp Suite e sua ferramenta Intruder. Eles colocam uma solicitação HTTP para repetição, instalam muitos fluxos e ativam o fluxo. Como por exemplo
neste artigo . Ou
neste . Essa é uma maneira bastante funcional, se o servidor permitir o uso de vários threads em seu recurso e, como dizem nos artigos acima, se não funcionar, tente novamente. Mas o fato é que, em algumas situações, isso pode não ser eficaz. Especialmente se você se lembrar de como esses aplicativos acessam o servidor.
O que há no servidor
Cada encadeamento estabelece uma conexão TCP, envia dados, aguarda uma resposta, fecha a conexão, abre novamente, envia dados e assim por diante. À primeira vista, todos os dados são enviados simultaneamente, mas as solicitações HTTP podem não chegar síncronas e são inconsistentes devido à natureza da camada de transporte, à necessidade de estabelecer uma conexão segura (HTTPS) e resolver o DNS (não no caso de burp) e muitas camadas abstrações que transmitem dados antes de serem enviadas para um dispositivo de rede. Quando se trata de milissegundos, isso pode desempenhar um papel fundamental.
Pipelining HTTP
Você pode recuperar o pipelining HTTP, no qual é possível enviar dados usando um único soquete. Você pode ver por si mesmo como ele funciona usando o utilitário netcat (você tem GNU / Linux, certo?).
De fato, você precisa usar o linux por vários motivos, porque existe uma pilha TCP / IP mais moderna, suportada pelos kernels do sistema operacional. O servidor provavelmente também está nele.Por exemplo, execute
nc google.com 80 e insira as linhas lá
GET / HTTP/1.1 Host: google.com GET / HTTP/1.1 Host: google.com GET / HTTP/1.1 Host: google.com
Assim, dentro de uma conexão, três solicitações HTTP serão enviadas e você receberá três respostas HTTP. Esse recurso pode ser usado para minimizar o tempo entre solicitações.
O que há no servidor
O servidor da Web receberá solicitações sequencialmente (palavra-chave) e processará as respostas em ordem de prioridade. Esse recurso pode ser usado para atacar em várias etapas (quando é necessário executar sequencialmente duas ações no período mínimo de tempo) ou, por exemplo, para desacelerar o servidor na primeira solicitação, a fim de aumentar o sucesso do ataque.
Truque - você pode impedir que o servidor processe sua solicitação carregando seu DBMS, especialmente se INSERT / UPDATE for usado. Solicitações mais pesadas podem "desacelerar" sua carga, portanto, será mais provável que você vença essa corrida.
Dividindo uma solicitação HTTP em duas partes
Primeiro, lembre-se de como a solicitação HTTP é gerada. Bem, como você sabe, a primeira linha é a versão do método, caminho e protocolo:
GET / HTTP/1.1
A seguir, os cabeçalhos antes da quebra de linha:
Host: google.com
Cookie: a=1
Mas como o servidor da Web sabe que a solicitação HTTP terminou?
Vejamos um exemplo, insira
nc google.com 80 e lá
GET / HTTP/1.1
Host: google.com
GET / HTTP/1.1
Host: google.com
, depois que você pressionar ENTER, nada acontecerá. Clique novamente - você verá a resposta.
Ou seja, para o servidor da Web aceitar a solicitação HTTP, são necessários dois feeds de linha. Uma consulta válida se parece com isso:
GET / HTTP/1.1\r\nHost: google.com\r\n\r\n
Se esse fosse o método POST (não se esqueça do Comprimento do conteúdo), a solicitação HTTP correta seria assim:
POST / HTTP/1.1
Host: google.com
Content-Length: 3
a=1
ou
POST / HTTP/1.1\r\nHost: google.com\r\nContent-Length: 3\r\n\r\na=1
Tente enviar uma solicitação semelhante na linha de comando:
echo -ne "GET / HTTP/1.1\r\nHost: google.com\r\n\r\n" | nc google.com 80
Como resultado, você receberá uma resposta, pois nossa solicitação HTTP está concluída. Mas se você remover o último \ n caractere, não receberá uma resposta.
De fato, muitos servidores da Web precisam usar \ n como transferência, por isso é importante não trocar \ re \ n, caso contrário, outros truques podem não funcionar.
O que isso dá? Você pode abrir simultaneamente várias conexões com um recurso, enviar 99% da sua solicitação HTTP e deixar o último byte não enviado. O servidor aguardará até você chegar ao último avanço de linha. Depois que ficar claro que a maior parte dos dados foi enviada, envie o último byte (ou vários).
Isso é especialmente importante quando se trata de uma solicitação POST grande, por exemplo, quando o upload de arquivo é necessário. Mas, mesmo em uma pequena solicitação, isso faz sentido, já que fornecer alguns bytes é muito mais rápido do que simultaneamente kilobytes de informações.
Atraso antes de enviar a segunda parte da solicitação
Segundo a pesquisa de
Vlad Roskov , é necessário não apenas dividir a solicitação, mas também faz sentido demorar alguns segundos entre o envio da parte principal dos dados e a final. E tudo porque os servidores da Web começam a analisar solicitações antes mesmo de recebê-las na íntegra.

O que há no servidor
Por exemplo, ao receber cabeçalhos de solicitação HTTP, o nginx começará a analisá-los, armazenando em cache a solicitação com defeito. Quando o último byte chega, o servidor da Web pega a solicitação parcialmente processada e a envia diretamente para o aplicativo, reduzindo assim o tempo de processamento das solicitações, o que aumenta a probabilidade de um ataque.
Como lidar com isso
Antes de tudo, é claro que isso é um problema de arquitetura. Se você projetar adequadamente um aplicativo da Web, poderá evitar essas corridas.
Normalmente, os seguintes métodos de controle de ataque são usados:
A operação bloqueia o acesso ao objeto bloqueado no DBMS até que ele seja desbloqueado. Outros ficam e esperam à margem. É necessário trabalhar com bloqueios corretamente, para não bloquear nada supérfluo.
Transações ordenadas (serializáveis) - assegure-se de que as transações sejam executadas estritamente em sequência, no entanto, isso pode afetar o desempenho.
Tome alguma coisa (por exemplo, etcd). No momento da chamada das funções, é criada uma entrada com uma tecla; se não foi possível criar uma entrada, ela já existe e a solicitação é interrompida. No final do processamento da solicitação, o registro é excluído.
E, em geral, gostei do
vídeo de Ivan, o Trabalhador, sobre bloqueios e transações, muito informativo.
Recursos da sessão em condição de corrida
Uma das características das sessões pode ser que, por si só, interfere na exploração da corrida. Por exemplo, no PHP, após session_start (), um arquivo de sessão é bloqueado e seu desbloqueio ocorre apenas no final do script (se não houver chamada para
session_write_close ). Se outro script que usa uma sessão for chamado neste momento, ele aguardará.
Para contornar esse recurso, você pode usar um truque simples - para autenticar quantas vezes for necessário. Se o aplicativo da web permitir criar várias sessões para um usuário, basta coletar todo o PHPSESSID e fazer com que cada solicitação seja seu próprio identificador.
Proximidade com o servidor
Se o site no qual você deseja operar a condição de corrida estiver hospedado na AWS - leve o carro na AWS. Se estiver no DigitalOcean - leve-o para lá.
Quando a tarefa é enviar solicitações e minimizar o intervalo de envio entre elas, a proximidade imediata com o servidor da Web será, sem dúvida, uma vantagem.
Afinal, há uma diferença ao executar ping no servidor de 200 e 10 ms. E se você tiver sorte, pode até acabar no mesmo servidor físico, será um pouco mais fácil voar :)
Resumir
Para uma condição de corrida bem-sucedida, você pode aplicar vários truques para aumentar a probabilidade de sucesso. Envie várias solicitações de manutenção de funcionamento em uma, diminuindo a velocidade do servidor da web. Divida a solicitação em várias partes e crie um atraso antes do envio. Reduza a distância para o servidor e o número de abstrações para a interface de rede.
Como resultado dessa análise, juntamente com Michail Badin, desenvolvemos a ferramenta
RacePWNConsiste em dois componentes:
- Biblioteca C librace, que envia muitas solicitações HTTP para o servidor no menor tempo possível e usa a maioria dos recursos do artigo
- Utilitários racepwn, que aceita a configuração json e geralmente guia essa biblioteca
O RacePWN pode ser integrado a outros utilitários (por exemplo, no Burp Suite), ou você pode criar uma interface da web para gerenciar voos (você ainda não consegue usá-lo). Aproveite!
Mas, na realidade, ainda há espaço para crescer e você pode recordar o HTTP / 2 e suas perspectivas de ataque. No momento, porém, o HTTP / 2, a maioria dos recursos possui apenas solicitações de proxy antecipadas para o bom e velho HTTP / 1.1.
Talvez você conheça outras sutilezas?
O original