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-запроса от другого пользователя.
Схематичная структура пакета
|
1 2 3 4 5 6 7 8 9 10 |
POST / HTTP/1.1 Host: target.corp Content-Length: 30 Transfer-Encoding: chunked 0 POST /admin/delete HTTP/1.1 Host: target.corp Foo: x |
- Фронтенд: видит
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, бэкенд воспринимает как начало следующего входящего запроса .
Схематичная структура пакета
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
POST / HTTP/1.1 Host: target.corp Content-Length: 4 Transfer-Encoding: chunked 5c GPOST /admin/leak HTTP/1.1 Host: target.corp Content-Type: application/x-www-form-urlencoded Content-Length: 150 x= 0 |
- Фронтенд: читает чанк
5c(шестнадцатеричные 92 байта), затем чанк0, пропуская всю конструкцию через WAF . - Бэкенд: смотрит только на
Content-Length: 4и считает телом первые 4 байта (5c\r\n) . - Все остальное (
GPOST /admin/leak...) остается лежать в сокете . Следующий запрос другого пользователя допишется в параметрx=, улетая на сервер вместе с чужими cookies и токенами авторизации .
Вариант TE.TE и методы обфускации заголовков
Если оба сервера поддерживают Transfer-Encoding, атакующий ищет способ «ослепить» один из компонентов с помощью обфускации заголовка:
|
1 2 3 4 5 6 7 8 9 |
Transfer-Encoding: chunked Transfer-Encoding: x Transfer-encoding: [tab]chunked Transfer-Encoding: chunked\r Transfer-Encoding: chunked X: X[\n]Transfer-Encoding: chunked |
Если один парсер отбросит искаженный заголовок из-за ошибки синтаксиса и откатится к 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в сокетах между прокси и бэкендом), если архитектура не позволяет гарантировать строгий парсинг заголовков .
Советы:
- Фазинг обфускации заголовков: проверить реакцию фронтенда на нестандартные разделители (вертикальная табуляция, пробелы перед двоеточием, CR без LF в заголовках).
- Анализ таймаутов (Time-based desync): замерить задержки ответа, отправляя половинчатые чанки — если бэкенд висит в ожидании данных ровно по
read_timeout, точка входа найдена. - Проверка понижения протокола (H2.CL / H2.TE): протестировать фронтенд, принимающий HTTP/2, на предмет того, как он транслирует псевдозаголовки в HTTP/1.1 при пересылке на внутренний сервер.
- Кэш-пойзонинг через десинк: проверить, можно ли контрабандным префиксом заставить бэкенд вернуть 301-редирект или статическую ошибку, которая затем закешируется прокси для всех пользователей.

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