
На недавнем Jeopardy-CTF три команды из десяти зависли на web-таске уровня medium: параметр ?file= принимал имя шаблона, классический ../../../etc/passwd возвращал пустую страницу, и народ переключился на другие категории. А решение заняло два шага — ....//....//....//etc/passwd (вложенная последовательность, которую однократная санитизация не поймала) и php://filter/convert.base64-encode/resource=index.php для вытаскивания кода фильтра. Два payload'а — флаг. Но чтобы до них дойти, нужно понимать механику, а не зубрить шпаргалки.
Path traversal и LFI уязвимости — фундамент web-категории CTF. По данным OWASP, path traversal (CWE-22) входит в категорию A03:2021 Injection и остаётся одним из самых частых классов уязвимостей веб-приложений. В MITRE ATT&CK эксплуатация таких багов мапится на Exploit Public-Facing Application (T1190, Initial Access) — начальный доступ через публично доступный веб-интерфейс. Дальше — чтение файлов (Data from Local System, T1005) и поиск учётных данных (Credentials In Files, T1552.001). Здесь — полный арсенал для CTF-тасков на обход директорий и включение файлов: от распознавания уязвимого параметра до получения RCE через PHP filter chains.
Путаница между этими двумя классами — классика на CTF. Разница принципиальная, и от неё зависит выбор payload'а.
Path traversal (обход директорий) — возможность выйти за пределы разрешённого каталога и прочитать произвольный файл на сервере. Приложение берёт пользовательский ввод для построения пути без валидации. Файл не исполняется, а возвращается как данные. Типичный сценарий: эндпоинт загрузки изображений ?filename=218.png, где замена на ?filename=../../../etc/passwd отдаёт содержимое системного файла.
LFI (Local File Inclusion) — возможность подключить и выполнить локальный файл через include() или require() в PHP. Ключевая разница: содержимое файла интерпретируется как код. Это открывает путь к RCE — через log poisoning, PHP wrappers или filter chains.
Как отличить на практике: если ?file=../../../etc/passwd выводит содержимое файла как текст — перед вами path traversal. Если ?page=../../../var/log/apache2/access.log исполняет PHP-код, который вы ранее внедрили в лог — это LFI. Согласно PayloadsAllTheThings, «File Inclusion will lead to the execution of arbitrary code», тогда как path traversal ограничивается чтением. На CTF обе уязвимости часто встречаются в одном задании — сначала path traversal для разведки, затем LFI для эксплуатации.
Уязвимый параметр — отправная точка. В CTF он обычно сидит в GET-запросе, реже в POST или JSON-теле. Типичные имена параметров для проверки:
?file=, ?page=, ?template=, ?include= — встречаются в подавляющем большинстве тасков?path=, ?doc=, ?folder=, ?style= — реже, но бывают?lang=en — параметр локализации, замена на ?lang=../../etc/passwd часто срабатывает?action=, ?content=, ?mod= — могут маппиться на файловую системуВ API-эндпоинтах параметр прячется в JSON: {"filename": "../../../etc/shadow"} или в GraphQL: readFile(path: "../../../../../.env"). По данным YesWeHack, path traversal часто скрывается в эндпоинтах загрузки и экспорта — /download?file=report.csv, /api/fetch?name=backup.tar.gz, /preview?template=invoice.html.
Признаки уязвимого параметра в CTF-задании: значение похоже на имя файла или путь; при подстановке index или index.php страница меняется; при подстановке несуществующего файла — ошибка 404 или пустой ответ вместо стандартной страницы приложения. Если видите что-то из этого — копайте дальше.
Стартовая точка — два payload'а на каждый подозрительный параметр.
Относительный путь: ?file=../../../etc/passwd — подъём через ../ из текущего каталога до корня файловой системы. Количество ../ можно ставить с запасом: большинство файловых систем схлопывают лишние переходы вверх на уровне корня. Поставили десять штук, а нужно было три — ничего страшного, сработает.
Абсолютный путь: ?file=/etc/passwd — если фильтр проверяет наличие .., но не блокирует абсолютные пути. Согласно PortSwigger Web Security Academy, этот вариант «defeats sanitizers that strip ".." but happily accept absolute paths». На Windows аналоги: ?file=..\..\..\windows\win.ini и ?file=C:\windows\win.ini.
Когда прямой ../ отсекается фильтром — переходим к кодированию. Основные варианты по данным OWASP:
| Payload | Что делает | Когда работает |
|---|---|---|
%2e%2e%2f |
URL-кодирование ../ |
Фильтр проверяет литеральную строку ../ |
%2e%2e/ или ..%2f |
Частичное кодирование | Фильтр проверяет комбинацию .. + / |
%252e%252e%252f |
Двойное кодирование | Сервер декодирует один раз, downstream-компонент — второй |
%c0%af |
Overlong UTF-8 для / |
Legacy IIS, без strict UTF-8 validation |
%ef%bc%8f |
Fullwidth solidus (U+FF0F) | Нормализация NFKC преобразует в / |
..%5c |
URL-кодирование \ |
Windows-серверы, толерантные к backslash |
На практике в CTF начинайте с %2e%2e%2f. Не сработало — двойное кодирование %252e%252e%252f. Эта пара покрывает подавляющее большинство тасков с фильтрацией кодирования.
Если фильтр удаляет ../ из строки однократно — используем вложенные последовательности. Payload ....// после удаления внутреннего ../ превращается в ../. Аналогичная логика у ....\/ с обратным слешем. Payload ..///////..////..//////etc/passwd содержит мусорные слеши, которые файловая система тупо игнорирует.
Обход проверки начала пути: если валидатор требует, чтобы путь начинался с /var/www/images, payload ?file=/var/www/images/../../../etc/passwd пройдёт проверку строки, но файловая система разрешит его в /etc/passwd. Фильтр доволен, мы тоже.
Null byte injection (обход проверки расширения): ?file=../../../etc/passwd%00.png — на PHP до версии 5.3.4 нулевой байт %00 обрезал строку, приписанное расширение .png отбрасывалось. В современном PHP не работает, но CTF-авторы намеренно используют старые рантаймы. Вариант с двойным декодированием: %2500 — прокси декодирует один раз, приложение декодирует в %00.
Path truncation: если длина строки ограничена (в PHP — 4096 байт), можно добавить /./ многократно после имени файла, чтобы суффикс вышел за лимит и был отброшен.
Реальный пример из verified данных: CVE-2010-1470 — directory traversal в компоненте Web TV (com_webtv) 1.0 для Joomla! (CWE-22). EPSS 0.1335 (percentile ~96.2%, то есть top 3.8% по вероятности эксплуатации). Exploit-DB ID: EDB-12166 (автор AntiSecurity, дата публикации 2010-04-12, озаглавлен как «Local File Inclusion»). Payload: index.php?option=com_webtv&controller=../../../../../../../../../../etc/passwd%00. Примечательно, что NVD классифицирует CVE-2010-1470 как directory traversal (чтение произвольных файлов), тогда как Exploit-DB называет PoC «Local File Inclusion» — на практике грань размыта: null byte truncation в include() превращает path traversal примитив в LFI-эффект с возможным исполнением кода. Nuclei-шаблон CVE-2010-1470.yaml из репозитория ProjectDiscovery доступен для автоматической проверки.
PHP wrappers — то, что отличает LFI от простого path traversal. Они позволяют не просто читать файлы, а манипулировать потоком данных на уровне PHP-рантайма.
Самый частый wrapper в CTF. Зачем он нужен: при обычном LFI через include() PHP-файл исполняется — вы видите результат, а не код. Wrapper php://filter читает файл до исполнения, кодируя в base64.
Payload: ?file=php://filter/convert.base64-encode/resource=index.php. Ответ приходит в base64 — декодируете через echo "строка" | base64 -d и получаете исходный код. Это критически важно: исходники раскрывают логику фильтра, захардкоженные пароли, имена скрытых файлов и путь к флагу. По сути, вы вскрываете приложение изнутри.
Вариации: php://filter/read=convert.base64-encode/resource=config.php с явным указанием read=; двойное кодирование php://filter/convert.base64-encode|convert.base64-encode/resource=file для обхода WAF, который фильтрует одинарный base64 в ответе.
Wrapper data:// позволяет передать PHP-код прямо в URL: ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOyA/Pg==. Декодированное значение — <?php system($_GET['c']); ?>. Требует allow_url_include = On и allow_url_fopen = On в php.ini — в реальных системах это редкость, но на CTF встречается.
Wrapper php://input читает тело POST-запроса как поток. Отправляете POST с телом <?php system('cat /flag.txt'); ?> на URL с ?file=php://input — получаете исполнение кода. В отличие от data://, php://input не требует allow_url_include = On, так как это не URL-обёртка для внешних ресурсов, а поток тела запроса — работает даже при allow_url_include = Off. Wrapper expect:// выполняет системные команды напрямую: ?file=expect://id. Требует установленного модуля expect — редкость даже на CTF.
PHP filter chains (техника Synacktiv) — современный вектор LFI-to-RCE без записи на диск и без allow_url_include. Идея: цепочка PHP-фильтров convert.iconv.* позволяет сгенерировать произвольный текст из пустого потока. Выстраивая цепочку из десятков конверсий кодировок, можно сконструировать валидный PHP-код — чистая магия конверсий. Payload выглядит как длинная строка вида php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|...|/resource=php://temp. Для генерации таких цепочек есть готовые инструменты (ищите «php_filter_chain_generator» на GitHub). Этот вектор уже появляется на CTF уровня medium-hard с нарастающей частотой.
Когда PHP wrappers заблокированы или приложение работает не на PHP — есть альтернативные пути к исполнению кода.
Log poisoning через User-Agent. Суть: внедряете PHP-код в лог веб-сервера, затем подключаете лог через LFI. Шаг первый — отправляете запрос с заголовком User-Agent: <?php system($_GET['cmd']); ?>, этот текст попадает в access.log. Шаг второй — подключаете лог через ?file=../../../var/log/apache2/access.log&cmd=cat /flag.txt. Сервер включает лог, PHP-парсер видит ваш код и выполняет его. Типичные пути к логам: /var/log/apache2/access.log (Apache), /var/log/nginx/access.log (Nginx), /var/log/auth.log (SSH — можно отправить имя пользователя с PHP-кодом при попытке входа). Грязный трюк, но работает.
/proc/self — виртуальная файловая система процесса. В Linux /proc/self/ содержит информацию о текущем процессе. Ценные файлы для CTF:
/proc/self/environ — переменные окружения. Часто содержат SECRET_KEY, FLAG, DATABASE_URL. В MITRE ATT&CK это Credentials In Files (T1552.001)/proc/self/cmdline — командная строка запуска процесса. Раскрывает пути к конфигам/proc/self/fd/N — файловые дескрипторы. Перебор N от 0 до 20 может раскрыть открытые файлы, включая загруженные через форму upload (полезно для цепочки upload + LFI)/proc/self/cwd — символическая ссылка на рабочую директорию процессаКомбинация /proc/self/environ + LFI — частый вектор: если переменные окружения содержат HTTP-заголовки (в CGI), можно внедрить PHP-код через заголовок и выполнить его.
Когда обход директорий подтверждён — нужно знать, куда идти. Вот систематизированный список по категориям.
Linux — системные файлы: /etc/passwd — подтверждение уязвимости и список пользователей (T1003.008); /etc/shadow — хеши паролей (требует root, но проверять стоит — мало ли); /etc/hostname и /etc/hosts — информация о сети; /home/<user>/.ssh/id_rsa — приватные SSH-ключи (T1552.004, Private Keys); /home/<user>/.bash_history — история команд (часто содержит пароли в открытом виде); /root/.ssh/id_rsa и /root/.bash_history — то же для root.
Конфигурации веб-приложений: .env — переменные окружения (пароли БД, API-ключи, секреты JWT); config.php, wp-config.php, settings.py, application.yml — конфиги CMS и фреймворков; .git/config и .git/HEAD — метаданные git-репозитория (иногда позволяют восстановить весь исходный код через T1213.003, Code Repositories); composer.json, package.json, requirements.txt — зависимости, раскрывающие стек.
Windows: C:\windows\win.ini — подтверждение уязвимости; C:\windows\system32\config\SAM — хеши паролей (требует привилегии); C:\inetpub\wwwroot\web.config — конфигурация IIS (согласно DISA STIG для IIS 10.0, конфигурационные файлы сервера — приоритетная цель).
Cloud-специфичные пути (по данным YesWeHack): /var/task/index.js — исходный код AWS Lambda; /proc/self/environ — временные security credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) с ограниченным TTL; metadata endpoints через SSRF-цепочку.
В CTF приоритетная последовательность: /etc/passwd (подтверждение) → исходный код приложения через php://filter → .env или конфиг с флагом → /proc/self/environ.
Ручной перебор payload'ов работает на простых тасках. Для заданий с нестандартной фильтрацией нужен фаззинг.
ffuf -u "https://target.com/download?file=FUZZ" \
-w /path/to/lfi-wordlist.txt \
-fc 403,404 -fs 0
Флаг -fc фильтрует ответы по статус-коду (убираем 403 и 404), -fs 0 убирает пустые ответы. Всё, что осталось — потенциально успешные включения.
Wordlist'ы: SecLists → Fuzzing/LFI/ с encoding-вариантами; PayloadsAllTheThings → File Inclusion/ с wrapper-payload'ами; предустановленный список Fuzzing - path traversal в Burp Intruder.
Специализированные инструменты: dotdotpwn — fuzzer для directory traversal, перебирает комбинации кодирований и глубину вложенности; LFISuite — автоматический эксплойтер LFI с поддержкой reverse shell; LFImap — обнаружение и эксплуатация LFI с поддержкой wrapper'ов. На CTF с жёсткими таймерами часто быстрее держать под рукой 15 проверенных payload'ов и прогонять руками в Burp Repeater, чем настраивать автоматизацию. Но для тасков с нестандартными фильтрами ffuf с кастомным wordlist'ом экономит десятки минут.
Когда видите web-таск с подозрением на path traversal или local file inclusion — работаем по алгоритму.
Шаг 1: Разведка параметров. Найдите все GET/POST параметры с именами файлов или путями. Проверьте HTML-исходник — пути к ресурсам (<img src="/loadImage?filename=...") раскрывают структуру каталогов и базовый путь.
Шаг 2: Базовая проверка. Подставьте ../../../etc/passwd и /etc/passwd. Если любой из них вернул содержимое файла — уязвимость подтверждена, переходите к шагу 5.
Шаг 3: Обход фильтров. Если базовые payload'ы не сработали — последовательно пробуйте: URL-кодирование %2e%2e%2f, двойное кодирование %252e%252e%252f, вложенные последовательности ....//, добавление базового пути /var/www/html/../../../etc/passwd, null byte %00 (если подозреваете старый PHP).
Шаг 4: PHP wrappers. Если фильтр блокирует ../, но php:// не фильтруется — используйте php://filter/convert.base64-encode/resource=index.php для чтения исходников. Исходники покажут точную логику фильтра — после этого обход становится тривиальным.
Шаг 5: Определение цели. Нужен ли RCE для получения флага? Если флаг лежит в файле — читайте его напрямую. Если флаг генерируется динамически — переходите к эскалации: log poisoning, data://, php://input, filter chains.
Шаг 6: Сбор ценных файлов. Даже после нахождения основного флага проверьте .env, конфиги приложения, /proc/self/environ — бонусные флаги и дополнительные очки.
Пример уязвимого кода, который встречается в CTF-тасках (по данным OWASP):
<?php
$template = 'blue.php';
if (isset($_COOKIE['TEMPLATE']))
$template = $_COOKIE['TEMPLATE'];
include("/home/users/phpguru/templates/" . $template);
?>
Здесь значение cookie TEMPLATE подставляется в путь без валидации. Запрос с Cookie: TEMPLATE=../../../../../../../../../etc/passwd приведёт к чтению системного файла. Обратите внимание: параметр передаётся не через GET, а через cookie — в CTF-тасках уязвимый параметр может прятаться где угодно.
Девять из десяти CTF-тасков на LFI решаются не перебором payload'ов, а чтением исходного кода через php://filter. Как только вы видите код фильтра — обход очевиден. Инверсия подхода: вместо «угадать bypass» вы сначала получаете полную картину и действуете точно. На соревнованиях с таймером разница — минуты против часов.
Отдельно про PHP filter chains от Synacktiv. Эта техника появляется на CTF всё чаще, и команды, которые умеют ей пользоваться, получают серьёзное преимущество. Суть: превращение «простого» чтения файла в полноценное исполнение кода — без записи на диск, без allow_url_include, без доступа к логам. Если вы решаете web-таски регулярно, потратьте вечер на изучение php_filter_chain_generator — окупится на ближайшем CTF.
Path traversal и LFI — уязвимости не только PHP. Node.js с path.join() без валидации, Python с os.path.join(), Java с File() — все подвержены, если разработчик не проверяет канонический путь после сборки. Согласно YesWeHack, в Node.js «developers often forget to validate the result» после path.resolve(), в Python «path normalisation alone doesn't guarantee security unless the final resolved path has been validated», а в старых Java-приложениях неправильное использование File.getCanonicalPath() ведёт к traversal. В CTF Python/Flask-таски с send_file(request.args.get('file')) встречаются не реже PHP-заданий. Payload'ы те же — ../../../etc/passwd, кодирование, вложенные последовательности. PHP wrappers, разумеется, специфичны для PHP, но базовый traversal через ../ работает идентично на любом стеке.
По моему опыту, главная ошибка новичков в CTF — начинать с автоматизации вместо ручного анализа. Копируют wordlist из SecLists на 10 тысяч строк, запускают ffuf и ждут. А таск решается за 30 секунд подстановкой php://filter в Burp Repeater. Автоматизация — для случаев, когда вы уже понимаете логику фильтра и нужно подобрать конкретный encoding. Не раньше. Команды, которые сначала читают код и строят ментальную модель фильтра, стабильно опережают тех, кто «фаззит и надеется». Это не вопрос инструментов — это вопрос методологии. Если хочешь не просто writeup, а пройти всю атаку самому — на WAPT эту цепочку проходят в течение двух модулей с лабами.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...