РАЗБОР
На проверке
Обход ACL через парсинг URL на реверс-прокси
[ обложка статьи ]
Режим чтения
На пентесте API-сервиса, стоявшего за nginx, административная панель
/admin была закрыта стандартным location /admin { deny all; }. Прямой запрос к /admin - 403. Ожидаемо. Отправляю curl --path-as-is "https://target/static/%2e%2e/admin" - бэкенд (Django за WSGI-сервером) возвращает 200 с полным интерфейсом управления. Обход ACL через парсинг URL - одна команда, три секунды, ноль аутентификации.Что произошло: nginx не декодировал
%2e при сопоставлении с location-блоком, а WSGI-сервер (uWSGI/Gunicorn) декодировал %2e%2e в .. и передал нормализованный PATH_INFO в Django, где URL resolver сопоставил его с /admin. Два парсера - две разные картины мира.Этот класс уязвимостей - URL parser confusion - стабильно всплывает на проектах с reverse proxy в стеке. На русском языке с конкретными payload'ами тема практически не описана. Исправляем.
Зачем атакующему URL parser confusion
URL parser confusion - прямой вектор Exploit Public-Facing Application (T1190, Initial Access по MITRE ATT&CK). Эксплуатация занимает минуты, не требует аутентификации и даёт доступ к ресурсам, которые разработчик считал защищёнными. Никакой теории ради теории.Что получает атакующий при обходе ACL:
- Доступ к admin-панелям - если ACL реверс-прокси обходится, бэкенд отдаёт административный интерфейс без проверки прав. Оттуда - privilege escalation, чтение данных, lateral movement
- SSRF - обход allowlist-валидации URL перенаправляет серверный запрос на внутренний endpoint. Прямое попадание в OWASP A10:2021 - Server-Side Request Forgery
- Cache poisoning - фронтенд кеширует ответ по сырому пути, бэкенд нормализует путь до другого ресурса. Результат: подмена кешированного контента для всех пользователей
- Open redirect через OAuth logout - parser confusion в logout URL позволяет перенаправить пользователя на вредоносный сайт. Именно так сработала CVE-2021-32786 в mod_auth_openidc - open redirect из-за расхождения
apr_uri_parse()и браузерного WHATWG-парсинга
| Требование | Детали |
|---|---|
| Архитектура | Реверс-прокси (nginx, Apache, Envoy, HAProxy) перед бэкендом |
| ACL на прокси | location-блоки с deny, allow или proxy-уровневая авторизация |
| Рассинхрон парсеров | Прокси и бэкенд используют разные правила декодирования/нормализации пути |
| Доступ | Сетевой доступ к прокси без аутентификации |
| Версии ПО | Зависит от конкретной пары; nginx любой версии + бэкенд с декодированием %2e |
Различия парсинга URL между прокси и бэкендом: механика
URL определён несколькими RFC: RFC 1738 (1994), RFC 2396 (1998), RFC 3986 (2005). Каждый менял правила. Параллельно браузеры следуют стандарту WHATWG, который отличается от RFC 3986 в обработке обратного слеша, userinfo и относительных путей. Точка с запятой (;) была допустима как параметр сегмента пути в RFC 2396, но deprecated в RFC 3986. Обратный слеш (\) - разделитель пути в WHATWG, но обычный символ в RFC-based парсерах.Весь этот зоопарк стандартов - не академический курьёз. По данным совместного исследования Snyk и Claroty (Team82, «URL Parsing Confusion», 2022), проанализировавших URL-парсинг-библиотеки на разных языках, несоответствия укладываются в четыре категории.
Четыре категории рассинхрона парсеров
| Категория | Суть | Payload-пример |
|---|---|---|
| Scheme confusion | Парсеры по-разному обрабатывают отсутствие схемы | javascript:alert(1) vs //host |
| Slash confusion | Количество слешей влияет на разбор authority vs path | ///evil.com - путь или хост? |
| Backslash confusion | \ = / в WHATWG, но не в RFC-парсерах | https://evil\@legit.com |
| URL-encoded confusion | Декодирование %2F, %2E до или после нормализации | /public/%2e%2e/admin |
Для обхода контроля доступа nginx через реверс-прокси самая опасная - четвёртая категория. Суть: прокси принимает решение о маршрутизации по пути до полного декодирования, а бэкенд декодирует до обработки запроса. Возникает differential parsing vulnerability - два компонента видят один HTTP-запрос как обращение к разным ресурсам.
Парсеры разных языков следуют разным стандартам (обобщение на основе документации и тестирования):
| Язык / библиотека | Стандарт | \ как / | Примечание |
|---|---|---|---|
| Node.js (new URL) | WHATWG | Да | Совместимость с браузерами |
| Python urllib | RFC 3986 (с отклонениями) | Нет | Нетипичное поведение среди RFC-парсеров |
| Go net/url | RFC 3986 (строгий) | Ошибка парсинга | Отклоняет неоднозначный ввод |
| Ruby URI | RFC 3986 (строгий) | Ошибка парсинга | Отклоняет неоднозначный ввод |
| PHP parse_url | Кастомный | Нет | Своя реализация, со своими сюрпризами |
| Java URI | RFC 2396 | Нет | Старый стандарт |
Когда nginx (C-based парсер, RFC-ориентированный) стоит перед Node.js-бэкендом (WHATWG) - рассинхрон практически гарантирован. Go-прокси перед Python-бэкендом, Apache перед Tomcat - та же история. Каждая комбинация - свой набор edge cases, и на каждом проекте приходится проверять заново.
Реверс-прокси уязвимости: конфигурация nginx proxy_pass
Misconfiguration reverse proxy в nginx чаще всего связана с директивойproxy_pass. Ключевой момент - указан ли URI (путь) в директиве. Разница в одном символе полностью меняет поведение нормализации путей URI.
NGINX:
# ПОВЫШЕННЫЙ РИСК: proxy_pass с путём (trailing slash)
location /api/ {
proxy_pass http://backend:8080/;
}
# МЕНЕЕ РИСКОВАННО: proxy_pass без пути (не гарантирует полную защиту)
location /api/ {
proxy_pass http://backend:8080;
}
/), nginx подставляет в проксируемый запрос декодированный и нормализованный путь - переменную $uri. Без URI nginx передаёт оригинальный сырой запрос. Один слеш - и поведение диаметрально противоположное.Что происходит при обработке закодированного пути
Запрос:GET /api/%2e%2e/secretС trailing slash (уязвимый конфиг):
- nginx декодирует
%2e%2eв..при построении$uri - nginx нормализует путь:
/secret - location
/api/совпало на этапе matching - deny-правил нет - Путь после отрезки
/api/:/../secret→ нормализуется до/secret - Бэкенд получает:
GET /secret- ACL обойден
%2F: encoded slash обрабатывается nginx иначе, чем %2e (encoded dot). В большинстве конфигураций nginx либо блокирует %2F, либо трактует его буквально, не декодируя в /. Основной вектор обхода - именно декодирование %2e в . и последующая нормализация ...Без trailing slash (менее рискованный конфиг, но не серебряная пуля - location-matching в nginx всё равно выполняется по нормализованному URI):
- nginx передаёт сырой URI:
/api/%2e%2e/secret - Бэкенд получает закодированный путь без нормализации на стороне прокси
- Дальнейшая обработка - ответственность бэкенда
/admin/%2e%2e/admin будет нормализован до /admin при matching - location /admin сработает и ACL отработает. Но если ACL стоит на /admin, а запрос идёт к /public/%2e%2e/admin, то при proxy_pass с trailing slash nginx нормализует путь до /admin на этапе matching, и deny сработает. Обход возможен, когда нормализованный путь не совпадает с location, на котором стоит ACL - например, при частичном декодировании или когда nginx не декодирует конкретную последовательность, но бэкенд декодирует.Конфигурации с trailing slash массово копируются из документации, StackOverflow и туториалов. Уязвимости reverse proxy настройки такого типа - одна из самых частых находок на проектах с API-сервисами. Копипаста из туториала - и контроль доступа превращается в декорацию.
Payload'ы для обхода ACL через нормализацию URL
Конкретные payload'ы, которые проверяю на каждом пентесте с reverse proxy. Все отправляю черезcurl --path-as-is - без этого флага curl нормализует .. на стороне клиента и payload не дойдёт до прокси в сыром виде.Допустим, ACL запрещает доступ к
/admin:
Bash:
# Базовый тест encoded traversal
curl -v --path-as-is "https://target/public/%2e%2e/admin"
# Backslash confusion
curl -v --path-as-is "https://target/public/..%5c/admin"
# Double encoding (если бэкенд декодирует дважды)
curl -v --path-as-is "https://target/public/%252e%252e/admin"
# Path parameter для Tomcat-бэкендов
curl -v --path-as-is "https://target/admin;test=1"
Backslash confusion: WHATWG против RFC
Обратный слеш\ (URL-encoded: %5C) - недооценённый вектор для нормализации URL атаки. WHATWG-совместимые парсеры (браузеры, Node.js new URL()) трактуют \ как /. RFC-based парсеры (Apache apr_uri_parse(), Python urllib) - нет.Отсюда конкретная path confusion атака: если прокси использует RFC-парсер, а бэкенд или итоговый клиент (браузер) - WHATWG, путь
/safe\..\/admin будет интерпретирован по-разному на каждом уровне. Прокси видит один хост или путь, браузер - другой. Красота.Semicolon: специфика Tomcat
Apache Tomcat трактует; в пути как начало path parameters (поведение из RFC 2396, deprecated в RFC 3986). Nginx этого не делает.Запрос
/admin;jsessionid=xxx - nginx сопоставляет с location, которого может не быть (нет location для /admin;jsessionid=xxx). Tomcat отрезает ;jsessionid=xxx и обрабатывает /admin. Различия парсинга URL прокси и бэкенда здесь работают как bypass access control без какого-либо декодирования. Просто точка с запятой - и ACL мимо.CVE: path confusion атака в реальных продуктах
CVE-2007-0450: Tomcat directory traversal через прокси
CVE-2007-0450 (CWE-22) затрагивала Apache Tomcat 5.x до 5.5.22 и 6.x до 6.0.10 при использовании proxy-модулей (mod_proxy, mod_rewrite, mod_jk). Комбинации/, \ и %5C позволяли читать произвольные файлы через path traversal - эти символы валидные разделители пути в Tomcat, но не в Apache, что и создаёт path confusion между прокси-модулем и бэкендом. Для уязвимости есть подтверждённый эксплойт (EDB-29739, автор D. Matscheko).Именно после CVE-2007-0450 Tomcat ввёл параметр
ALLOW_ENCODED_SLASH (по умолчанию false), а Apache - директиву AllowEncodedSlashes. 2007 год, а на проектах до сих пор встречаю конфиги, где ALLOW_ENCODED_SLASH=true включено «для совместимости».CVE-2021-32786: backslash в mod_auth_openidc
Уязвимость в mod_auth_openidc до версии 2.4.9 (CVSS 4.7, MEDIUM, вектор CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:L/A:N). CWE-601 (Open Redirect).Функция
oidc_validate_redirect_url() использовала apr_uri_parse(), парсящую URL по RFC 2396/3986. Браузеры следуют WHATWG. Атакующий отправлял https://evil.com\@legit.com/ в параметре logout - модуль Apache видел evil.com\ как userinfo (часть до @), а host считал legit.com. Валидация проходила. Браузер же интерпретировал \ как / по WHATWG и перенаправлял пользователя на evil.com.Фикс оказался до смешного простым: замена всех backslash на forward slash перед валидацией.
Аналогичная уязвимость - CVE-2019-3877 в mod_auth_mellon до v0.14.2 (CWE-601): backslash в logout URL проходил валидацию как относительный путь, но браузер трактовал его как абсолютный URL. Один и тот же паттерн, разные модули, разные годы.
CVE-2022-0691: control characters в url-parse
Уязвимость в NPM-пакете url-parse до версии 1.5.9 (CVSS 9.8, CRITICAL, вектор CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). CWE-639 (Authorization Bypass Through User-Controlled Key).Regex для обрезки whitespace не покрывал ASCII control characters
\x00–\x1f. Атакующий отправлял \x00http://evil.com - url-parse возвращал некорректный host, обходя allowlist. Результат: SSRF или bypass аутентификации.Кроме CVE-2022-0691, url-parse был подвержен path traversal (GHSA-9m6j-fcg5-2442), некорректному парсингу
@ (GHSA-8v38-pw62-9cw2) и недостаточной валидации (GHSA-46c4-8wrp-j99v). Библиотека с 7+ миллионами скачиваний в неделю, и в ней - целый букет проблем парсинга. Классический пример уязвимого компонента по OWASP A06:2021 - Vulnerable and Outdated Components.Fingerprinting парсеров и защита от обхода ACL
Recon: определение парсера на каждом уровне
Перед эксплуатацией нужен fingerprinting стека - определить, как каждый уровень обрабатывает encoded path. Это фаза Vulnerability Scanning (T1595.002, Reconnaissance по MITRE ATT&CK). Отправляю серию запросов к известному endpoint и сравниваю ответы:| Запрос | Ответ 200 | Интерпретация |
|---|---|---|
GET /known/%2e./known | Да | Прокси декодирует %2e и нормализует .. |
GET /known/%2e./known | 400 / 404 | Прокси не декодирует или блокирует encoded dots |
GET /known/..%5c/known | Да | Прокси обрабатывает backslash traversal |
GET /known;param/ | 200 (контент /known) | Бэкенд вырезает path parameters |
GET /%6b%6e%6f%77%6e | 200 (контент /known) | Полная декодировка пути на бэкенде |
Если на первый запрос ответ 200 - перед вами уязвимая конфигурация. Дальше определяем, какие location-блоки имеют ACL, и строим payload для конкретного обхода.
Чеклист защиты и детектирования
На стороне nginx:- Не указывайте путь (URI) в
proxy_pass-proxy_pass http://backend;вместоproxy_pass http://backend/; - Блокируйте запросы с encoded traversal-последовательностями: добавьте
if ($request_uri ~* "%2[eE]") { return 403; }в server-блок. Осторожно: правило сработает на%2e/%2Eв любом месте URI, включая query string - высок риск false positives. Перед применением в продакшне протестируйте на реальном трафике и рассмотрите WAF-правило с учётом позиции в path - Используйте
$request_uri(сырой путь) для логирования и security-проверок вместо$uri(декодированный) - Проверьте, что
merge_slashes on;включено (по умолчанию - да, но стоит убедиться)
- Tomcat: оставьте
ALLOW_ENCODED_SLASH=false(по умолчанию). Параметр введён после CVE-2007-0450 - Node.js: не используйте url-parse ниже 1.5.9 для security-решений. Предпочитайте встроенный
new URL()(WHATWG) - Django/Flask: валидируйте путь после нормализации, не до. Middleware нормализации должен отрабатывать раньше ACL
- Нормализуйте URL до канонической формы до применения ACL - единственный надёжный подход при нескольких парсерах в цепочке
Ищите в access-логах запросы с закодированными traversal-символами:
Bash:
grep -iE '%2[eEfF]|%5[cC]' /var/log/nginx/access.log
$request_uri и $uri рядом. Если они различаются для одного запроса и запрос прошёл на бэкенд - нормализация произошла, и это потенциальный вектор path traversal через прокси.Значительная часть проектов с nginx перед бэкендом, которые я тестировал за последние два года, имела хотя бы одну конфигурацию
proxy_pass с trailing slash. Разработчики копируют этот паттерн из туториалов, не задумываясь, что за ним стоит полная декодировка и нормализация пути. ACL на уровне location-блока nginx в этом случае - иллюзия контроля.Проблема глубже: индустрия привыкла думать об ACL как о чём-то, что «ставится один раз и работает». На практике два компонента в цепочке с разными URL-парсерами - это не вопрос «если», а вопрос «когда» desync атака HTTP приведёт к обходу. Go-прокси перед Python-бэкендом, nginx перед Tomcat, Envoy перед Express - каждая комбинация со своими вариациями. Стандарт WHATWG и RFC 3986 не сойдутся никогда - слишком много legacy, слишком много обратной совместимости. URL parser confusion как класс уязвимостей будет жить ещё долго.
Единственная рабочая стратегия - не доверять ни одному уровню по отдельности: проверять ACL и на прокси, и на бэкенде, тестировать рассинхрон при каждом изменении стека. На HackerLab лежит сценарий, где подобный primitive нужно собрать в полную цепочку - от encoded traversal до доступа к закрытым endpoint'ам за proxy.
AI-выжимка
сгенерировано ИИ
Тезисы статьи скоро
Статья читается полностью. Тезисы со ссылками на разделы появятся позже.
Содержание
Codeby Academy
Практика и мастерство
От основ до продвинутого — программы для практиков Codeby.
Перейти к курсу →
Поиск в обсуждении
Продолжить чтение
Следующий разбор
Разбор атаки SSTI Standoff 365: от инъекции до root
Комментарии
0