Ноутбук на тёмном антистатическом коврике, на экране открыт Burp Suite Repeater с HTTP-запросом, разбитым на две части: голубые заголовки Content-Length и янтарное тело с chunked-данными, строка ст...


На 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 через внутренние эндпоинты, недоступные извне
По классификации CWE это CWE-444: Inconsistent Interpretation of HTTP Requests. Уязвимость инфраструктурная - эксплуатируется не баг в коде приложения, а несогласованность парсеров двух серверов. И вот это самое неприятное для защитников: код приложения может быть идеальным, а дыра - в слое, который никто не проверяет.

Как работает рассинхронизация HTTP-запросов​

Предусловия применимости:
  • Работает если: фронтенд и бэкенд связаны persistent connection (keep-alive), HTTP/1.1, фронтенд мультиплексирует запросы разных клиентов в один TCP-канал к бэкенду
  • Не работает если: end-to-end HTTP/2 без downgrade, connection reuse отключён, бэкенд strict-парсит и отклоняет запросы с обоими CL и TE (400 Bad Request)
[Применимо: внешний пентест, bug bounty, CTF web - везде, где между клиентом и приложением стоит промежуточный HTTP-агент]

Механика строится на конфликте двух заголовков 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
Фронтенд парсит chunked: чанк размером 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 reuseSmuggled 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 есть лаба именно с этим вектором и ментор в чате при затыке.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab