3 puntos por GN⁺ 2024-04-06 | 1 comentarios | Compartir por WhatsApp
  • HTTP/2 CONTINUATION Flood es una familia de vulnerabilidades en implementaciones de HTTP/2 que puede derribar la disponibilidad de un servidor al seguir enviando frames de encabezados sin END_HEADERS
  • Como la solicitud atacante no se completa, no queda en los logs de acceso HTTP, y para identificar la causa puede ser necesario analizar los bytes del tráfico sin procesar
  • Según la implementación, el alcance del impacto varía entre agotamiento de CPU, OOM con múltiples conexiones, OOM con una sola conexión e incluso crasheos por bugs en el momento de cierre de la conexión
  • En los casos de Go, Firefox y Node.js se observaron, respectivamente, decodificación HPACK continua, ausencia de límite al tamaño de encabezados de respuesta y un conflicto entre el cierre de la conexión y la actualización de contadores de memoria durante el manejo de CONTINUATION
  • A diferencia de Rapid Reset, en muchas implementaciones era posible crashear el servidor con una sola conexión TCP, por lo que una amplia variedad de servicios de Internet que usan HTTP/2 pudo verse afectada

Cómo se usa el frame CONTINUATION en HTTP/2

  • HTTP/2 no intercambia líneas de texto como HTTP/1.1, sino que es un protocolo que intercambia frames binarios
  • El frame HEADERS envía los encabezados HTTP de solicitudes y respuestas, y los datos de encabezados se almacenan en un field block fragment codificado con HPACK
  • El frame HEADERS tiene flags que indican el final de los encabezados y del stream
    • END_HEADERS: señal de que ese frame contiene todos los encabezados que se quieren enviar
    • END_STREAM: señal de que ya no hay cuerpo de solicitud o respuesta
  • Los frames tienen un tamaño máximo definido al inicio de la comunicación; si un frame recibido supera el tamaño permitido, la conexión se corta por error de protocolo
  • Si no es posible incluir todos los encabezados en un único frame HEADERS, a un HEADERS sin END_HEADERS le siguen frames CONTINUATION
    • Frame HEADERS sin END_HEADERS
    • Frames CONTINUATION adicionales sin END_HEADERS
    • END_HEADERS configurado en el último frame CONTINUATION
  • Después del último frame de encabezados, llega un frame DATA con los datos de la solicitud o termina el stream HTTP/2

Núcleo de la vulnerabilidad: un stream de encabezados que nunca termina

  • Si un cliente inicia un nuevo stream HTTP/2 y luego envía frames HEADERS y CONTINUATION sin configurar nunca END_HEADERS, el servidor intenta seguir parseando y almacenando un stream infinito de encabezados
  • Los servidores HTTP/1.1 generalmente cuentan con dos mecanismos para evitar encabezados infinitos
    • Un límite de tamaño de encabezados que corta la conexión si la lista de encabezados supera el tamaño permitido
    • Un timeout de solicitud/encabezados que corta la conexión si la solicitud o los encabezados no se transmiten a tiempo
  • Varias implementaciones de HTTP/2 no tenían estas protecciones o las habían implementado mal, incluidos Apache httpd, Envoy y varios paquetes y códecs de HTTP/2
  • Los resultados según la implementación se dividen en cuatro categorías
    • Agotamiento de CPU: al leer y decodificar encabezados adicionales, aumenta el uso de CPU y las respuestas a otras solicitudes se vuelven lentas o se bloquean
    • OOM basado en múltiples conexiones: los encabezados CONTINUATION se almacenan en memoria y, aunque existe un límite de tamaño para la lista de encabezados, al no haber timeout de encabezados cada conexión sigue ocupando memoria
    • OOM basado en una sola conexión: algunas implementaciones siguen leyendo encabezados hasta llenar la memoria, lo que hace que el SO termine el proceso
    • Crasheo con apenas unos frames: cuando la conexión se corta en medio de un stream CONTINUATION, un bug de implementación hace crashear el servidor
  • Si no hay END_HEADERS, la solicitud no se cierra correctamente, por lo que la solicitud del cliente malicioso no se guarda en los logs de acceso

Caso Go: agotamiento de CPU porque la decodificación HPACK no se detiene

  • Go es un caso destacado de agotamiento de CPU en CONTINUATION Flood
  • La implementación de Go agrupa un frame HEADERS, cero o más frames CONTINUATION y el decodificador HPACK mediante la abstracción http2MetaHeadersFrame
  • Cuando readMetaFrame alcanza el límite de tamaño de encabezados o se produce un error, llama a SetEmitEnabled(false) para dejar de emitir los encabezados decodificados
  • Sin embargo, incluso después de detener la emisión de encabezados, el decodificador HPACK sigue decodificando los bytes de entrada
  • El bucle que suministra frames solo se detiene cuando HeadersEnded() pasa a ser true, lo que ocurre cuando se configura el flag END_HEADERS
  • Si el atacante no envía END_HEADERS, readMetaFrame no retorna y el decodificador HPACK sigue procesando nuevos bytes mientras el atacante los envía

Casos OOM e impacto en el cliente Firefox

  • El OOM ocurre en implementaciones que no limitaban el tamaño de la lista de encabezados creada por frames CONTINUATION
  • En implementaciones sin timeout de encabezados, era posible crashear un servidor con una sola conexión HTTP/2
  • Incluso en implementaciones con idle timeout, era posible hacer que varias conexiones HTTP/2 ocuparan una cantidad de RAM cercana al límite por conexión y luego mantenerlas vivas enviando el último frame CONTINUATION byte por byte cada pocos segundos
  • CONTINUATION Flood puede producirse no solo en servidores, sino también del lado del cliente, como navegadores
  • El commit de corrección de Mozilla Firefox agregó una verificación que devuelve un error de sesión PROTOCOL_ERROR si la suma del tamaño agregado de encabezados y el tamaño del nuevo frame supera network_http_max_response_header_size()

Caso Node.js: crasheo por assertion durante el cierre de la conexión

  • Node.js manejaba correctamente el stream infinito de frames CONTINUATION en sí, pero se producía una data race cuando la conexión se cortaba durante el stream de encabezados
  • Durante la ejecución del código de ataque, Node.js crasheaba por una falla de assertion CHECK_EQ(current_nghttp2_memory_, 0) en Http2Session::~Http2Session()
  • El crasheo estaba relacionado con el momento exacto en que el cliente HTTP/2 cortaba la conexión con el servidor Node.js, y la assertion estaba dentro del destructor de Http2Session
  • Node.js incorpora la biblioteca nghttp2 para manejar conexiones HTTP/2
  • current_nghttp2_memory_ rastrea la memoria asignada internamente por nghttp2 y, después de session_.reset() en el destructor, verifica que todos los artefactos de nghttp2 se hayan eliminado de la memoria
  • La investigación mostró que había casos en los que callbacks de nghttp2 y reset() se ejecutaban juntos durante el parseo de frames CONTINUATION
    • Llega un frame CONTINUATION en el estado NGHTTP2_IB_EXPECT_CONTINUATION
    • El estado cambia a NGHTTP2_IB_READ_HEADER_BLOCK
    • Sigue el flujo session_after_header_block_received, session_call_on_frame_received, on_frame_recv_callback
    • OnFrameReceive y HandleHeadersFrame de Node.js actualizan el contador de memoria
  • Si HandleHeadersFrame y Http2Session::~Http2Session() se ejecutan al mismo tiempo, current_session_memory_ se actualiza simultáneamente, el valor de current_nghttp2_memory_ se vuelve negativo y CHECK_EQ falla

Diferencias con las vulnerabilidades de HTTP/2 de 2019

  • El conjunto de vulnerabilidades de HTTP/2 reportado por Netflix y Google en 2019 está resumido en la CERT/CC Vulnerability Note VU#605641
  • CVE-2019-9516, “0-Length Headers Leak”, es un problema en el que algunas implementaciones asignan memoria para nombres y valores de encabezado de longitud 0 y la mantienen hasta que termina la sesión
  • CONTINUATION Flood no usa encabezados vacíos, sino que envía muchos encabezados aleatorios hasta el límite de tamaño de frame configurado por el servidor
  • CVE-2019-9518, “Empty Frame Flooding”, consiste en enviar frames DATA, HEADERS, CONTINUATION, PUSH_PROMISE, etc. con payload vacío y sin end-of-stream, haciendo que el par invierta un tiempo de procesamiento desproporcionado respecto del ancho de banda del ataque
  • CONTINUATION Flood no usa frames vacíos, sino frames lo más grandes posible para ocupar memoria y consumir ciclos de CPU durante el proceso de decodificación

Por qué pudo haber sido más grave que Rapid Reset

  • En octubre de 2023 se publicaron detalles de “Rapid Reset”, un zero-day del protocolo HTTP/2, y se lo describió como “el mayor ataque DDoS hasta la fecha”
  • Rapid Reset usa una combinación de frames HEADERS con END_STREAM y END_HEADERS configurados, y frames RST_STREAM
  • En este método, mitigaciones estándar como rate limiting pueden reducir el daño, y los administradores del servidor pueden ver muchas solicitudes entrantes en los logs y recibir alertas
  • En CONTINUATION Flood no hay END_HEADERS, por lo que no se completa ni una sola solicitud, y los administradores no pueden ver las solicitudes en los logs
  • En muchas implementaciones, CONTINUATION Flood podía crashear el servidor con una sola conexión TCP, y en algunos casos bastaban muy pocos datos
  • Rapid Reset se usó en ataques DDoS y, en la mayoría de los casos, un ataque exitoso requería una botnet

Impacto que pudo haber tenido en servicios de Internet

  • Según Cloudflare Radar, el tráfico HTTP/2 representa alrededor del 60% del tráfico HTTP humano, excluyendo bots
  • Cloudflare Radar estima que el tráfico HTTP representa más del 70% de toda la transmisión de Internet
  • Considerando la importancia de los proyectos afectados y la facilidad de explotación, una gran parte de Internet estuvo expuesta a esta vulnerabilidad
  • HTTP se usa no solo en sitios web, sino también en muchas API RESTful
  • Los problemas de disponibilidad en API y sitios web importantes de empresas y gobiernos pueden generar pérdidas o caos por millones de dólares
  • Si se hubiera explotado, habría podido ser muy difícil de depurar para administradores de servidores sin conocimientos de HTTP/2
    • La solicitud HTTP maliciosa no se cierra correctamente
    • La solicitud no aparece en los logs de acceso del servidor
    • La mayoría de los servidores HTTP/2 carecen de funciones avanzadas de análisis de frames
    • Es necesario analizar manualmente los datos sin procesar de la conexión

Divulgación coordinada y respuesta de CERT/CC

  • Esta familia de vulnerabilidades representaba un riesgo considerable para la seguridad de Internet
  • Después del reporte de enero de 2024, CERT/CC abrió un caso de Vulnerability Coordination para hacer seguimiento del problema
  • Varias grandes empresas tecnológicas y proyectos open source participaron en el proceso de divulgación responsable relacionado
  • Como es difícil que un solo investigador revise todas las implementaciones, los problemas que afectan a múltiples proveedores requieren Vulnerability Coordination
  • CERT/CC publicó una Vulnerability Note sobre este problema, y solo se publican unas pocas notas de este tipo cada año

1 comentarios

 
GN⁺ 2024-04-06
Opiniones de Hacker News
  • El mes pasado mitigamos exactamente este problema en Bandit
    https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
    Desde el punto de vista de quien implementa, honestamente es algo demasiado obvio que hay que bloquear. Lo teníamos en mente desde hace mucho y asumíamos que otras implementaciones también se estarían defendiendo

    • Ya sabemos qué pasa cuando asumimos. Termina convirtiéndonos a ti y a mí en titulares de portada
  • En los últimos meses revisé decenas de implementaciones y, curiosamente, incluso los principales servidores HTTP/2 no tenían esta protección o la tenían mal implementada
    En esencia, lo veo como resultado de una cultura de desarrollo acostumbrada a escalar todo de forma dinámica y automática, sin preocuparse por qué tan grande puede llegar a ser
    Este tipo de problema no se limita a HTTP/2, pero es muy probable que la enorme complejidad de HTTP/2 haya contribuido. En la época de HTTP/1.x había más desarrolladores acostumbrados a lenguajes como C, que prestaban atención constante al manejo de longitudes de búfer, y no habrían dejado que algo creciera indefinidamente cuando, para toda una solicitud, unas cuantas KB de asignación de encabezados bastaban de sobra

    • La gente sigue enfocándose y optimizando solo el camino normal, pero no se detiene a pensar qué pasa si un atacante provoca deliberadamente el peor caso una y otra vez
      Muchos ataques de denegación de servicio, como slowloris o las colisiones de hash en parámetros de consulta, se volvieron reales porque el uso acotado de recursos se consideró demasiado tarde
  • > No afectados: Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]
    0: https://nowotarski.info/http2-continuation-flood/

    • En otras palabras, son implementaciones que desde hace 10 años se oponían al uso de CONTINUATION por el riesgo de denegación de servicio. Si lees los hilos largos, el punto central siempre era cómo evitar las problemáticas CONTINUATION: https://lists.w3.org/Archives/Public/ietf-http-wg/2014JulSep...
      Habría sido más robusto si al menos se hubiera aceptado la propuesta de prohibirlas después de un frame HEADERS que no estuviera lleno, pero se consideró que eso podía volver más difícil el trabajo de codificación. Por temas como los límites de bytes del compresor
      Da risa ver cómo cada 10 años se “redescubren” las mismas cosas. Hace poco fue el conocido flood de RESET_STREAM, ahora es CONTINUATION, y pronto quizá vengan los frames DATA de longitud 0, WINDOW_UPDATE de 1 byte o SETTINGS de INITIAL_WINDOW que hagan consumir mucho CPU. Si a un problema conocido se le puede poner un nombre y, si es posible, un logo, el mundo seguirá girando este circo de seguridad
    • ¿Y Caddy qué tal? Es un gran proyecto, merece una línea aparte ;)
  • Un artículo anterior del mismo autor que resume los servidores web/proxies inversos afectados
    https://nowotarski.info/http2-continuation-flood/

  • Este artículo estuvo todo el día en la parte más alta
    Me da curiosidad: si un sitio web tiene poco tráfico, ¿tal vez sería más seguro simplemente operarlo con HTTP/1.1?

    • HTTP/1.1 es mucho más fácil de implementar, así que es razonable pensar que tendría menos bugs
      HTTP/2 y HTTP/3 son muy distintos en funcionalidades. Con la multiplexación, el windowing, HPACK, etc., la conexión casi sin estado de HTTP/1.1 se convierte en una conexión con estado. Para mantener una conexión con estado hay que guardar datos como el estado y la configuración, y de ahí surgen estos problemas
      En HTTP/2 se agrega multiplexación, así que también cambian las características defensivas. Por ejemplo, si la conexión viene de solicitudes de origen de un CDN, podrías permitir pocas conexiones y un gran pool de canales multiplexados por conexión; pero si es acceso directo de usuarios, quizá quieras permitir muchas conexiones y reducir la cantidad de canales multiplexados por conexión. En HTTP/1 casi todo se veía parecido, por lo que la defensa era mucho más simple
    • Actualizar solo por actualizar no es una buena práctica de ingeniería. Si la actualización no aporta beneficios adicionales, es difícil justificarla
    • No necesariamente. Quienes dicen que HTTP/1.1 es simple nunca han implementado un parser completo compatible con entornos reales
      HTTP/1 tiene muchas condiciones de borde poco visibles y comportamientos heredados de excepción. El formato de texto es mucho más flexible de lo que parece cuando solo ves encabezados válidos, y también hay funciones ambiguas como encabezados multilínea, funciones MIME antiguas, condiciones de carrera con 100-continue, encabezados hop-by-hop personalizados y cuerpos en GET
      Por suerte, los nuevos RFC de HTTP documentan muchas trampas. Si implementas basándote solo en RFC 2616, no obtendrás una implementación segura
      El tamaño real de una solicitud o respuesta puede especificarse simultáneamente de varias maneras, con valores que incluso pueden entrar en conflicto. Además, depende de distintas combinaciones de funcionalidades y de valores de encabezado que requieren reglas de parsing extrañas por compatibilidad hacia atrás, de modo que una implementación “simple” de HTTP puede ser engañada con request smuggling
      En cualquier caso se necesita una implementación madura, robusta y bien probada
    • Yo también me preguntaba eso. Al ser más maduro y menos complejo, parece posible que sea más seguro
    • Probablemente sí. HTTP/2 es bueno para streaming, e incluso eso está siendo reemplazado por protocolos más nuevos
      Para servir recursos estáticos comunes, la única ventaja de HTTP/1 es que, como tiene límites de conexiones por dominio, permite cargar más recursos en paralelo. Si usas CDNs en dominios distintos, normalmente también puedes evitar ese problema
      En teoría podrías servir recursos JavaScript sin empaquetar con HTTP/2, pero no lo he visto en entornos reales de producción. Probablemente porque en la mayoría de los casos sigue siendo necesario un paso de compilación
  • Si haces esto lentamente, podrías llamarlo slowloris v2 :(

  • HTTP/2, o cómo meter a la fuerza una “actualización” de la capa de transporte en un protocolo de capa de aplicación