На CTF-финале задача по web стоила 500 очков - решили её 2 команды из 40. Связка nginx + gunicorn, HTTP/1.1, connection reuse включён. Нужно было подбросить
GET /admin через «хвост» в chunked-теле - классический CL.TE desync. Почти все участники знали теорию, но застревали на одном и том же: неправильно считали байты в Content-Length, забывали про \r\n между чанками и не снимали галку Update Content-Length в Burp Repeater. HTTP Request Smuggling - техника, где разрыв между «прочитал статью» и «воспроизвёл атаку» максимален. Разберём её от механики конфликта заголовков до payload'ов, которые можно скопировать и протестить.Бизнес-логика desync атаки веб-приложений
Зачем атакующему HTTP Request Smuggling? Цель - не уронить сервер, а рассинхронизировать парсинг запросов между фронтендом (reverse proxy, CDN, WAF) и бэкендом (application server). Если оба компонента по-разному определяют, где заканчивается один запрос и начинается следующий, атакующий «приклеивает» вредоносный запрос к запросу другого пользователя. Подробнее - в нашем материале про пентест веб-приложений.В цепочке атаки HTTP Request Smuggling - точка входа (Exploit Public-Facing Application, T1190, Initial Access по MITRE ATT&CK), которая открывает дорогу дальше:
- Обход ACL reverse proxy - доступ к
/adminбез авторизации (Broken Access Control, OWASP A01:2021) - Угон сессий - request queue poisoning: «хвост» атакующего приклеивается к запросу жертвы, и ты перехватываешь cookie или токены
- Web cache poisoning - отравление CDN-кэша вредоносным ответом, который получат тысячи пользователей
- Content Injection (T1659) - подмена содержимого ответов
- SSRF через внутренние эндпоинты, недоступные извне
Как работает рассинхронизация HTTP-запросов
Предусловия применимости:- Работает если: фронтенд и бэкенд связаны persistent connection (keep-alive), HTTP/1.1, фронтенд мультиплексирует запросы разных клиентов в один TCP-канал к бэкенду
- Не работает если: end-to-end HTTP/2 без downgrade, connection reuse отключён, бэкенд strict-парсит и отклоняет запросы с обоими CL и TE (400 Bad Request)
Механика строится на конфликте двух заголовков HTTP/1.1.
Content-Length (CL) указывает длину тела в байтах - фиксированное число. Transfer-Encoding: chunked (TE) означает, что тело передаётся чанками: каждый начинается с размера в hex, за ним \r\n, тело чанка, снова \r\n, и вся последовательность завершается чанком 0\r\n\r\n.По RFC 7230, если в запросе присутствуют оба заголовка -
Content-Length должен игнорироваться. На практике не все серверы следуют этому правилу. По документам - нельзя использовать оба. На практике - фронтенд парсит по CL, а бэкенд по TE (или наоборот). Результат - front-end back-end рассинхронизация: один сервер считает, что запрос завершён, а другой продолжает читать. «Лишние» байты становятся началом следующего запроса в очереди.Деталь, которая ломает первые попытки в Burp Repeater: Burp по умолчанию автоматически распаковывает chunked encoding и обновляет Content-Length. Для работы с request smuggling нужно зайти в меню Repeater и снять Update Content-Length. Я на этом потерял часа полтора, пока не понял, почему payload не срабатывает - Burp молча пересчитывал CL за спиной. Кроме того, Burp по умолчанию использует HTTP/2 для серверов с ALPN - переключите протокол на HTTP/1.1 вручную через Inspector → Request Attributes.
CL.TE, TE.CL и TE.TE - три вектора desync атаки
CL.TE - фронтенд верит Content-Length
Работает если: фронтенд парсит по CL (nginx в дефолте, ряд CDN), бэкенд парсит по TE (gunicorn, некоторые конфигурации Apache). Не работает если: фронтенд тоже парсит chunked или бэкенд отклоняет запрос с конфликтующими заголовками.
Код:
POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
Content-Length: 13 и считает телом запроса всё от начала body до 13-го байта - это 0\r\n\r\nSMUGGLED. Пересылает целиком на бэкенд. Бэкенд видит Transfer-Encoding: chunked, парсит первый чанк размером 0 - всё, конец запроса. Байты SMUGGLED остаются в буфере TCP-соединения и приклеиваются к следующему запросу. Если следующий запрос - от другого пользователя, он превращается в SMUGGLEDGET /profile HTTP/1.1... - сломанный запрос, который бэкенд обработает непредсказуемо.Именно этот вектор был на том CTF. Простой, как палка, - но 38 команд из 40 не смогли правильно посчитать 13 байт.
TE.CL - фронтенд верит Transfer-Encoding
Работает если: фронтенд парсит chunked (HAProxy, некоторые WAF), бэкенд ориентируется на CL (Tomcat, IIS в определённых конфигурациях). Не работает если: оба сервера сходятся на одном заголовке.
Код:
POST / HTTP/1.1
Host: target.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0
8 с данными SMUGGLED, затем чанк 0 - всё легитимно, пересылает. Бэкенд видит Content-Length: 3 → читает три байта тела (8\r\n) → остаток SMUGGLED\r\n0\r\n\r\n застревает в буфере. В Burp здесь обязательно снять Update Content-Length, иначе Burp пересчитает CL автоматически и payload сломается. Также после финального 0 нужна последовательность \r\n\r\n - без неё бэкенд не считает чанк-последовательность завершённой. Забудешь один \r\n - и вместо smuggling получишь 400-й ответ.TE.TE - обфускация Transfer-Encoding
Оба сервера поддерживают TE, но один можно заставить проигнорировать заголовок через обфускацию - и он откатится на CL. Варианты обфускации по данным OWASP WSTG и PortSwigger:| Техника | Пример заголовка |
|---|---|
| Tab вместо пробела | Transfer-Encoding:\tchunked |
| Дублирование с невалидным значением | Transfer-Encoding: chunked + Transfer-Encoding: identity |
| Невалидный суффикс | Transfer-Encoding: chunkedx |
| Лишний пробел перед значением | Transfer-Encoding: chunked (два пробела) |
Один сервер берёт валидный
Transfer-Encoding: chunked, другой видит невалидное значение (или два конфликтующих заголовка) и откатывается на Content-Length. Результат - аналогичный CL.TE или TE.CL desync в зависимости от того, какой сервер «сломался».TE.TE - самый хитрый из трёх. Тут уже не формулы, а перебор: какой именно мусор в заголовке заставит конкретный сервер сдаться. На практике это означает, что
smuggler.py прогоняет десятки вариантов обфускации и смотрит, кто первый моргнёт.HTTP Request Smuggling пентест: обнаружение через тайминг
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
, которого нет - наступает таймаут (5–30 секунд). Для проверки TE.CL - зеркально: фронтенд пересылает по chunked, бэкенд видит CL больше полученного и ждёт оставшиеся байты.
Критический порядок: всегда начинайте с CL.TE. Если сразу отправить TE.CL-пробу, а приложение уязвимо к CL.TE - вы отравите очередь запросов на бэкенде и повлияете на других пользователей. На bug bounty это прямой путь к бану программой. Я видел, как человек получил перманентный бан на HackerOne именно за это - сначала стрелял TE.CL, отравил очередь, реальные пользователи получили 500-е ответы.
После обнаружения тайминга подтвердите уязвимость через дифференциальный ответ: отправьте attack request с «хвостом»
GET /nonexistent HTTP/1.1 и сразу - обычный запрос на валидный endpoint, но через другое TCP-соединение. Если обычный запрос вернул 404 вместо 200 - «хвост» приклеился, десинхронизация подтверждена. По методике PortSwigger, оба запроса должны идти на один URL и с одинаковыми параметрами - иначе фронтенд может отправить их на разные бэкенды, и атака не сработает.HTTP/2 downgrade - обход прокси и балансировщиков нагрузки
Даже HTTP/2-фронтенд не спасёт, если бэкенд остаётся на HTTP/1.1. Фронтенд выполняет downgrade: конвертирует HTTP/2-фреймы в HTTP/1.1-запросы. Именно в этом слое трансляции возникают современные варианты desync, описанные Джеймсом Кеттлом в ресерче «HTTP/2: The Sequel is Always Worse» (название, кстати, не врёт).H2.CL: при конвертации фронтенд сохраняет заголовок
Content-Length из HTTP/2-фрейма. Если длина DATA-фрейма расходится с CL - бэкенд парсит по CL, а «лишние» байты превращаются в smuggled запрос.H2.TE: фронтенд переносит
Transfer-Encoding из HTTP/2 в HTTP/1.1, хотя в HTTP/2 этот заголовок не определяет длину (длина задаётся на уровне фреймов). Бэкенд на HTTP/1.1 начинает парсить chunked - и десинхронизация повторяется.H2C smuggling (по данным Bishop Fox): клиент инициирует upgrade с HTTP/1.1 на cleartext HTTP/2 через заголовок
Upgrade: h2c. Если фронтенд частично принимает upgrade, а бэкенд продолжает парсить HTTP/1.1 - остаточные байты в буфере становятся отдельным запросом.Не работает если: end-to-end HTTP/2 без downgrade на бэкенде, фронтенд полностью очищает hop-by-hop заголовки при трансляции, H2C-upgrade явно запрещён.
Защита от HTTP Request Smuggling
| Мера | Эффект | Ограничение |
|---|---|---|
| End-to-end HTTP/2 | Устраняет CL/TE конфликт полностью | Требует поддержки HTTP/2 на всех бэкендах - legacy не всегда позволяет |
| Отключение connection reuse | Smuggled bytes не попадут в чужой запрос | Серьёзная деградация производительности при высокой нагрузке |
| Strict RFC parsing (400 на CL+TE) | Отклоняет запросы с конфликтующими заголовками | Не все серверы и прокси поддерживают strict mode |
| Нормализация заголовков на фронтенде | Удаление дубликатов TE, trim пробелов | Не защищает от HTTP/2 downgrade vectors |
| Отключение H2C upgrade | Закрывает вектор H2C smuggling | Не влияет на implicit downgrade при HTTP/2→HTTP/1.1 |
| Разрыв соединения при parse error | Предотвращает request queue poisoning | Повышает латентность при ложных срабатываниях |
По рекомендации OWASP WSTG: при обнаружении parsing error - немедленно разрывать backend connection и не пытаться «дочитать» оставшиеся байты. Это единственный способ гарантированно предотвратить отравление очереди запросов.
Многие считают HTTP Request Smuggling «академической» темой - в CTF-writeup'ах выглядит элегантно, в реальных проектах встречается якобы редко. Это ошибка выжившего. Проблема не в редкости уязвимостей HTTP протокола, а в том, что большинство пентестеров просто не проверяют: стандартный чеклист заканчивается на SQLi, XSS, IDOR, SSRF. Смузлинг запросов HTTP требует ручной работы в Burp Repeater, аккуратного подсчёта байтов и понимания, как конкретная связка фронтенд/бэкенд парсит чанки. Из моего опыта прохождения лаб PortSwigger Web Security Academy: smuggling-серия (больше десятка заданий - от базового CL.TE до H2 downgrade) фильтрует жёстче, чем большинство тасков по web на CTFtime. В bug bounty CWE-444 стабильно оценивается как High/Critical с соответствующими выплатами - именно потому, что мало кто умеет это эксплуатировать. Рекомендация: пройдите лабы PortSwigger, потом возьмите реальную цель с CDN перед бэкендом и попробуйте timing-based detection на живом стеке. Если хочешь не просто writeup, а пройти всю атаку от рекона до эксплуатации самому - на WAPT есть лаба именно с этим вектором и ментор в чате при затыке.