
HTTP3ONSTEROIDS - Une recherche sur CVE-2023-25950 où l'implémentation HTTP/3 de HAProxy ne parvient pas à bloquer un nom de champ d'en-tête HTTP malformé.

L'implémentation HTTP/3 de HAProxy ne parvient pas à bloquer un nom de champ d'en-tête HTTP malformé et, lorsqu'elle est déployée devant un serveur qui traite incorrectement cet en-tête malformé, elle peut être utilisée pour mener une attaque de contrebande de requêtes/réponses HTTP (HTTP request/response smuggling). Un attaquant distant peut modifier la requête d'un utilisateur légitime. Par conséquent, l'attaquant peut obtenir des informations sensibles ou provoquer une condition de déni de service (DoS).
https://jvn.jp/en/jp/JVN38170084/
Une très bonne approche avant de commencer toute recherche sur les CVE consiste à lire d'abord la description de la vulnérabilité, puis à vérifier si le ou les commits de correctif sont disponibles.
Dans mon cas, le commit était disponible, et il est évident que les développeurs de HAProxy ont oublié d'inclure les vérifications de la RFC 9114 4.1.2. Malformed Requests and Responses lors de l'analyse des en-têtes standard dans l'implémentation HTTP3.
Reportez-vous au commit de correctif ci-dessous.
--- 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;
Dépôts - haproxy-2.7.git/commit
Ci-dessous, vous pouvez trouver le code qui démontre l'absence d'assainissement des noms d'en-tête dans l'implémentation du traitement des en-têtes standard sur 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;
}
Ci-dessous, vous pouvez trouver le code utilisé pour vérifier si le nom d'en-tête est valide.
Le code commence par une boucle for qui parcourt chaque caractère d'un nom de champ d'en-tête. La boucle s'exécute de i = 0 à i < list[hdr_idx0.n.len], où list est un tableau ou une structure contenant les informations d'en-tête, et hdr_idx est un index représentant l'en-tête spécifique en cours de vérification.
À l'intérieur de la boucle, le code extrait le caractère actuel c du nom du champ d'en-tête.
list[hdr_idx].n.ptr[i] accède au caractère à la position i dans le nom du champ d'en-tête.
La partie suivante du code contient une instruction if. Elle vérifie si le caractère actuel c satisfait l'une des deux conditions :
(uint8_t)(c - 'A') < 'Z' - 'A' : Ceci vérifie si le caractère est une lettre majuscule (A à Z) en soustrayant 'A' de c et en castant le résultat en uint8_t. Si le résultat est inférieur à la différence entre 'Z' et 'A', alors le caractère est une lettre majuscule.!HTTP_IS_TOKEN(c) : Cette condition vérifie si le caractère est un caractère de jeton (token) HTTP valide. La macro HTTP_IS_TOKEN fonctionne en vérifiant d'abord si le nom d'en-tête contient des jetons. Un jeton est une séquence de caractères qui n'est pas un caractère réservé dans le protocole HTTP. Les caractères réservés sont des caractères ayant une signification spéciale dans le protocole HTTP, par exemple :, /, ?, et #./*
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;
}
}
L'ensemble du laboratoire fonctionne sous Docker. Vous pouvez lancer le laboratoire avec les commandes suivantes :
cd /labdocker-compose up --buildVeuillez noter que la compilation Docker prendra 15-20 minutes.
Toutefois, avant de lancer le laboratoire, vous devez effectuer quelques modifications de configuration :
/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
Vous pouvez choisir entre la version vulnérable et la version corrigée en modifiant la valeur de l'argument à 'vuln' ou 'patched' pour effectuer votre test.
...
args:
- haproxy_version=patched || vuln
...
et enfin, importez le certificat minica.crt dans votre navigateur.
⚠️ Veuillez exécuter le laboratoire dans un environnement Linux.
Envoyez la requête curl suivante :
curl --http3 -H "foooooo\r\n: barr" -iL -k https://192.168.1.104/Réponse de la version vulnérable :
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
Réponse de la version corrigée :
curl: (56) HTTP/3 stream 0 reset by server
Sur la base des constatations ci-dessus, les réponses correspondantes indiquent que la version vulnérable de HAProxy a autorisé le préfixe \r\n à passer jusqu'au serveur backend, tandis que la version corrigée a interrompu la connexion entre le client et HAProxy.
Un attaquant peut mener une attaque de contrebande de requêtes HTTP (HTTP Request Smuggling) en fonction du comportement du backend et de la manière dont le serveur backend traitera l'en-tête malformé. À mon avis, la préoccupation la plus importante est qu'un attaquant pourrait exploiter la CVE susmentionnée pour mener une attaque par déni de service (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