Entrada

CL.0 request smuggling

Laboratorio de Portswigger sobre HTTP Request Smuggling

CL.0 request smuggling

Certificaciones

  • eWPT
  • eWPTXv2
  • OSWE
  • BSCP

Descripción

Este laboratorio es vulnerable a ataques de request smuggling CL.0. El servidor back-end ignora la cabecera Content-Length en las solicitudes dirigidas a algunos endpoints

Para resolver el laboratorio, tenemos que identificar un endpoint vulnerable, smugglear una solicitud al servidor back-end para acceder al panel de administración en /admin y, a continuación, eliminar al usuario carlos


Resolución

Al acceder a la web vemos esto

Si pulsamos sobre Admin panel y capturamos la petición con Burpsuite vemos que no podemos acceder

Capturamos la petición con Burpsuite, la enviamos al Repeater, eliminamos las cabeceras innecesarias, pulsamos sobre Show non-printable chars y en el apartado Request atributes del Inspector cambiamos el protocolo de HTTP/2 a HTTP/1. Una vez tengamos todo esto hecho, vamos a realizar la petición, si todo funciona bien significa que la petición se puede realizar con las cabeceras que estamos usando

Lo siguiente que debemos de hacer es pulsar sobre el engranaje y descheckear la opción Update Content-Length para que no se actualice el Content-Length

Ahora vamos a cambiar el método a POST, para ello hacemos click derecho > Change request method

Ahora vamos a proceder a testear si nos encontramos ante un TE.CL o ante un CL.TE. He añadido la cabecera Transfer-Encoding con el valor chunked, esto quiere decir que vamos a enviar los datos que se proporcionan en el body en este formato. También he añadido la cabecera Content-Length porque también es necesaria

Vamos a explicar la petición. El Content-Length debe indicar un tamaño superior al del body que realmente enviamos, por eso le ponemos 6, porque es un byte mayor que el tamaño del body, el cual es 5

Si estuviéramos ante un TE.CL, el frontend procesaría el Transfer-Encoding y cortaría el body chunked después del 0\r\n\r\n (antes de la x). El backend, usando Content-Length: 6, esperaría 6 bytes pero recibiría 5 solamente, lo que provocaría un timeout

Respecto a la letra x, se pone ahí para detectar si el servidor front-end ha interpretado Transfer-Encoding y ha cortado el body antes de esa x. Si el frontend no interpreta Transfer-Encoding, la x se reenviará al backend junto con el resto del body

En este caso al enviar la petición, vemos un error. Según el RFC 7230, si las cabecera Transfer-Encoding y Content-Length están presentes, la cabecera Transfer-Encoding tiene prioridad y Content-Length se ignora. Además de este caso, también puede ser que el servidor backend o frontend o ambos, rechazen la petición porque la interpretan como un intento de ataque de HTTP request smuggling al tener estas dos cabeceras en la petición. Aquí podemos leer más información acerca del RFC 7230 https://datatracker.ietf.org/doc/html/rfc7230

También podríamos intentar usar una inyección CRLF u ofuscar la cabecera Transfer-Encoding para crear una discrepancia pero en este caso ninguna de estas cosas funciona. Teniendo todo esto en cuenta, podemos descartar la explotación de un TE.TE, TE.CL y CL.TE

Aunque las técnicas anteriores no funcionen, todavía puede ser posible explotar un HTTP request smuggling si el servidor front-end realiza HTTP/2 downgrading de las solicitudes HTTP/2

Esta técnica es posible debido a que como HTTP/2 sigue siendo relativamente nuevo, los servidores web que lo admiten a menudo todavía tienen que comunicarse con infraestructuras back-end heredadas que solo utilizan HTTP/1. Como resultado, se ha convertido en una práctica habitual que los servidores front-end reescriban cada solicitud HTTP/2 entrante utilizando la sintaxis de HTTP/1, generando de forma efectiva su equivalente en HTTP/1. Esta solicitud downgradeada se reenvía posteriormente al servidor back-end correspondiente y cuando el servidor back-end que utiliza HTTP/1 emite una respuesta, el servidor front-end invierte este proceso para generar la respuesta HTTP/2 que devuelve al cliente

En HTTP/2 la cabecera Content-Length es opcional, es decir, si no la proporcionamos se calcula automáticamente el tamaño del body de la solicitud sin necesidad de usar la cabecera, sin embargo, durante el HTTP/2 downgrading los servidores front-end suelen añadir una cabecera Content-Length de HTTP/1, derivando su valor a partir del mecanismo integrado de longitud de HTTP/2

Para que el ataque tenga éxito necesitamos que el Content-Length que proporcionemos nosotros en la solicitud HTTP/2 llegue al servidor backend. Esto se debe a que aunque el servidor front-end utilizara la longitud implícita de HTTP/2 para determinar dónde termina la solicitud, el servidor back-end que utiliza HTTP/1 tendrá que basarse en la cabecera Content-Length derivada de la que inyectamos, lo que provocará una desincronización

Antes de seguir, vamos a capturar una solicitud por POST para verificar lo que hemos dicho anteriormente de la cabecera Content-Length cuando se usa HTTP/2. Para ello, publicamos un comentario en cualquier publicación y capturamos la petición

Si desactivamos la opción Update Content-Length y bajamos el Content-Length a 20 vemos que solo se deberían de enviar esos 20 bytes del body

Sin embargo, si miramos el código fuente vemos que en este caso vemos que se está ignorando el valor que proporcionamos nosotros a través de la cabecera Content-Length

Si quitamos la cabecera Content-Length, sigue funcionando como al inicio porque estamos usando HTTP/2. En esta solicitud podemos ver como hemos podido enviar una petición mediante HTTP/2 sin proporcionar la cabecera Content-Length. Para que esto funcione debemos de tener descheckeada la opción Update content-length

Sin embargo, si cambiamos a HTTP/1 vemos que ni siquiera se envía el mensaje porque no hemos especificado el tamaño del body mediante la cabecera Content-Length

Una vez aclarado esto, vamos a empezar a testear. Lo primero que tenemos que hacer es pulsar sobre el engranaje y checkear la opción Allow HTTP/2 ALPN override para enviar solicitudes HTTP/2 incluso cuando el servidor no anuncie compatibilidad con HTTP/2 mediante ALPN. Esto nos permite comprobar si existe compatibilidad oculta con HTTP/2. Aunque en este caso no es necesario habilitar esta opción, porque ya vemos que sí que hay compatibilidad con HTTP/2, es buena práctica seguir siempre la misma metodología

Luego, en el apartado Request atributes del Inspector cambiamos el protocolo de HTTP/1 a HTTP/2

Una vez tenemos estas opciones configuradas, vamos a crear una petición para verificar si front-end realiza HTTP/2 downgrading de las solicitudes HTTP/2. Existen dos variaciones de esta técnica, H2.TE y H2.CL, en este caso vamos a probar con H2.TE porque anteriormente hemos visto que el valor del Content-Length que hemos proporcionado cuando hemos hecho la solicitud HTTP/2 ha sido ignorado

Para ello tenemos que construir esta solicitud

Ahora vamos a explicar la solicitud, la cabecera Transfer-Encoding: chunked es la que le dice al servidor frontend que usa HTTP/2 que va a recibir los datos que se proporcionan en el body en este formato. En este caso con el 0 le decimos que ese es el final del body y como no hemos proporcionado nada en el body pues no se envía nada. Si quisiéramos enviar datos debemos de especificar el tamaño del body en hexadecimal y luego indicar el final del body con un 0. Por ejemplo:

1
2
3
c
smuggled=yes
0

Una vez la solicitud llega el servidor backend, como usa HTTP/1.1 pues ocurre lo mismo, interpreta que el body está vacío. Ojo, esto es siempre y cuando el servidor backend interprete la cabecera Transfer-Encoding

Vamos a proceder a enviar la petición dos veces, esto es lo que vemos después de enviar la primera solicitud

Y esto es lo que vemos después de enviar la segunda solicitud. Como vemos, hemos obtenido la misma respuesta en ambas solicitudes, por lo tanto, algo debe estar pasando

Cuando nosotros hacemos la segunda petición o cuando algún usuario accede a la webla petición que se realiza es esta

1
2
3
4
5
GET /404 HTTP/1.1\r\n
Foo: xGET / HTTP/1.1\r\n                                          ← absorbe su request line
Host: 0a440078035cb3a881b5533600370030.web-security-academy.net\r\n ← absorbe su Host
Cookie: session=abc123\r\n                                         ← absorbe sus cookies
\r\n                                                               ← cierra las cabeceras

Como vemos, al no añadir \r\n\r\n al final de nuestra petición smuggleada, la request line de la víctima se absorbe como parte del valor de la cabecera Foo y el backend usa nuestra request line (GET /404) en su lugar

Esto en este caso puede que no hayamos podido verificar si la web es vulnerable a un HTTP request smuggling H2.TE porque algunos sitios web toman medidas para evitar ataques básicos H2.CL o H2.TE, como validar la cabecera content-length o eliminar cualquier cabecera transfer-encoding. Sin embargo, el formato binario de HTTP/2 permite nuevas formas de eludir este tipo de medidas implementadas en el servidor front-end

En HTTP/1, en ocasiones podemos explotar discrepancias entre la forma en que los servidores manejan los caracteres de nueva línea independientes (\n) para introducir cabeceras prohibidas mediante request smuggling. Si el servidor back-end lo interpreta como un delimitador pero el servidor front-end no, podría ser que algunos servidores front-end no detecten en absoluto la segunda cabecera

1
Foo: bar\nTransfer-Encoding: chunked

Esta discrepancia no existe con el manejo de una secuencia CRLF (\r\n) completa, porque todos los servidores HTTP/1 coinciden en que esta termina la cabecera

Por otro lado, como los mensajes HTTP/2 son binarios en lugar de estar basados en texto, los límites de cada cabecera se basan en desplazamientos explícitos y predeterminados, en lugar de caracteres delimitadores. Esto significa que \r\n deja de tener un significado especial dentro del valor de una cabecera y, por tanto, puede incluirse dentro del propio valor sin provocar que la cabecera se divida. Por ejemplo:

1
foo	bar\r\nTransfer-Encoding: chunked

Esto puede parecer relativamente inofensivo por sí solo, pero cuando se reescribe como una solicitud HTTP/1, el \r\n volverá a interpretarse como un delimitador de cabeceras. Como resultado, un servidor back-end que utiliza HTTP/1 verá dos cabeceras distintas:

1
2
Foo: bar
Transfer-Encoding: chunked

Una vez sabemos esto, vamos a añadir una nueva cabecera debajo de Content-Type inyectando los caracteres CRLF

Añadimos esta nueva cabecera, para añadir el CRLF aquí tenemos que pulsar Shift + Enter

Una vez hecho esto, enviamos nuevamente dos peticiones. En este caso no funciona esta inyección CRLF

Aunque esto no ha funcionado, todavía podemos intentar hacer un HTTP/2 request splitting y luego llevar a cabo un response queue poisoning. Cuando vimos el response queue poisoning, aprendimos cómo dividir una única solicitud HTTP en exactamente dos solicitudes completas en el back-end

En el ejemplo que vimos, la división se producía dentro del cuerpo del mensaje, pero cuando entra en juego el downgrade de HTTP/2 a HTTP/1, también podemos hacer que esta división se produzca en las cabeceras

Este enfoque es más versátil porque no dependemos de utilizar métodos de solicitud a los que se les permite contener un body. Por ejemplo, incluso podemos utilizar una solicitud GET:

1
2
3
4
5
6
7
8
:method      GET
:path        /
:authority   vulnerable-website.com
foo
bar\r\n
\r\n
GET /admin HTTP/1.1\r\n
Host: vulnerable-website.com

Para dividir una solicitud en las cabeceras, necesitamos comprender cómo el servidor front-end reescribe la solicitud y tener esto en cuenta al añadir manualmente cualquier cabecera HTTP/1. De lo contrario, una de las solicitudes podría carecer de cabeceras obligatorias. Para entender esto mejor, es recomendable leerse este artículo https://justice-reaper.github.io/posts/HTTP-Request-Smuggling-Lab-8/

Una vez sabemos todo esto, vamos a crear esta solicitud

Una vez creada, vamos a enviar peticiones hasta que veamos algún cambio en la respuesta. Si vemos un cambio en la respuesta que no corresponde a nuestra petición, esto significa que el ataque ha funcionado y si no vemos ningún cambio después de enviar unas 10 peticiones lo más seguro es que no haya funcionado el ataque

Esta es la primera petición y vemos que tampoco podemos hacer este ataque

Lo siguiente que podemos testear es un CL.0. En algunos casos, se puede conseguir que los servidores ignoren la cabecera Content-Length, lo que significa que asumen que cada solicitud termina al final de las cabeceras. Esto es, en la práctica, equivalente a tratar el valor de Content-Length como 0

Si el servidor back-end presenta este comportamiento, pero el servidor front-end sigue utilizando la cabecera Content-Length para determinar dónde termina la solicitud, podemos aprovechar potencialmente esta discrepancia para realizar un HTTP request smuggling. Esto es lo que se conoce como HTTP request smuggling CL.0

Para comprobar si existe esta vulnerabilidad, primero debemos enviar una solicitud que contenga otra solicitud parcial en su body y a continuación, enviar una segunda solicitud normal. Después, debemos comprobar si la respuesta a esta segunda solicitud se ha visto afectada por la solicitud parcial introducida mediante request smuggling

Es importante destacar que para este ataque no es necesario manipular las cabeceras de ninguna manera, ya que la longitud de la solicitud está especificada mediante una cabecera Content-Length completamente normal y correcta

Para testear esto nosotros mismo usando Repeater tenemos que:

  • Crear una pestaña que contenga la solicitud de preparación y otra que contenga una solicitud normal cualquiera

  • Añadir ambas pestañas a un grupo en el orden correcto

  • Utilizar el menú desplegable situado junto al botón Send, cambiar el modo de envío a Send group in sequence (single connection)

  • Cambiar la cabecera Connection a keep-alive

  • Envíar la secuencia y comprobar las respuestas

En la práctica, este comportamiento se da principalmente en endpoints que simplemente no esperan recibir solicitudes POST, por lo que asumen implícitamente que las solicitudes no tienen cuerpo. Los endpoints que desencadenan redirecciones a nivel de servidor y las solicitudes de archivos estáticos son candidatos especialmente adecuados

Una vez sabemos esto, lo primero que vamos a hacer es añadir el dominio al scope pulsando en Target > Scope > Add

Una vez hecho esto, nos vamos a Target > Site map > Scan

Seleccionamos la opción Crawl y pulsamos sobre Scan

En mi caso voy a usar el recurso estático correspondiente a la imagen de perfil de los usuarios que comentan

En el Repeater tenemos que hacer un grupo con dos solicitudes, esta solicitud tiene que ser la primera y como segunda solicitud vamos a usar una normal que se hace a la raíz de la web

Esta sería la primera solicitud

Y esta sería la segunda solicitud

Una vez hecho esto, creamos un grupo y metemos ambas solicitudes dentro, para ello, lo primero que tenemos que hacer es pulsar en el símbolo + que está a la derecha de estas dos tab y luego pulsar en New tab group

Como modo de envío vamos a usar Send group in sequence (single connection)

Una vez hecho esto vamos a configurar ambas peticiones, la primera petición tiene que tener esta estructura y usar HTTP/1.1 porque si usáramos HTTP/2 ya no sería un CL.0, si no que sería un H2.0

Respecto a las opciones de la primera petición, las dejamos por defecto

Una vez configurada la primera petición, vamos a configurar la segunda petición. En esta petición solamente tenemos que poner que se haga uso del protocolo HTTP/1.1 en vez de HTTP/2

Una vez tenemos todo configurado, vamos a pulsar sobre Send group (single connection). Para ver la respuesta tenemos que hacerlo en la pestaña de la segunda petición. Como vemos, el ataque ha funcionado, así que podemos confirmar la existencia de un HTTP request smuggling CL.0

En caso de que no hubiera funcionado, tendríamos que haber probado con otros recursos estáticos o con endpoints que desencadenaran una redirección. Anteriormente hemos visto que se nos bloquea el acceso al Admin panel, sin embargo, puede ser que ahora a través del HTTP request smuggling CL.0 podamos bypassear algún control de seguridad que se hace en el servidor frontend y acceder a ese endpoint

Para hacer esto, tenemos que modificar solicitud smuggleada para que haga una petición a /admin

Una vez hecho esto, pulsamos en Send group (single connection)

Si observamos el código de la respuesta vemos que para eliminar al usuario carlos hay que hacer una petición a /admin/delete?username=carlos

Una vez sabemos esto, modificamos nuevamente la primera petición para que se realice a /admin/delete?username=carlos

Vemos que ha funcionado y por lo tanto, ya hemos completado el laboratorio

Esta entrada está licenciada bajo CC BY 4.0 por el autor.