
Detecta e explora vulnerabilidades de contrabando de requisições HTTP através da conversão de HTTP/2 para HTTP/1.1, utilizando técnicas automatizadas de contrabando de cabeçalhos para identificar discrepâncias de análise no backend.
Esta ferramenta ajuda a detectar e explorar o contrabando de requisições HTTP em casos em que pode ser alcançado via conversão HTTP/2 -> HTTP/1.1 pelo servidor frontend.
O esquema é o seguinte:
O atacante quer encontrar uma requisição que seja vista como duas requisições separadas pelo servidor backend.
Se a conexão HTTP/1.1 entre frontend<->backend usar keep-alive, o frontend pode enviar requisições de outros usuários para a mesma conexão. Se formos capazes de "envenenar" a conexão com uma requisição parcial que vem após uma legítima, podemos recuperar a requisição de outro usuário.
Outros cenários possíveis incluem contornar a proteção e reescritas do servidor frontend, envenenamento de cache ou engano de cache.
Para mais informações sobre o Contrabando de Requisições HTTP, consulte a Portswigger Web Security Academy.
No HTTP/2, todos os nomes e valores dos cabeçalhos HTTP são binários. Isso significa que tecnicamente eles podem conter espaços adicionais ou até mesmo quebras de linha.
RFC7540#10.3 afirma que implementações que traduzem requisições HTTP/2 para HTTP/1 devem cuidar das limitações no conjunto de caracteres que surgem dessa conversão; a maioria das implementações de fato os rejeita. Apesar disso, esperamos encontrar algumas que permitam tais cabeçalhos. Eles irão corromper a requisição HTTP/1.1 convertida para o backend.
Outro ponto é que algumas correções recentes relacionadas ao Contrabando de Requisições HTTP podem ser implementadas apenas para parsers HTTP/1.1.
Em geral, esperamos que existam implementações de HTTP/2 que não estejam muito cientes das pesquisas recentes sobre Contrabando de Requisições HTTP em HTTP/1.1 e não incluam mitigações correspondentes.
Surpreendentemente, sim!
Encontrei a possibilidade de contrabandear um cabeçalho com um caractere de espaço através do Cloudflare, abrindo assim uma porta para contrabando entre Cloudflare e cliente (caso o software de um cliente do Cloudflare aceite e faça trim nos nomes dos cabeçalhos). Aqui está o post no blog.
Há também outro relatório de bug bounty que ainda não é público. Ele faz uso do fato de que softwares personalizados não filtram quebras de linha em cabeçalhos HTTP2, e o contrabando acontece 100% (consigo ver requisições de outros usuários).
No entanto, entendo que esse tipo de vulnerabilidade deve ser frustrantemente raro: ao contrário do HTTP/1.1, não existem tantas implementações de HTTP/2, e a maioria delas é feita com segurança em mente, rejeitando cabeçalhos suspeitos ou inválidos.
A ferramenta possui um subcomando que tenta detectar automaticamente se um alvo é vulnerável ao ataque de Contrabando de Requisições HTTP. O algoritmo por trás dessa funcionalidade é descrito nesta seção.
Para realizar um ataque de Contrabando de Requisições HTTP, na verdade precisamos primeiro "contrabandear" um único cabeçalho (seja Content-Length ou Transfer-Encoding). Isso significa que precisamos enviar um cabeçalho que a) controle onde o corpo da requisição termina e b) não seja processado pelo frontend, mas seja processado pelo backend.
Isso geralmente é conseguido modificando um cabeçalho de alguma forma: adicionando espaços ou tabulações ao final do seu nome, substituindo o valor por um semiequivalente, etc.
A ideia básica do algoritmo de detecção de vulnerabilidade é detectar se o servidor realmente processa um cabeçalho contrabandeado como se fosse Content-Length ou Transfer-Encoding. Fazemos isso enviando múltiplas requisições: algumas com valores válidos e outras com valores inválidos para o cabeçalho. Em seguida, tentamos detectar se há uma maneira de distinguir as respostas desses dois grupos.
É por isso que a saída da ferramenta não contém as palavras "vulnerável/invulnerável": ela apenas informa se consegue distinguir as respostas que vieram das requisições desses dois grupos.
A ferramenta considera dois conjuntos de respostas HTTP como distinguíveis se pelo menos uma das duas condições a seguir for atendida:
Os timeouts são tratados como um valor de código de status único, não igual a nenhum outro; assim, a ferramenta substitui o esquema clássico de "detectar por tempo".
Vamos considerar um exemplo. Suponha que estamos tentando contrabandear o cabeçalho transfer-encoding substituindo o traço por um sublinhado.
Se parecer que o servidor responde com status 400 toda vez que enviamos transfer_encoding:zalupa e trava quando é transfer_encoding:chunked, podemos dizer que o servidor provavelmente processa o cabeçalho como o valor para transfer encoding. Teoricamente, poderia ser o servidor frontend ou backend.
O primeiro caso não é interessante, pois poderíamos enviar a versão não contrabandeada do cabeçalho de qualquer forma, e o segundo é o que procuramos. Como tudo acontece sobre HTTP/2, o primeiro caso é evitável na maioria das vezes: o servidor HTTP/2 determina o final do corpo da requisição de outra forma, não relacionada aos cabeçalhos da requisição, e nunca espera que o corpo esteja no formato chunked do HTTP/1.1 (aquele com comprimentos de chunk hexadecimais).
As variações concretas das técnicas de detecção são:
Enviamos uma versão contrabandeada de Transfer-Encoding: chunked (por exemplo, transfer_encoding:chunked) e corpos diferentes: válido é 0\r\n\r\n e inválido é 999\r\n.
Caso as respostas sejam diferentes, podemos ter certeza de que o servidor backend recebe e processa o cabeçalho contrabandeado. Não há razão para o frontend fazer isso: HTTP/2 não usa o formato chunked que enviamos, então seria inválido.
Esperamos que o backend trave (ou seja, a requisição expire) para as requisições inválidas, pois ele aguarda a chegada de mais dados.
Esta é a variante de detecção mais confiável: se o servidor travar ao ler o corpo, algo provavelmente deu errado, já que não há uso para transfer encoding HTTP/1.1 em uma requisição HTTP/2.
Enviamos uma versão contrabandeada de Transfer-Encoding e corpos diferentes novamente: 0\r\n\r\n como corpo válido e X\r\n\r\n como inválido.
O caso é o mesmo que acima, mas em vez de ler o corpo, esperamos que o backend pelo menos o valide.
Enviamos uma versão contrabandeada do cabeçalho Content-Length com valores 1 e -1.
Ao enviar múltiplos pares de requisições válidas/inválidas, podemos reduzir a chance de falsos positivos aleatórios. Por outro lado, podemos parar cedo se percebermos que não há como separar as respostas das requisições "válidas" das respostas das "inválidas".
A ferramenta tenta empregar múltiplas técnicas de contrabando modificando um cabeçalho de várias maneiras. Nenhuma delas é nova e ao mesmo tempo óbvia.
Para contrabandear um cabeçalho, anexamos um espaço a ele. É o método mais comum e clássico. Esperamos que o cabeçalho não seja processado pelo frontend, mas seja enviado como está para o backend, que remove o espaço.
A ferramenta tenta uma variedade de caracteres como espaços: inclui , \t, \v, \x00 e os Unicode.
Para contrabandear um cabeçalho, substituímos o traço (-) por um sublinhado (_). Se o backend for inspirado em CGI de alguma forma, pode converter cabeçalhos como Header-Name para a forma HEADER_NAME; assim, o traço se tornará sublinhado de qualquer forma. Ao determinar como parsear o corpo, esse backend supostamente solicita o valor de CONTENT_LENGTH / TRANSFER_ENCODING de seu dicionário de cabeçalhos, e ele estará lá.
Este é específico do HTTP/2. Como HTTP/2 é um protocolo binário, podemos tentar enviar quebras de linha no nome ou valor do cabeçalho. O padrão proíbe, mas esperamos encontrar uma implementação que ainda aceite isso.
Durante a conversão HTTP/2 -> HTTP/1.1, o cabeçalho se divide em dois cabeçalhos diferentes, o que significa que a requisição parecerá diferente para o backend.
Para contrabandear um cabeçalho, colocamos seu nome e valor após uma quebra de linha: um cabeçalho com o nome "Transfer-Encoding" e o valor "chunked" se torna um com o nome "fake" com o valor "fake\r\ntransfer-encoding: chunked".
Suponha que o backend use alguma linguagem de alto nível e não realize validação suficiente dos cabeçalhos. Nesse caso, ele pode converter os nomes para maiúsculas antes de fazer qualquer outra coisa e fazer isso usando funções que lidam com Unicode. Felizmente, TRANSFER-ENCODING contém a letra S, que é ſ (\u017f) em maiúscula.
Da mesma forma, podemos procurar por um backend que converta o valor de Transfer-Encoding para minúsculas: enviamos chunKed em vez de chunked com \u212a em vez de K.
Claro, é necessário que o frontend passe nomes/valores de cabeçalho UTF-8 para o backend.
Para instalar a ferramenta, execute go install github.com/neex/http2smugl@latest.
A ferramenta contém dois subcomandos: request e detect. O primeiro é apenas para criar requisições HTTP/2: a maioria das ferramentas de cliente não aceita cabeçalhos inválidos, então é útil ter uma que envie a entrada do usuário ao servidor como está.
O outro é detect. Ele tenta várias técnicas de contrabando de requisições HTTP para detectar se o alvo é vulnerável. O algoritmo de detecção é complicado; agradeço se você ler e me enviar seus comentários. Ele está descrito abaixo na seção correspondente.
http2smugl requestUse este subcomando para enviar uma requisição http2 (provavelmente um pouco malformada). O primeiro parâmetro é a URL, e os outros são apenas cabeçalhos no formato nome:valor (note que não há espaço após os dois pontos). A escapada com barra invertida é suportada: você pode usar códigos de escape \r,\n e \xXX. Por exemplo, para enviar um nome de cabeçalho que contenha dois pontos, use \x3a (ex.: nome\x3acom\x3adoispontos:valor).
http2smugl detectEste subcomando tenta detectar o contrabando de requisições HTTP usando várias técnicas. Para usá-lo, basta executar http2smugl detect [URL HTTPS].
O comando só produzirá saída se conseguir detectar que o servidor interpreta um cabeçalho contrabandeado. Para entender o que isso significa, leia a seção correspondente.
Um suporte experimental para HTTP/3 (quic) foi implementado. No entanto, não sugiro usá-lo, pois não encontrei nenhum bug relacionado ao HTTP/3.
Para usar HTTP/3 com o subcomando request, forneça o proto https+h3:// na URL em vez de apenas https. O mesmo é suportado no comando detect.
Há também uma flag --try-http3 para o subcomando request, que altera o comportamento caso o protocolo não seja especificado na URL (apenas hostname). Se a flag estiver presente, o comando tentará o proto https+h3 para tais entradas na linha de comando ou no arquivo de alvos (assim como HTTP/2). Ex.: http2smugl detect --try-http3 www.example.com tentará tanto HTTP/3 quanto HTTP/2, mas http2smugl detect --try-http3 https://www.example.com/ ainda tentará apenas HTTP/2.
Nesta seção, descrevo alguns casos em que a ferramenta diz que as respostas são "distinguíveis", mas nenhuma vulnerabilidade poderia existir.
O Elastic Load Balancer da Amazon implementa múltiplas mitigações contra Contrabando de Requisições HTTP. Embora nem todas rejeitem a requisição por padrão, o ELB não reutiliza uma conexão após enviar uma requisição com cabeçalhos "suspeitos". Portanto, um ataque real não é possível.
Para detectar se você está lidando com ELB, você pode usar o cabeçalho de resposta Server. Se ele estiver filtrado, você pode enviar uma requisição com o cabeçalho content__length (note dois sublinhados) e valor -1: se for 400, provavelmente é ELB ou outro WAF (veja abaixo).
O Apache Traffic Server é usado principalmente no Yahoo. Ele processa HTTP/2 de forma incomum: converte-o para HTTP/1.1 na memória e depois re-interpreta a requisição resultante. Assim, embora um cabeçalho tecnicamente possa ser "contrabandeado", não há como resultar em uma vulnerabilidade: você poderia enviar os mesmos bytes em uma conexão HTTP/1.1.
A maneira mais fácil de detectar se você está lidando com ATS (além do cabeçalho Server) é enviando uma requisição TRACE. Se um cabeçalho Max-Forwards: 0 estiver presente na requisição, o ATS retornará uma resposta para requisições TRACE por padrão sem encaminhá-la ao backend.
Parece que o Microsoft IIS suporta decodificar codificação chunked dentro de corpos HTTP/2. Esse é um comportamento estranho; no entanto, é inofensivo do ponto de vista de segurança.
Para detectar se você está lidando com IIS, você pode enviar uma requisição com um cabeçalho "transfer-encoding:chunked" e um corpo chunked incorreto. Se você vir Microsoft-HTTPAPI ou algo assim no cabeçalho Server, é isso.
Os WAFs tentam detectar cabeçalhos suspeitos - é o trabalho deles. Às vezes, isso resulta na ferramenta dizendo "distinguível": o WAF pode bloquear algo como content_length:-1 e permitir content_length:1 apenas porque seus filtros acabam tomando essas decisões.
Se você vir um WAF no cabeçalho Server, provavelmente é um falso positivo.
Se você tiver alguma ideia sobre este tópico, entre em contato comigo via @emil_lerner no Twitter ou @neexemil no Telegram - ou poste um problema aqui no GitHub.
Ambos os valores são inválidos do ponto de vista do frontend: há outro mecanismo para determinar o comprimento do corpo em HTTP/2, e nenhum corpo de requisição é realmente enviado em ambos os casos. Se as respostas forem diferentes, supomos que foi o servidor backend que parseou os cabeçalhos.
Este método é o menos confiável: o frontend pode emitir erros diferentes quando Content-Length tem um valor inválido e quando não corresponde ao comprimento real.