3 pontos por GN⁺ 2024-04-06 | 1 comentários | Compartilhar no WhatsApp
  • HTTP/2 CONTINUATION Flood é uma classe de vulnerabilidades de implementação em que o envio contínuo de frames de cabeçalho sem END_HEADERS pode derrubar a disponibilidade do servidor
  • Como a requisição do ataque não é concluída, ela não aparece nos logs de acesso HTTP, e identificar a causa pode exigir análise dos bytes do tráfego bruto
  • Dependendo da implementação, o impacto varia entre esgotamento de CPU, OOM com múltiplas conexões, OOM com uma única conexão e até crash por bug no momento da desconexão
  • Nos casos de Go, Firefox e Node.js, apareceram respectivamente a continuidade da decodificação HPACK, a ausência de limite para o tamanho dos cabeçalhos de resposta e o conflito entre desconexão durante o processamento de CONTINUATION e a atualização do contador de memória
  • Diferentemente do Rapid Reset, em muitas implementações era possível causar crash no servidor com uma única conexão TCP, o que significava potencial impacto sobre uma ampla variedade de serviços de internet que usam HTTP/2

Como frames CONTINUATION são usados no HTTP/2

  • HTTP/2 é um protocolo que troca frames binários, em vez de linhas de texto como no HTTP/1.1
  • Frames HEADERS enviam os cabeçalhos HTTP de requisições e respostas, e os dados de cabeçalho ficam armazenados em um field block fragment codificado com HPACK
  • O frame HEADERS possui flags que indicam o fim dos cabeçalhos e do stream
    • END_HEADERS: sinaliza que aquele frame contém todos os cabeçalhos a serem enviados
    • END_STREAM: sinaliza que não haverá mais corpo na requisição ou resposta
  • Os frames têm um tamanho máximo definido no início da comunicação, e se um frame recebido ultrapassar o tamanho permitido, a conexão é encerrada com erro de protocolo
  • Se não for possível colocar todos os cabeçalhos em um único frame HEADERS, frames CONTINUATION são enviados em sequência após um HEADERS sem END_HEADERS
    • frame HEADERS sem END_HEADERS
    • frames CONTINUATION adicionais sem END_HEADERS
    • END_HEADERS definido no último frame CONTINUATION
  • Após o último frame de cabeçalho, vem um frame DATA com os dados da requisição ou o stream HTTP/2 é encerrado

O núcleo da vulnerabilidade: um stream de cabeçalhos que nunca termina

  • Se o cliente iniciar um novo stream HTTP/2 e então enviar frames HEADERS e CONTINUATION sem nunca definir END_HEADERS, o servidor tentará continuar analisando e armazenando um stream infinito de cabeçalhos
  • Servidores HTTP/1.1 normalmente têm dois mecanismos para impedir cabeçalhos infinitos
    • limite de tamanho de cabeçalhos que encerra a conexão quando a lista de cabeçalhos ultrapassa o tamanho permitido
    • timeout de requisição/cabeçalho que encerra a conexão se a requisição ou os cabeçalhos não forem enviados a tempo
  • Em várias implementações de HTTP/2, essas proteções não existiam ou estavam incorretas, incluindo Apache httpd, Envoy e vários pacotes e codecs HTTP/2
  • Os resultados variavam por implementação em quatro categorias
    • Esgotamento de CPU: ao continuar lendo e decodificando cabeçalhos adicionais, o uso de CPU cresce e outras respostas ficam lentas ou bloqueadas
    • OOM baseado em múltiplas conexões: os cabeçalhos CONTINUATION ficam armazenados em memória e, embora exista limite para o tamanho da lista de cabeçalhos, a ausência de timeout faz cada conexão reter memória continuamente
    • OOM com uma única conexão: algumas implementações continuam lendo cabeçalhos até a memória encher e o SO encerrar o processo
    • Crash com poucos frames: quando a conexão cai no meio do stream CONTINUATION, um bug de implementação faz o servidor falhar
  • Sem END_HEADERS, a requisição não é encerrada corretamente, então as requisições do cliente malicioso não são registradas nos logs de acesso

Caso do Go: esgotamento de CPU porque a decodificação HPACK não para

  • Go é um caso marcante de esgotamento de CPU no CONTINUATION Flood
  • A implementação em Go agrupa um frame HEADERS, zero ou mais frames CONTINUATION e o decodificador HPACK na abstração http2MetaHeadersFrame
  • readMetaFrame chama SetEmitEnabled(false) quando atinge o limite de tamanho dos cabeçalhos ou ocorre um erro, interrompendo a emissão dos cabeçalhos decodificados
  • Porém, mesmo depois de parar a emissão dos cabeçalhos, o decodificador HPACK continua decodificando os bytes de entrada
  • O loop que fornece os frames só para quando HeadersEnded() se torna true, o que acontece quando a flag END_HEADERS é definida
  • Se o atacante não enviar END_HEADERS, readMetaFrame nunca retorna, e o decodificador HPACK continua processando novos bytes enquanto eles forem enviados

Casos de OOM e impacto no cliente Firefox

  • O OOM ocorre em implementações que não limitam o tamanho da lista de cabeçalhos formada pelos frames CONTINUATION
  • Em implementações sem timeout de cabeçalho, era possível causar crash no servidor com uma única conexão HTTP/2
  • Mesmo em implementações com idle timeout, era possível fazer várias conexões HTTP/2 ocuparem quase o limite de RAM por conexão e depois manter essas conexões vivas enviando o último frame CONTINUATION byte a byte a cada poucos segundos
  • O CONTINUATION Flood pode ocorrer não apenas em servidores, mas também no lado do cliente, como em navegadores
  • O commit de correção do Mozilla Firefox adicionou uma verificação que retorna erro de sessão PROTOCOL_ERROR se a soma do tamanho agregado dos cabeçalhos com o tamanho do novo frame ultrapassar network_http_max_response_header_size()

Caso do Node.js: crash por assertion durante desconexão

  • O Node.js lidava adequadamente com o stream infinito de frames CONTINUATION, mas ocorria uma data race quando a conexão era encerrada no meio do stream de cabeçalhos
  • Durante a execução do código de ataque, o Node.js caía com falha de assertion CHECK_EQ(current_nghttp2_memory_, 0) em Http2Session::~Http2Session()
  • O crash estava ligado ao instante exato em que o cliente HTTP/2 encerrava a conexão com o servidor Node.js, e a assertion ficava no destrutor de Http2Session
  • O Node.js embute a biblioteca nghttp2 para tratar conexões HTTP/2
  • current_nghttp2_memory_ rastreia a memória alocada internamente no nghttp2 e, após session_.reset() no destrutor, verifica se todos os artefatos do nghttp2 foram removidos da memória
  • A investigação mostrou que havia casos em que callbacks do nghttp2 e reset() eram executados juntos durante o parsing de frames CONTINUATION
    • um frame CONTINUATION chegava no estado NGHTTP2_IB_EXPECT_CONTINUATION
    • o estado mudava para NGHTTP2_IB_READ_HEADER_BLOCK
    • o fluxo seguia por session_after_header_block_received, session_call_on_frame_received e on_frame_recv_callback
    • o Node.js atualizava o contador de memória em OnFrameReceive e HandleHeadersFrame
  • Se HandleHeadersFrame e Http2Session::~Http2Session() executarem ao mesmo tempo, current_session_memory_ é atualizado simultaneamente, current_nghttp2_memory_ fica negativo e CHECK_EQ falha

Diferença em relação às vulnerabilidades de HTTP/2 de 2019

  • O conjunto de vulnerabilidades de HTTP/2 relatado por Netflix e Google em 2019 está resumido na Vulnerability Note do CERT/CC VU#605641
  • A CVE-2019-9516, “0-Length Headers Leak”, é um problema em que algumas implementações alocam memória para nomes e valores de cabeçalho de tamanho zero e mantêm essa memória até o fim da sessão
  • O CONTINUATION Flood não usa cabeçalhos vazios, mas sim muitos cabeçalhos aleatórios até o limite de tamanho de frame configurado no servidor
  • A CVE-2019-9518, “Empty Frame Flooding”, é um problema em que frames DATA, HEADERS, CONTINUATION, PUSH_PROMISE e outros com payload vazio são enviados sem end-of-stream, fazendo a outra ponta gastar tempo de processamento desproporcional à banda usada no ataque
  • O CONTINUATION Flood não usa frames vazios, mas frames tão grandes quanto possível para ocupar memória e consumir ciclos de CPU durante a decodificação

Por que poderia ser mais grave que o Rapid Reset

  • Em outubro de 2023, foram divulgados os detalhes do zero-day de protocolo HTTP/2 chamado “Rapid Reset”, descrito como “o maior ataque DDoS já visto até hoje”
  • O Rapid Reset usa a combinação de um frame HEADERS com END_STREAM e END_HEADERS definidos e um frame RST_STREAM
  • Nesse modelo, mitigações padrão como rate limiting podem reduzir os danos, e administradores de servidor conseguem ver muitos requests de entrada nos logs e receber alertas
  • No CONTINUATION Flood, como não existe END_HEADERS, nenhuma única requisição chega a ser concluída, e os administradores não conseguem ver requests nos logs
  • Em muitas implementações, o CONTINUATION Flood podia causar crash no servidor com apenas uma única conexão TCP, e em alguns casos com pouquíssimos dados
  • O Rapid Reset foi usado em ataques DDoS e, na maioria dos casos, um ataque bem-sucedido exigia uma botnet

Impacto potencial sobre serviços de internet

  • Segundo o Cloudflare Radar, o tráfego HTTP/2 representa cerca de 60% do tráfego HTTP humano, excluindo bots
  • O Cloudflare Radar estima que o tráfego HTTP representa mais de 70% de toda a transmissão na internet
  • Considerando a importância dos projetos afetados e a facilidade de exploração, uma grande parte da internet estava exposta a essa vulnerabilidade
  • HTTP é usado não só em sites, mas também em muitas APIs RESTful
  • Problemas de disponibilidade em APIs e sites críticos de empresas e governos podem causar perdas de milhões de dólares ou grande confusão
  • Se a falha tivesse sido explorada, a depuração poderia ser muito difícil para administradores sem conhecimento de HTTP/2
    • a requisição HTTP maliciosa não é encerrada corretamente
    • a requisição não aparece nos logs de acesso do servidor
    • a maioria dos servidores HTTP/2 não tem recursos avançados de análise de frames
    • pode ser necessário analisar manualmente os dados brutos da conexão

Divulgação coordenada e resposta do CERT/CC

  • Essa classe de vulnerabilidades representava um risco considerável para a segurança da internet
  • Após o reporte em janeiro de 2024, o CERT/CC abriu um caso de Vulnerability Coordination para acompanhar o problema
  • Várias grandes empresas de tecnologia e projetos open source participaram do processo de divulgação responsável relacionado ao tema
  • Como é difícil para um único pesquisador verificar todas as implementações, problemas que afetam vários fornecedores exigem Vulnerability Coordination
  • O CERT/CC publicou a Vulnerability Note sobre o problema, e esse tipo de nota é publicado apenas algumas vezes por ano

1 comentários

 
GN⁺ 2024-04-06
Comentários do Hacker News
  • No mês passado, mitigamos exatamente esse problema no Bandit
    https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
    Do ponto de vista de quem implementa, sinceramente, é algo óbvio demais para não bloquear. Eu já vinha me preocupando com isso havia muito tempo e achava que outras implementações naturalmente também estariam se defendendo

    • Você sabe o que acontece quando a gente presume. Isso transforma você e eu em manchete da primeira página
  • Nos últimos meses, verifiquei dezenas de implementações e, estranhamente, até os principais servidores HTTP/2 não tinham essa proteção ou a tinham implementado de forma incorreta
    Essencialmente, vejo isso como resultado de uma cultura de desenvolvimento acostumada a expandir tudo automaticamente de forma dinâmica e a não se preocupar com o quanto o tamanho pode crescer
    Esse tipo de problema não se limita ao HTTP/2, mas é bem provável que a enorme complexidade do HTTP/2 tenha contribuído. Na época do HTTP/1.x, havia mais desenvolvedores acostumados a linguagens como C, então eles prestavam atenção constante ao gerenciamento do tamanho dos buffers e não teriam deixado as alocações de cabeçalhos crescerem indefinidamente numa requisição inteira, mesmo que alguns KB bastassem

    • As pessoas continuam se concentrando e otimizando apenas o caminho feliz, mas não param para pensar no que acontece se um atacante provocar deliberadamente, e repetidas vezes, a pior situação possível
      Muitos ataques de negação de serviço, como slowloris e colisões de hash em parâmetros de consulta, se tornam reais porque o uso limitado de recursos só é considerado depois
  • > Sem impacto: Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]
    0: https://nowotarski.info/http2-continuation-flood/

    • Em outras palavras, são implementações que se opunham ao uso de CONTINUATION por risco de negação de serviço desde 10 anos atrás. Se você ler as longas threads, o ponto central sempre era como evitar o problemático CONTINUATION: https://lists.w3.org/Archives/Public/ietf-http-wg/2014JulSep...
      Teria sido mais robusto se ao menos a proposta de proibi-lo depois de um frame HEADERS que não estivesse cheio tivesse sido aceita, mas achou-se que isso poderia dificultar a própria tarefa de codificação. Por causa de problemas como fronteiras de bytes do compressor
      É engraçado ver as mesmas coisas sendo “redescobertas” a cada 10 anos. Recentemente foi o já conhecido flood de RESET_STREAM, agora é CONTINUATION e em breve devem aparecer frames DATA de comprimento 0, WINDOW_UPDATE de 1 byte, SETTINGS INITIAL_WINDOW que consomem muita CPU e coisas do tipo. Se for possível apenas dar um nome e, de preferência, um logotipo a um problema conhecido, o mundo continua girando nesse circo de segurança
    • E o Caddy? É um projeto excelente, merece uma linha própria ;)
  • Um post anterior do mesmo autor que organiza os servidores web/proxies reversos afetados
    https://nowotarski.info/http2-continuation-flood/

  • Este artigo ficou no topo o dia inteiro
    Fiquei curioso: para um site com pouco tráfego, talvez seja mais seguro simplesmente operar com HTTP/1.1?

    • HTTP/1.1 é muito mais fácil de implementar, então é razoável supor que tenha menos bugs
      HTTP/2 e HTTP/3 são bem diferentes em termos de funcionalidade. Com a adição de multiplexação, janelamento, HPACK etc., as conexões quase sem estado do HTTP/1.1 viraram conexões com estado. Para manter uma conexão com estado, é preciso armazenar dados como estado e configurações, e é daí que surgem problemas como estes
      No HTTP/2, a multiplexação é adicionada, então as características de defesa também mudam. Por exemplo, se a conexão vem de uma requisição de origem de um CDN, você pode permitir poucas conexões e manter um grande pool de canais multiplexados em cada uma; mas, se for acesso direto de usuários, talvez queira permitir muitas conexões, mas reduzir o número de canais multiplexados por conexão. No HTTP/1, quase tudo parecia parecido, então a defesa era muito mais simples
    • Fazer upgrade só por fazer upgrade não é uma boa prática de engenharia. Se o upgrade não traz benefício adicional, é difícil justificá-lo
    • Não necessariamente. Quem diz que HTTP/1.1 é simples nunca implementou um parser completo compatível com ambientes reais
      O HTTP/1 tem muitas condições de contorno pouco visíveis e comportamentos de exceção antigos. O formato textual é muito mais flexível do que parece quando se veem apenas cabeçalhos válidos, e também há recursos ambíguos como cabeçalhos de várias linhas, recursos MIME antigos, condições de corrida com 100-continue, cabeçalhos hop-by-hop personalizados e corpo em GET
      Felizmente, os RFCs HTTP mais novos documentam muitas armadilhas. Implementar olhando apenas o RFC 2616 não resulta em uma implementação segura
      O tamanho real de uma requisição ou resposta pode ser especificado simultaneamente de várias maneiras, e os valores podem entrar em conflito. Além disso, ele depende de várias combinações de recursos e de valores de cabeçalhos que exigem regras de parsing estranhas por compatibilidade retroativa, de modo que uma implementação HTTP “simples” pode ser enganada por request smuggling
      De qualquer forma, é preciso uma implementação madura, robusta e bem testada
    • Eu também fiquei curioso sobre isso. Por ser mais maduro e menos complexo, parece plausível que seja mais seguro
    • Provavelmente sim. HTTP/2 é bom para streaming, e mesmo isso já está sendo substituído por protocolos mais novos
      Para servir assets estáticos comuns, a única vantagem é que o HTTP/1 sofre com limite de conexões por domínio, então dá para carregar mais assets em paralelo. Usar CDNs em domínios diferentes normalmente também contorna esse problema
      Em tese, seria possível servir assets JavaScript não empacotados com HTTP/2, mas nunca vi isso em produção. Provavelmente porque, na maioria dos casos, ainda é necessária uma etapa de compilação
  • Se fizer isso devagar, dá para chamar de slowloris v2 :(

  • HTTP/2, ou como enfiar à força um “upgrade” da camada de transporte em um protocolo da camada de aplicação