
HTTP3ONSTEROIDS - A research on CVE-2023-25950 where HAProxy's HTTP/3 implementation fails to block a malformed HTTP header field name.

Die HTTP/3-Implementierung von HAProxy blockiert einen fehlerhaften HTTP-Header-Feldnamen nicht, und wenn sie vor einem Server eingesetzt wird, der diesen fehlerhaften Header falsch verarbeitet, kann sie für einen HTTP-Request/Response-Smuggling-Angriff verwendet werden. Ein entfernter Angreifer kann eine legitime Benutzeranfrage verändern. Infolgedessen kann der Angreifer vertrauliche Informationen erlangen oder einen Denial-of-Service (DoS)-Zustand verursachen.
https://jvn.jp/en/jp/JVN38170084/
Ein sehr guter Ansatz vor Beginn einer Forschung zu CVEs ist, die Beschreibung der Sicherheitslücke zu lesen und dann zu prüfen, ob der oder die Commits für den Patch verfügbar sind.
In meinem Fall war der Commit verfügbar, und es ist offensichtlich, dass die Entwickler von HAProxy vergessen haben, die Prüfungen aus RFC 9114 4.1.2. Fehlerhafte Anfragen und Antworten beim Parsen der Standard-Header in der HTTP3-Implementierung zu berücksichtigen.
Siehe den folgenden Patch-Commit.
--- 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;
Repositories - haproxy-2.7.git/commit
Unten finden Sie den Code, der das Fehlen der Bereinigung von Header-Namen in der Implementierung zur Behandlung von Standard-Headern in HAProxy 2.7.0 zeigt.
/*
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;
}
Unten finden Sie den Code, der verwendet wird, um zu prüfen, ob der Header-Name gültig ist.
Der Code beginnt mit einer for-Schleife, die jedes Zeichen eines Header-Feldnamens durchläuft. Die Schleife läuft von i = 0 bis i < list[hdr_idx0.n.len], wobei list ein Array oder eine Struktur ist, die Header-Informationen enthält, und hdr_idx ein Index, der den überprüften bestimmten Header darstellt.
Innerhalb der Schleife extrahiert der Code das aktuelle Zeichen c aus dem Header-Feldnamen. list[hdr_idx].n.ptr[i] greift auf das Zeichen an der i-ten Position im Header-Feldnamen zu. Der nächste Teil des Codes enthält eine if-Anweisung. Sie prüft, ob das aktuelle Zeichen c eine der beiden Bedingungen erfüllt:
(uint8_t)(c - 'A') < 'Z' - 'A': Dies prüft, ob das Zeichen ein Großbuchstabe (A bis Z) ist, indem 'A' von c subtrahiert und das Ergebnis in uint8_t umgewandelt wird. Wenn das Ergebnis kleiner als die Differenz zwischen 'Z' und 'A' ist, ist das Zeichen ein Großbuchstabe.!HTTP_IS_TOKEN(c): Diese Bedingung prüft, ob das Zeichen ein gültiges HTTP-Token-Zeichen ist. HTTP_IS_TOKEN funktioniert, indem zuerst geprüft wird, ob der Header-Name Token enthält. Ein Token ist eine Zeichenfolge, die kein reserviertes Zeichen im HTTP-Protokoll ist. Reservierte Zeichen sind Zeichen mit besonderer Bedeutung im HTTP-Protokoll, z.B. :, /, ? und #./*
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;
}
}
Das gesamte Lab läuft auf Docker. Sie können das Lab mit den folgenden Befehlen ausführen:
cd /labdocker-compose up --buildBitte beachten Sie, dass der Docker-Build 15–20 Minuten dauert.
Bevor Sie das Lab ausführen, müssen Sie jedoch einige Konfigurationsänderungen vornehmen:
/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
Sie können zwischen der anfälligen Version und der gepatchten Version wählen, indem Sie den Wert des Arguments auf 'vuln' oder 'patched' ändern, um Ihren Test darauf durchzuführen.
...
args:
- haproxy_version=patched || vuln
...
und importieren Sie schließlich das minica.crt-Zertifikat in Ihren Browser.
⚠️ Bitte führen Sie das Lab in einer Linux-Umgebung aus.
Senden der folgenden curl-Anfrage:
curl --http3 -H "foooooo\r\n: barr" -iL -k https://192.168.1.104/Antwort der anfälligen Version:
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
Antwort der gepatchten Version:
curl: (56) HTTP/3 stream 0 reset by server
Basierend auf den obigen Erkenntnissen zeigen die entsprechenden Antworten, dass die anfällige Version von HAProxy das \r\n-Präfix zum Backend-Server durchgelassen hat, während die gepatchte Version die Verbindung zwischen Client und HAProxy abgebrochen hat.
Ein Angreifer kann einen HTTP-Request-Smuggling-Angriff basierend auf dem Backend-Verhalten und der Art und Weise, wie der Backend-Server den fehlerhaften Header behandelt, durchführen. Meiner Ansicht nach besteht die größte Sorge darin, dass ein Angreifer die oben genannte CVE ausnutzen könnte, um einen Denial-of-Service (DoS)-Angriff durchzuführen.
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