На bug bounty в финтехе я потратил три минуты на обнаружение CRLF injection в эндпоинте редиректа - и два часа на эскалацию до XSS через HTTP response splitting с захватом сессии администратора. Триажер поставил Medium, хотя цепочка вела до полного session hijacking через подставленный
Set-Cookie. Пришлось писать развёрнутый комментарий с видео-PoC, чтобы severity пересмотрели. CRLF injection не входит в OWASP Top 10 как отдельный класс - она подпадает под A03:2021 Injection - но при грамотной эскалации два инжектированных байта (\r\n) превращаются в полноценный захват сессии. Разберём механику от байт-последовательности до kill chain на реальных CVE.Место CRLF injection в цепочке атаки
CRLF injection - не финальная цель, а точка входа. Сама по себе она стоит копейки на любой bug bounty платформе. Ценность появляется, когда из неё вырастает цепочка. По классификации MITRE ATT&CK она покрывает несколько тактик в зависимости от вектора эскалации:| Этап kill chain | MITRE ATT&CK | Роль CRLF injection |
|---|---|---|
| Initial Access | Exploit Public-Facing Application (T1190) | Инъекция через параметр публичного веб-приложения |
| Initial Access / C2 | Content Injection (T1659) | Внедрение произвольного контента в HTTP-ответ |
| Execution | JavaScript (T1059.007) | Выполнение JS через XSS из response splitting |
| Credential Access | Steal Web Session Cookie (T1539) | Кража cookie инжектированным JavaScript |
| Credential Access | Web Cookies (T1606.001) | Подделка cookie через инъекцию Set-Cookie |
| Collection | Browser Session Hijacking (T1185) | Перехват активной сессии пользователя |
| Lateral Movement | Web Session Cookie (T1550.004) | Использование украденной сессии для доступа к ресурсам |
| Impact | Transmitted Data Manipulation (T1565.002) | Модификация HTTP-ответа при cache poisoning |
Типичная цепочка: CRLF injection в заголовок ответа → инъекция
Set-Cookie для session fixation (T1606.001), либо response splitting с XSS (T1059.007) для кражи cookie (T1539) → lateral movement через украденную web-сессию (T1550.004). Именно выбор вектора эскалации определяет итоговый severity. Голый CRLF без цепочки на большинстве платформ - Low или Informational. С цепочкой до session hijacking - совсем другой разговор.Механика инъекции: кодировки, серверы и обходы
HTTP/1.1 использует последовательность CR (ASCII 13,\r) + LF (ASCII 10, \n) как разделитель заголовков. Одиночный CRLF разделяет заголовки между собой, двойной CRLF (\r\n\r\n) отделяет заголовки от тела ответа. По CWE-93 (Improper Neutralization of CRLF Sequences) уязвимость возникает, когда приложение не нейтрализует CRLF-последовательности из пользовательского ввода. CWE-74 (Injection) описывает более широкий класс: приложение формирует структуру данных из внешнего ввода без фильтрации спецсимволов - и атакующий модифицирует данные, обходит защиту, меняет логику выполнения.Кодировки и обход фильтров
Символы\r\n в raw-виде через стандартный HTTP-клиент или WAF не пройдут. Рабочий вариант зависит от контекста:| Кодировка | Запись | Где работает | Где не работает |
|---|---|---|---|
| URL-encoded | %0d%0a | GET-параметры, query string | Тело POST с application/json |
| Double URL-encoded | %250d%250a | Прокси-цепочки с двойным декодированием | Одноуровневое декодирование |
| Unicode overlong | %c0%8d%c0%8a | Устаревшие парсеры UTF-8 | Современные фреймворки |
| Bare LF | %0a | Node.js http module (ряд версий) | Apache, Nginx (требуют полный CRLF) |
| Null byte + CRLF | %00%0d%0a | Приложения с truncation по null | Языки без C-string семантики |
Принципиальная разница между
%0d%0a и текстовым \r\n: первое - URL-encoded управляющие символы, которые веб-сервер декодирует перед передачей значения в приложение. Если приложение вставляет декодированное значение в HTTP-заголовок без санитизации - CRLF injection состоялась. Второе - четыре печатных символа, не управляющие коды. Путаница между ними - частая причина ложноотрицательных тестов.Различия обработки CRLF на разных серверах
Поведение при встрече CRLF в пользовательском вводе зависит от стека. Это критически важно при выборе payload - то, что работает на Node.js, может не пройти на Apache, и наоборот.Node.js (http module) - исторически принимал bare LF (
\n) как разделитель заголовков, что расширяло поверхность атаки. В ряде версий http.request() позволял инъекцию через заголовки, что привело к нескольким CVE в экосистеме. Актуальные версии Node.js фильтруют CRLF в res.setHeader(), но ручное формирование ответа через res.socket.write() остаётся уязвимым. И именно этот паттерн встречается в legacy-коде чаще, чем хотелось бы.Nginx - при использовании как reverse proxy с директивой
proxy_set_header и подстановкой переменных $arg_* может передать CRLF-последовательности на бэкенд, если значение не проходит фильтрацию через map. Сам Nginx при генерации ответа CRLF в заголовках не допускает - тут он молодец.Apache -
mod_headers экранирует CRLF при добавлении заголовков через Header set. Уязвимость сохраняется при использовании ErrorDocument с пользовательскими данными или mod_rewrite с инъекцией в заголовок Location. Второй вариант я встречал на реальных проектах - кто-то написал RewriteRule с %{QUERY_STRING} прямо в Location, и привет.Фреймворки (Express.js, Django, Spring Boot) - нейтрализуют CRLF в стандартных API для установки заголовков в актуальных версиях. Уязвимость появляется при ручном формировании HTTP-ответа через raw socket, использовании устаревших библиотек или прокси-слоёв, подставляющих пользовательские данные до фреймворка. Фреймворк защищает - но только если через него работают, а не мимо.
HTTP Response Splitting и захват сессии через CRLF
Session fixation через Set-Cookie injection
Простейший вектор - инъекция заголовкаSet-Cookie в HTTP-ответ. Если приложение подставляет пользовательский ввод в заголовок (типично: Location при редиректе, Content-Disposition при скачивании файла, кастомные заголовки), атакующий добавляет CRLF и свой заголовок.Пример: приложение формирует заголовок
X-Custom-Name: <user_input>. Атакующий отправляет значение test%0d%0aSet-Cookie:%20session=attacker_value;%20Path=/. Если сервер не фильтрует CRLF, ответ содержит дополнительный заголовок Set-Cookie: session=attacker_value; Path=/. Браузер жертвы принимает подставленный cookie - session fixation готова. Атакующий уже знает значение сессии, которую будет использовать жертва. По MITRE ATT&CK - Web Cookies (T1606.001, Credential Access).Один заголовок, два байта - и сессия зафиксирована. Дальше остаётся подождать, пока жертва залогинится.
XSS через response splitting
Двойной CRLF (%0d%0a%0d%0a) завершает секцию заголовков и начинает тело ответа. Всё, что идёт после двойного CRLF, браузер интерпретирует как HTML/JavaScript.По описанию из Acunetix, полноценное HTTP response splitting строится так:
Код:
http://target.example.com/page?param=value%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0aContent-Length:%2025%0d%0a%0d%0a<script>alert(1)</script>
Content-Length: 0 заставляет браузер считать первый ответ завершённым. Далее начинается второй «ответ» с собственными заголовками и телом, содержащим JavaScript. Браузер выполняет скрипт - это XSS (T1059.007), который далее используется для кражи cookie (T1539) или перехвата сессии (T1185).На практике полноценный response splitting с двумя «ответами» в modern стеках работает редко - серверы и прокси нормализуют заголовки. Но инъекция одного заголовка (Set-Cookie, Location, Access-Control-Allow-Origin) через одиночный CRLF работает значительно чаще. И этого хватает.
Web cache poisoning через CRLF
Когда HTTP-ответ с инъекцией кэшируется промежуточным прокси (Varnish, Squid, CDN-edge), каждый последующий пользователь получает отравленный ответ. Один запрос атакующего через CDN может затронуть тысячи пользователей до истечения TTL кэша. По CWE-113 (Improper Neutralization of CRLF Sequences in HTTP Headers) cache poisoning - одно из основных последствий HTTP response splitting.Но есть нюанс: cache poisoning через CRLF работает только если между клиентом и сервером стоит кэширующий прокси, этот прокси не нормализует HTTP-заголовки перед кэшированием, а приложение не фильтрует CRLF до формирования ответа. Без кэширующего прокси вектор ограничен единичной жертвой. Зато если прокси есть и он кэширует - масштаб поражения растёт на порядки.
CVE-2023-4767: CRLF в ManageEngine Desktop Central
Разбор реальной CVE показывает, как CRLF injection выглядит в production-коде корпоративного продукта. Не в учебном примере, а в системе, которая управляет тысячами рабочих станций.CVE-2023-4767 - CRLF injection в ManageEngine Desktop Central версии 9.1.0 (Zoho Corp.). Уязвимый параметр -
fileName в эндпоинте /STATE_ID/1613157927228/InvSWMetering.csv.| Параметр | Значение |
|---|---|
| CVSS 3.1 | 6.1 (MEDIUM) |
| Вектор | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N |
| CWE | CWE-93 (CRLF Injection), CWE-74 (Injection) |
| Затронутый продукт | zohocorp:manageengine_desktop_central 9.1.0 |
| Источник | INCIBE (Spanish National Cybersecurity Institute) |
Разбор CVSS-вектора по компонентам: AV:N - атака по сети, локальный доступ не нужен. AC:L - низкая сложность эксплуатации, никаких особых условий. PR:N - привилегии не требуются, эндпоинт доступен без аутентификации. UI:R - нужно действие пользователя (жертва кликает по ссылке). S:C - Changed scope: уязвимость в серверном компоненте влияет на другой security domain (браузер жертвы). C:L / I:L - низкий импакт на конфиденциальность и целостность (cookie manipulation, header injection). A:N - доступность не затронута.
Параметр
fileName подставляется в HTTP-заголовок ответа (предположительно Content-Disposition) без нейтрализации CRLF. Атакующий формирует URL с %0d%0a в значении fileName, отправляет ссылку жертве (отсюда UI:R в векторе) - и инжектирует произвольные HTTP-заголовки в ответ сервера.ManageEngine Desktop Central - корпоративная система управления конечными устройствами. CRLF injection в таком продукте - это Exploit Public-Facing Application (T1190, Initial Access): продукт выставлен в сеть, атака не требует аутентификации. Эскалация до session hijacking через подставленный
Set-Cookie - вопрос одного дополнительного заголовка. А учитывая, что Desktop Central по своей природе имеет доступ ко всем управляемым хостам, импакт от угнанной админской сессии выходит далеко за рамки CVSS 6.1.Обнаружение и эксплуатация CRLF injection
Требования к окружению
- ОС: Kali Linux 2024+, Parrot OS, macOS или Windows с установленным Burp Suite
- Инструменты: Burp Suite Community/Pro (Repeater), curl 7.80+, nuclei 3.x (опционально)
- Сеть: доступ к целевому приложению (тестовая среда или скоуп bug bounty)
- Привилегии: не требуются - CRLF injection в большинстве случаев эксплуатируется без аутентификации
Ручная проверка и автоматизация
Первый шаг
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Для массовой проверки в скоупе bug bounty -
nuclei с темплейтами из набора projectdiscovery/nuclei-templates (каталог http/vulnerabilities/crlf/). Nuclei отправляет запросы с CRLF-пейлоадами в параметры и проверяет наличие инжектированного заголовка в ответе. Для кастомных кейсов - ffuf с wordlist из CRLF-пейлоадов в разных кодировках, фильтрация ответов по наличию маркерного заголовка.Где WAF ловит CRLF injection - и где промахивается
Большинство WAF (ModSecurity с CRS, Cloudflare WAF, AWS WAF) детектируют стандартные%0d%0a в URL-параметрах - базовое правило, которое работает из коробки. Но ряд сценариев проходят мимо:| Сценарий | Почему WAF пропускает |
|---|---|
CRLF в теле POST с Content-Type: application/json | Многие WAF не разбирают JSON-тело на CRLF |
Double encoding (%250d%250a) при наличии reverse proxy | WAF декодирует один раз, апстрим - второй |
| CRLF в значении Cookie-заголовка | Cookie parsing в WAF менее строгий |
| CRLF через WebSocket upgrade | WAF может не инспектировать заголовки WebSocket handshake |
| Unicode overlong encoding | Устаревшие WAF без Unicode-нормализации |
На стороне серверных фреймворков - Express.js (
res.setHeader()), Django (HttpResponse), Spring Boot (HttpServletResponse.setHeader()) нейтрализуют CRLF в актуальных версиях. Уязвимость живёт в ручном формировании HTTP-ответа через raw socket, устаревших версиях библиотек и middleware-слоях, подставляющих пользовательские данные в заголовки до фреймворка.Отдельный вектор - log injection. WAF тут вообще не помощник - он защищает HTTP-ответы, а не серверные логи. Если приложение пишет пользовательский ввод в лог без фильтрации CRLF, атакующий подставляет ложные записи: в лог попадает строка с IP
127.0.0.1 вместо реального IP. Это не HTTP response splitting, но при пентесте используется для сокрытия следов - tampered log усложняет расследование.По четырём годам в bug bounty могу сказать: CRLF injection встречается примерно в каждом десятом веб-приложении с legacy-кодом. Паттерн один и тот же - разработчики защищают SQL injection и XSS, но про заголовки забывают. CRLF injection подпадает под A03:2021 Injection в OWASP Top 10, но в чеклистах разработчиков отдельным пунктом фигурирует редко. Фреймворк защищает стандартные API, а кастомный middleware, формирующий
Content-Disposition по имени файла из запроса, оставляет дыру размером в два байта.Severity CRLF injection на bug bounty платформах систематически занижается. Триажеры видят
%0d%0a в URL и ставят Low или Informational, не проверяя эскалацию. Я несколько раз показывал цепочку от CRLF до полного захвата сессии через cache poisoning - и severity пересматривали с Low на High. Мораль: нашли CRLF - не отправляйте голый PoC с одним инжектированным заголовком. Покажите полную цепочку до реального импакта: cache poisoning, session fixation, XSS. Любой из этих векторов поднимает severity и выплату. Если хочется посмотреть, как injection-цепочка от CRLF до захвата сессии собирается на живом стенде - web-задачи на HackerLab.pro построены именно на таких примитивах.