HTTP Request Smuggling, Web-Pentest

HTTP Request Smuggling: десинк-атаки CL.TE/TE.CL, которые ломают весь reverse-proxy стек

Салют, старик! Заваривай крепкий кофе, расчехляй консоль — сегодня разберем одну из самых красивых концепций веб-пентеста, которая регулярно ставит на колени корпоративные балансировщики и CDN.

Десинхронизация запросов (HTTP Desync / Request Smuggling) бьет не в логику кода приложения, а в архитектурный зазор между фронтендом (Nginx, HAProxy, Cloudflare, AWS ALB) и бэкенд-серверами (Node.js, Gunicorn, Tomcat, IIS).

Архитектурный корень проблемы

В современном вебе клиент редко общается с бэкендом напрямую: между ними стоит цепочка reverse proxy для балансировки, кэширования и фильтрации трафика.

Чтобы не открывать новое TCP-соединение на каждый чих, прокси используют HTTP Keep-Alive (HTTP Pipelining/Reuse). Запросы от десятков клиентов упаковываются в один постоянный сокет к бэкенду.

Границы сообщений в протоколе HTTP/1.1 определяются двумя заголовками :

  • Content-Length (CL) — указывает длину тела запроса в байтах .
  • Transfer-Encoding: chunked (TE) — указывает, что тело передается кусками фиксированного размера в шестнадцатеричном формате, а завершается нулевым чанком 0\r\n\r\n.

Стандарт RFC 7230 четко говорит: если присутствуют оба заголовка, Transfer-Encoding обязан иметь приоритет. Но реальный мир суров — разные серверы написаны разными людьми, и если фронтенд парсит длину по одному заголовку, а бэкенд — по другому, возникает критический рассинхрон границы TCP-потока.


Вектор CL.TE: Фронтенд верит CL, Бэкенд верит TE

Фронтенд обрабатывает запрос, ориентируясь на Content-Length, а бэкенд парсит чанки через Transfer-Encoding .

Механика десинхронизации

Фронтенд считывает ровно столько байт, сколько указано в CL, и пробрасывает тело дальше в сокет . Бэкенд же читает поток чанками, доходит до терминатора 0 и считает текущий запрос завершенным . Оставшиеся байты «хвоста» зависают в буфере сокета и склеиваются с началом следующего легитимного HTTP-запроса от другого пользователя.

Схематичная структура пакета

  • Фронтенд: видит Content-Length: 30, считает все байты до конца тела валидным пакетом и шлет дальше .
  • Бэкенд: видит Transfer-Encoding: chunked. Первый чанк имеет длину 0 . Бэкенд решает, что запрос завершен, а строчки POST /admin/delete... оставляет в буфере конвейера .
  • Жертва: присылает следом свой обычный GET /index.html HTTP/1.1. Бэкенд приклеивает его к остатку в буфере, выполняя скрытый запрос с правами или сессией жертвы.

Вектор TE.CL: Фронтенд верит TE, Бэкенд верит CL

Здесь ситуация зеркальная: фронтенд понимает chunked, а бэкенд игнорирует его (или считает заголовок некорректным) и ориентируется исключительно на Content-Length .

Механика десинхронизации

Фронтенд считывает чанки вплоть до финального нуля, а бэкенд забирает только короткий кусочек по Content-Length . Все, что лежало внутри чанка после границы CL, бэкенд воспринимает как начало следующего входящего запроса .

Схематичная структура пакета

  • Фронтенд: читает чанк 5c (шестнадцатеричные 92 байта), затем чанк 0, пропуская всю конструкцию через WAF .
  • Бэкенд: смотрит только на Content-Length: 4 и считает телом первые 4 байта (5c\r\n) .
  • Все остальное (GPOST /admin/leak...) остается лежать в сокете . Следующий запрос другого пользователя допишется в параметр x=, улетая на сервер вместе с чужими cookies и токенами авторизации .

Вариант TE.TE и методы обфускации заголовков

Если оба сервера поддерживают Transfer-Encoding, атакующий ищет способ «ослепить» один из компонентов с помощью обфускации заголовка:

Если один парсер отбросит искаженный заголовок из-за ошибки синтаксиса и откатится к Content-Length, а второй сервер его распарсит, архитектура немедленно сваливается в состояние CL.TE или TE.CL.


Сравнение векторов десинхронизации

ВекторПоведение Front-EndПоведение Back-EndХарактерное проявление при детекте
CL.TEСчитывает тело по CLСчитывает тело по TE (до 0)Таймаут бэкенда при ожидании следующего чанка
TE.CLСчитывает чанки до 0Считывает только байты по CLНеожиданный ответ 404/405 на последующий запрос
TE.TEПарсит один из вариантовОткатывается к fallback-режимуПоведение сводится к CL.TE либо TE.CL

Архитектурные меры защиты

Чтобы закрыть этот класс уязвимостей на уровне инфраструктуры, применяются следующие подходы:

  • Использование сквозного HTTP/2 или HTTP/3 от границы периметра до самих микросервисов приложения. В бинарных протоколах длина фреймов строго специфицирована, что устраняет двусмысленность текстовых заголовков.
  • Настройка реверс-прокси на строгую нормализацию и отбрасывание любых входящих запросов, содержащих одновременно Content-Length и Transfer-Encoding .
  • Запрет повторного использования TCP-соединений (отключение keep-alive в сокетах между прокси и бэкендом), если архитектура не позволяет гарантировать строгий парсинг заголовков .

Советы:

  1. Фазинг обфускации заголовков: проверить реакцию фронтенда на нестандартные разделители (вертикальная табуляция, пробелы перед двоеточием, CR без LF в заголовках).
  2. Анализ таймаутов (Time-based desync): замерить задержки ответа, отправляя половинчатые чанки — если бэкенд висит в ожидании данных ровно по read_timeout, точка входа найдена.
  3. Проверка понижения протокола (H2.CL / H2.TE): протестировать фронтенд, принимающий HTTP/2, на предмет того, как он транслирует псевдозаголовки в HTTP/1.1 при пересылке на внутренний сервер.
  4. Кэш-пойзонинг через десинк: проверить, можно ли контрабандным префиксом заставить бэкенд вернуть 301-редирект или статическую ошибку, которая затем закешируется прокси для всех пользователей.
HTTP Request Smuggling: десинк-атаки CL.TE/TE.CL, которые ломают весь reverse-proxy стек

На этом всё. Всем хорошего дня!