
На Fetch the Flag CTF 2022 задача File Explorer решалась одной строкой: curl http://target/public/%2e%2e%2f/flag — сервер отдавал флаг (writeup от Snyk). Стандартный ../ блокировался, а URL-кодированный %2e%2e%2f проходил без единого алерта. Фильтр проверял сырой ввод до декодирования — ошибка, на которой спотыкаются CTF-игроки, привыкшие к базовому dot-dot-slash.
Чистый path traversal без фильтра — задача на пять секунд. С фильтром — уже квест. Побеждает тот, кто держит в голове шесть техник обхода и умеет определить, какая сработает на конкретном стеке. Большинство пробуют два-три варианта и уходят — а надо было просто прогнать все шесть.
Path traversal — не изолированная строчка из чеклиста, а точка входа для полноценной цепочки. В терминах MITRE ATT&CK она выстраивается так: Подробнее — в нашем материале про пентест веб-приложений.
.././etc/hostname, /proc/self/cmdline, каталоги приложения..env, config.php, settings.py.В CTF traversal обычно ведёт к файлу с флагом. На пентесте — к паролям от баз данных, JWT-секретам, SSH-ключам. По OWASP path traversal входит в категорию A03:2021 — Injection: пользовательский ввод попадает в файловые операции без валидации. Mandiant M-Trends 2025 подтверждает: эксплуатация уязвимостей — самый распространённый вектор первоначального доступа (38% всех инцидентов), и path traversal в этой статистике занимает своё место.
Разница между path traversal и Local File Inclusion (LFI): при path traversal приложение возвращает содержимое файла. При LFI — выполняет его как код (через include() в PHP). На практике обе уязвимости эксплуатируются одинаковыми payload'ами, но импакт LFI выше: от чтения файлов до удалённого выполнения кода. В CTF writeup'ах термины часто мешают — для задачи некритично, для реального пентеста различие принципиально.
--path-as-isgo install github.com/ffuf/ffuf/v2@latestapt install seclists или git clone https://github.com/danielmiessler/SecListsdocker run -d -p 80:80 vulnerables/web-dvwaТипичная точка входа — endpoint загрузки изображений: GET /image?filename=photo.jpg. Серверная логика конкатенирует базовый каталог /var/www/images/ с пользовательским вводом. Без валидации вместо photo.jpg можно передать ../../../etc/passwd, и приложение прочитает /var/www/images/../../../etc/passwd, что файловая система резолвит в /etc/passwd.
# Простейший случай — без фильтров (--path-as-is обязателен!)
curl --path-as-is "https://target.com/image?filename=../../../etc/passwd"
# На Windows-целях — обратные слеши
curl --path-as-is "https://target.com/image?filename=..\..\..\..\windows\win.ini"
# Verbose-режим: видно точный URL, уходящий на сервер
curl --path-as-is -v "https://target.com/image?filename=../../../etc/passwd"
Флаг --path-as-is — тот момент, который ломает людям мозг на первом CTF. Без него curl нормализует ../ в URL ещё до отправки запроса, и payload тупо не доходит до сервера. В Burp Suite этой проблемы нет: Repeater отправляет запрос как есть.
Если ответ содержит строки вида root:x:0:0:root:/root:/bin/bash — уязвимость подтверждена. Код 400, 403, 500 или пустое тело — сервер фильтрует ввод.
Apache vs Nginx: поведение при обработке ../ в path. Apache по умолчанию нормализует ../ в URL-пути до передачи запроса приложению — простой traversal через URL-путь часто не проходит даже без фильтра в самом приложении. Nginx при определённых конфигурациях (особенно с proxy_pass без trailing slash) передаёт путь as-is. На практике: если за Nginx стоит Python или Node.js — пробуйте простой ../ первым. Если за Apache — сразу переходите к URL-кодированию.
Фильтр блокирует ../, но не проверяет абсолютные пути? Передайте /etc/passwd напрямую: GET /image?filename=/etc/passwd. Некоторые фреймворки при получении абсолютного пути игнорируют базовый каталог и читают файл из корня файловой системы. В лабораториях PortSwigger Web Security Academy это один из первых уровней — и не зря, потому что в реальности такое тоже попадается.
Работает когда: фильтр ищет только ../ или ..\\, не проверяет начало пути. Не работает когда: приложение принудительно конкатенирует базовый путь через os.path.join() с последующей канонизацией.
Фильтр удаляет ../ из строки однократно, без рекурсии. Payload ....// после удаления внутреннего ../ превращается обратно в ../. Три таких конструкции дают три уровня подъёма: ....//....//....//etc/passwd.
Механика по шагам: фильтр видит ....// → находит внутри ../ → вырезает → остаётся ../. На Windows можно использовать аналогичный трюк с обратным слешем: ....\/....\/....\/.
В Burp Suite Repeater отправляйте GET /image?filename=....//....//....//etc/passwd и сравнивайте длину ответа с обычным запросом. Длина выросла — файл прочитан.
Работает когда: однократный strip последовательности ../. Не работает когда: рекурсивное удаление (пока строка не перестанет меняться) или regex-паттерн \.\.[\\/].
Техника из той самой задачи File Explorer на Fetch the Flag CTF 2022. Точка . кодируется как %2e, слеш / как %2f. Payload: %2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd.
Фильтр видит строку без символов ../ и пропускает. Затем веб-сервер или фреймворк декодирует URL-кодированные символы — в файловую систему попадает уже ../../../etc/passwd.
С точки зрения MITRE ATT&CK это Command Obfuscation (T1027.010) — маскировка payload через кодирование. Серверная сторона выполняет Deobfuscate/Decode (T1140) при обработке запроса. Отправка через curl: curl "https://target.com/image?filename=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd" — curl НЕ декодирует %2e обратно в ., символы дойдут до сервера как есть.
Работает когда: валидация происходит до URL-декодирования. Не работает когда: приложение сначала декодирует, потом проверяет.
Символ % кодируется как %25, поэтому %2e превращается в %252e. Цепочка декодирования: %252e → первое декодирование → %2e → второе декодирование → ..
Payload: %252e%252e%252f%252e%252e%252f%252e%252e%252fetc/passwd.
Типичный сценарий для многоуровневых стеков, где запрос проходит через reverse proxy (Nginx) и затем через приложение (Node.js, Python). Техника работает когда порядок операций: validate → decode → use. Двойное кодирование обходит валидацию, потому что на этапе проверки строка не содержит ничего похожего на ../.
Для CTF-задач повышенной сложности есть и экзотика: overlong UTF-8 (%c0%ae вместо .), 16-bit Unicode (%u002e). Стоит пробовать и %c0%af вместо / — на legacy-системах эти последовательности иногда декодируются в стандартные символы.
Работает когда: запрос проходит через несколько слоёв декодирования. Не работает когда: единая точка обработки.
Приложение проверяет, что путь начинается с ожидаемого каталога — например, /var/www/images/. Без канонизации проверку можно пройти, включив нужный префикс: GET /image?filename=/var/www/images/../../../etc/passwd.
Приложение видит /var/www/images/ в начале и одобряет. Файловая система резолвит ../ и возвращает /etc/passwd. Как узнать ожидаемый каталог: смотрите сообщения об ошибках (часто раскрывают полный путь), исходный код задачи, стандартные пути (/var/www/html/, /opt/app/, /srv/).
Работает когда: проверка startsWith() без последующей канонизации через realpath(). Не работает когда: приложение вызывает getCanonicalPath() или аналог после проверки.
На PHP < 5.3.4 и старых C-based серверах null-byte %00 обрывает строку на уровне файловой системы. Если приложение проверяет расширение файла (только .png, .jpg), payload ../../../etc/passwd%00.png пройдёт проверку, но файловая система проигнорирует всё после %00.
В CTF встречается в задачах, эмулирующих legacy-окружения. На PHP 7+, Node.js, Python 3, Java — null-byte не работает. Если описание задачи намекает на «старый сервер» или указана PHP-версия < 5.4 — null-byte в первую очередь.
Работает когда: PHP < 5.3.4, C-based серверы с null-terminated strings. Не работает когда: любой современный runtime.
| Техника | Преимущества | Ограничения | Когда использовать | Когда НЕ использовать |
|---|---|---|---|---|
| Абсолютный путь | Простейший payload | Не обходит canonicalization | Фильтр ищет только ../ |
Приложение конкатенирует базовый путь |
Вложенные ....// |
Обходит однократный strip | Бесполезен против regex | Strip ../ без рекурсии |
Рекурсивная очистка |
| URL-кодирование | Широко применимо | Не поможет при decode-first | Валидация до декодирования | Canonicalization на входе |
| Двойное кодирование | Обходит многоуровневые стеки | Сложнее в отладке | Архитектура proxy + app | Единая точка обработки |
| Префикс + traversal | Обходит startsWith |
Нужно знать базовый путь | Без realpath после проверки |
После канонизации |
| Null-byte | Обходит проверку расширения | Только legacy | PHP < 5.3.4 | Любой современный runtime |
Алгоритм перебора: начинайте с простого ../../../etc/passwd, при неудаче двигайтесь по таблице сверху вниз. Каждый шаг — 30 секунд в Burp Repeater или один вызов curl.
Repeater — ручная работа. Перехватите запрос с файловым параметром (filename, file, path, template, doc, resource) через Proxy → HTTP history. Отправьте в Repeater (Ctrl+R). Модифицируйте значение параметра, подставляя payload'ы из таблицы выше. Главный индикатор — длина ответа: если при подстановке traversal-payload'а ответ стал длиннее обычного — файл прочитан. Статус-код не всегда информативен: некоторые приложения возвращают 200 даже при ошибке, меняя только тело.
Intruder — автоматизация. Отметьте значение параметра как payload position (кнопка Add §). В Burp Pro подключите встроенный wordlist Fuzzing - path traversal — он содержит URL-кодированные, двойно-кодированные и overlong-UTF-8 последовательности. В Community Edition берите внешний wordlist из PayloadsAllTheThings (Directory Traversal/Intruder/deep_traversal.txt). После запуска атаки сортируйте результаты по длине ответа — аномально длинный ответ сигнализирует об успешном чтении файла.
Path traversal в API. По данным YesWeHack, в современных приложениях traversal чаще встречается в JSON-параметрах, чем в query string. Payload внутри POST-запроса: {"filename":"../../../etc/passwd"}. В Burp Repeater переключите метод на POST, установите Content-Type: application/json и модифицируйте тело. Отдельный вектор — internal API forwarding: если приложение пробрасывает user_id из JSON-тела во внутренний маршрут (/api/v1/users/{user_id}), подстановка ../../admin/roles вместо ID может зарезолвиться в /api/v1/admin/roles. По сути — доступ к привилегированным endpoint'ам через обход маршрутизации.
После подтверждения уязвимости — что читать? В CTF цель — файл flag или flag.txt. На пентесте задача шире.
Linux-системы: /etc/passwd (подтверждение + пользователи), /etc/shadow (хеши, требуют root), /proc/self/environ (переменные окружения — API-ключи, пароли БД), /proc/self/cmdline (аргументы процесса), /proc/version (версия ядра), /proc/net/tcp (активные TCP-соединения для маппинга внутренних сервисов), /home/<user>/.ssh/id_rsa (SSH-ключи).
Файлы приложений: .env (секреты), config.php / settings.py / application.yml (конфигурация с паролями БД, JWT-секретами), /var/log/apache2/access.log или /var/log/nginx/access.log (логи — полезны для log poisoning через LFI).
Cloud и контейнеры: YesWeHack отмечают, что serverless-приложения на AWS Lambda хранят исходный код в /var/task/. Чтение /var/task/index.js или /var/task/handler.py — полный source code disclosure. В Docker стоит проверить /proc/1/cgroup для подтверждения контейнерной среды и /run/secrets/ для Docker secrets.
Windows-цели: C:\Windows\win.ini (подтверждение), C:\inetpub\wwwroot\web.config (конфигурация ASP.NET с connection strings), C:\Windows\System32\config\SAM (хеши, требуют привилегий).
Для автоматизации перебора — ffuf:
# Фаззинг ценных файлов через подтверждённый path traversal
ffuf -u "https://target.com/download?file=../../../FUZZ" \
-w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
-fc 403,404 -fs 0 -mc 200 -t 50
Флаг -fc отсеивает ответы 403/404, -fs 0 убирает пустые, -t 50 ограничивает потоки — в CTF-среде важно не положить сервер. Wordlist LFI-Jhaddix.txt из SecLists содержит более 900 путей к стандартным файлам Linux и Windows. Если traversal работает только с определённым кодированием — адаптируйте wordlist: замените ../ на %2e%2e%2f или ....// по всему файлу через sed.
Path traversal не работает в следующих сценариях — и понимание этого экономит время в CTF:
Канонизация пути. Приложение вызывает realpath() (Python), File.getCanonicalPath() (Java) или path.resolve() (Node.js) с последующей проверкой, что результат начинается с базового каталога. Ни одна техника кодирования это не обходит. Пример из документации PortSwigger:
File file = new File(BASE_DIRECTORY, userInput);
if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
// process file — traversal невозможен
}
Если видите такую конструкцию в исходниках задачи — не тратьте время на traversal, ищите другой вектор.
Whitelist файлов. Приложение принимает только имена из заранее определённого списка (product1.jpg, product2.jpg). Traversal-последовательности не совпадут ни с одним элементом.
Chroot или контейнерная изоляция. Процесс веб-сервера работает в chroot-jail или минимальном контейнере. Даже при успешном traversal файловая система ограничена — /etc/passwd может отсутствовать в контейнере на distroless-образе.
Права файловой системы. Веб-сервер работает от www-data. Файлы с ограниченными правами (например, /etc/shadow с правами 640) не прочитаются без root-привилегий.
WAF с актуальными сигнатурами. Web Application Firewall блокирует большинство кодированных вариантов. Но WAF — защита на уровне транспорта, и при цепочечном кодировании через многоуровневый стек обходы возможны.
filename, file, path, template, page, doc, resource, img — в query string, POST-теле, JSON, заголовках../../../etc/passwd через curl --path-as-is — проверить базовый случай....//, URL-кодирование, двойное кодирование, префикс + traversal, null-byteLFI-Jhaddix.txt.env, config.*, /proc/self/environ для извлечения секретов и эскалации/var/task/ для Lambda-функций и /proc/1/cgroup для Docker..\..\ и обратные слеши наряду с прямымиPath traversal в CTF принято считать «простой» категорией — и это стоит людям очков. Задача без фильтра решается за секунды, но таких на соревнованиях уровня medium и выше уже не осталось. Проблема не в сложности отдельных техник — каждая из шести укладывается в одну строку curl. Проблема в том, что большинство пробуют два-три варианта, упираются и переключаются на другое задание. Прогнать все шесть за две минуты через Burp Intruder — вот что решает.
Но есть момент, который CTF-формат не покрывает: на реальном пентесте path traversal — стартовая точка, а не финишная. Прочитанный .env с паролем от PostgreSQL, JWT-секрет из config.py, SSH-ключ из /home/deploy/.ssh/id_rsa — каждый артефакт открывает следующий вектор. Умение выстроить цепочку от чтения файла до полного компрометирования хоста — то, что отделяет CTF-решателя от пентестера. Изолированные задачи тут бессильны: нужна среда, где вся цепочка проходится от начала до конца. На WAPT эту цепочку проходят в течение нескольких модулей с лабами, и это заметно ближе к реальному пентесту, чем решение отдельных задач.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...