
HTTP3ONSTEROIDS - Una ricerca su CVE-2023-25950 dove l'implementazione HTTP/3 di HAProxy non riesce a bloccare un nome di campo header HTTP malformato.

L'implementazione HTTP/3 di HAProxy non riesce a bloccare un nome di campo header HTTP malformato, e quando implementato davanti a un server che elabora erroneamente questo header malformato, può essere utilizzato per condurre un attacco di smuggling di richieste/risposte HTTP. Un attaccante remoto può alterare una richiesta di un utente legittimo. Di conseguenza, l'attaccante può ottenere informazioni sensibili o causare una condizione di denial-of-service (DoS).
https://jvn.jp/en/jp/JVN38170084/
Un ottimo approccio prima di iniziare qualsiasi ricerca su CVE è iniziare leggendo la descrizione della vulnerabilità e poi verificare se i commit per la correzione sono disponibili.
Nel mio caso, il commit era disponibile, ed è evidente che gli sviluppatori di HAProxy hanno dimenticato di includere i controlli della RFC 9114 4.1.2. Richieste e Risposte Malformate durante l'analisi degli header standard nell'implementazione di HTTP3.
Fai riferimento al commit di patch qui sotto.
--- 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;
Repository - haproxy-2.7.git/commit
Di seguito, puoi trovare il codice che dimostra l'assenza di sanificazione dei nomi degli header nell'implementazione della gestione degli header standard su 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;
}
Di seguito, puoi trovare il codice che viene utilizzato per verificare se il nome dell'header è valido.
Il codice inizia con un ciclo for che itera attraverso ogni carattere di un nome di campo header. Il ciclo va da i = 0 a i < list[hdr_idx0.n.len], dove list è un array o struttura contenente informazioni sugli header, e hdr_idx è un indice che rappresenta l'header specifico che viene controllato.
All'interno del ciclo, il codice estrae il carattere corrente c dal nome del campo header. list[hdr_idx].n.ptr[i] accede al carattere alla posizione i-esima del nome del campo header.
La parte successiva del codice contiene un'istruzione if. Controlla se il carattere corrente c soddisfa una delle due condizioni:
(uint8_t)(c - 'A') < 'Z' - 'A': Questo controlla se il carattere è una lettera maiuscola (da A a Z) sottraendo 'A' da c e convertendo il risultato in uint8_t. Se il risultato è minore della differenza tra 'Z' e 'A', allora il carattere è una lettera maiuscola.!HTTP_IS_TOKEN(c): Questa condizione controlla se il carattere è un carattere token HTTP valido. HTTP_IS_TOKEN funziona controllando prima se il nome dell'header contiene token. Un token è una sequenza di caratteri che non è un carattere riservato nel protocollo HTTP. I caratteri riservati sono caratteri che hanno un significato speciale nel protocollo HTTP, ad esempio :, /, ? e #./*
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'intero laboratorio è eseguito su Docker. Puoi avviare il laboratorio con i seguenti comandi:
cd /labdocker-compose up --buildNota che la build Docker impiegherà 15-20 minuti per terminare.
Tuttavia, prima di eseguire il laboratorio, devi apportare alcune modifiche di configurazione:
/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
Puoi scegliere tra la versione vulnerabile e quella corretta cambiando il valore dell'argomento in 'vuln' o 'patched' per condurre il tuo test.
...
args:
- haproxy_version=patched || vuln
...
e infine importa il certificato minica.crt nel tuo browser.
⚠️ Esegui il laboratorio in un ambiente Linux.
Inviando la seguente richiesta curl:
curl --http3 -H "foooooo\r\n: barr" -iL -k https://192.168.1.104/Risposta della versione vulnerabile:
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
Risposta della versione corretta:
curl: (56) HTTP/3 stream 0 reset by server
Sulla base dei risultati sopra, le corrispondenti risposte indicano che la versione vulnerabile di HAProxy ha permesso al prefisso \r\n di passare al server backend, mentre la versione corretta ha interrotto la connessione tra il client e HAProxy.
Un attaccante può condurre un attacco di HTTP Request Smuggling basato sul comportamento del backend e su come il server backend tratterà l'header malformato. A mio avviso, la preoccupazione più significativa è che un attaccante potrebbe sfruttare il suddetto CVE per effettuare un attacco Denial of 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