Уязвимости из-за обработки XML-файлов: XXE в C# приложениях в теории и на практике

Уязвимости из-за обработки XML-файлов: XXE в C# приложениях в теории и на практике


Как простая обработка XML-файлов может стать дефектом безопасности? Каким образом блог, развёрнутый на вашей машине, может стать причиной утечки данных? Сегодня мы ответим на эти вопросы и разберём, что такое XXE и как эта уязвимость выглядит в теории и на практике.


0918_XXE_BlogEngine_ru/image1.png



Сразу хочется отметить, что возможных типов уязвимостей, связанных с обработкой XML, несколько. Наиболее популярными, пожалуй, являются XXE, XEE, XPath injection. Сегодня мы поговорим про XXE. Если вам интересно почитать, в чём суть XEE, предлагаю ознакомиться со статьёй "Как Visual Studio 2022 съела 100 Гб памяти и при чём здесь XML бомбы?". До XPath injection доберёмся как-нибудь в следующий раз. :)


Что такое XXE?


XXE (акроним от XML eXternal Entities) – дефект безопасности приложения, который может образоваться в результате парсинга скомпрометированных данных небезопасно сконфигурированным XML-парсером. Последствием атаки может быть, например, раскрытие данных с целевой машины или SSRF (server-side request forgery).


Стандарт XML предусматривает возможность использования DTD (document type definition), описывающего структуру XML-документа. DTD даёт нам возможность определять и использовать XML-сущности.


Выглядеть это может, например, так:


<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE order [
  <!ENTITY myEntity "lol">
]>
<order>&myEntity;</order>


В этом XML мы определили сущность myEntity, которую дальше используем – &myEntity;. В данном случае сущность является внутренней и определена как литерал. Если XML-парсер выполнит раскрытие этой сущности, то вместо &myEntity; будет подставлено фактическое значение – lol. Кроме того, внутренние сущности могут раскрываться через другие. Таким образом могут создаваться XML-бомбы и проводиться XEE-атаки.


Однако сущности могут быть и внешними. Они могут ссылаться на какие-то локальные файлы или обращаться к внешним ресурсам:


<!ENTITY myExternalEntity SYSTEM "https://test.com/target.txt">


Пример XML-файла, в котором внешняя сущность ссылается на локальный файл:


<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE order [
  <!ENTITY myExternalEntity SYSTEM "file:///D:/HelloWorld.cs">
]>
<order>&myExternalEntity;</order>


В данном случае XML-парсер вместо сущности myExternalEntity подставит содержимое файла D:/HelloWorld.cs. При условии, что он сконфигурирован соответствующим образом, конечно.


Суть XXE-атаки заключается в том, чтобы эксплуатировать описанную выше особенность. 


Разберём на примере. Предположим, что есть приложение, которое принимает запросы в виде XML-файлов и обрабатывает товары с соответствующим идентификатором.


Формат XML-файла, с которым работает приложение:


<?xml version="1.0" encoding="utf-8" ?>
<order>
  <itemID>62</itemID>
</order>


Упрощённый C# код:


static void ProcessItemWithID(XmlReader reader, String pathToXmlFile)
{
  ....
  while (reader.Read())
  {
    if (reader.Name == "itemID")
    {
      var itemIdStr = reader.ReadElementContentAsString();
      if (long.TryParse(itemIdStr, out var itemIdValue))
      {
        // Process item with the 'itemIdValue' value
        Console.WriteLine(
          $"An item with the '{itemIdValue}' ID was processed.");
      }
      else
      {
        Console.WriteLine($"{itemIdStr} is not valid 'itemID' value.");
      }
    }
  }
}


Логика тривиальна:


  • если ID является числом, приложение сообщит о том, что соответствующий товар был обработан;
  • если ID не является числом, приложение сообщит об ошибке. 


Соответственно, для приведённого ранее XML-файла приложение распечатает следующую строку:


An item with the '62' ID was processed.


Если вместо номера в ID будет записано что-то другое (например, строка "Hello world"), приложение сообщит об ошибке:


"Hello world" is not valid 'itemID' value.


Если XML-парсер (reader), который обрабатывает файлы, разбирает внешние сущности, возникает дефект безопасности. Ниже представлен XML-файл, с помощью которого можно скомпрометировать приложение:


<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE order [
  <!ENTITY xxe SYSTEM "file:///D:/MySecrets.txt">
]>
<order>
  <itemID>&xxe;</itemID>
</order>


В этом файле объявляется внешняя сущность xxe. При обработке вместо &xxe; будет подставлено содержимое файла D:/MySecrets.txt (например, такое: "This is an XXE attack target."). Соответственно, вывод приложения будет следующим:


"This is an XXE attack target." is not valid 'itemID' value.


Получается, что приложение уязвимо к XXE, если:


  • XML-парсер настроен на разбор внешних сущностей и не обрабатывает их безопасным образом;
  • злоумышленник имеет возможность подать на вход парсеру скомпрометированные данные прямо или косвенно.


Если подставленное значение сущности как-то возвращается обратно злоумышленнику, он может получать содержимое файлов со скомпрометированного устройства. Понятно, что это уже опасно. Дополнительная же опасность состоит в том, что таким образом можно попробовать собрать больше данных о системе в целом и, как следствие, найти другие дефекты безопасности.


Кроме того, XXE может являться мостиком к SSRF-атаке. Суть в том, что у самого хакера может не быть доступа к каким-то ресурсам (ограничение доступа для внешних пользователей), но они могут быть у эксплуатируемого приложения. Так как XXE позволяет выполнять запросы и по сети, скомпрометированное приложение является брешью в защите ресурсов от внешнего мира. 


Говоря про важность и опасность XXE следует вспомнить о том, что этот дефект безопасности часто фигурирует в различных стандартах, топах и перечислениях.


CWE


В Common Weakness Enumeration для XXE есть отдельная запись: CWE-611: Improper Restriction of XML External Entity Reference.


CWE Top 25


Каждый год из CWE отбираются 25 наиболее распространённых и опасных дефектов безопасности, из которых составляется CWE Top 25


В 2021 году XXE потеряла 4 позиции по сравнению с 2020-ым, но всё ещё осталась в топе, обосновавшись на 23-ем месте.


OWASP ASVS


В OWASP ASVS (Application Security Verification Standard) приведён список требований к безопасной разработке. Тему XXE в нём также не обошли стороной: OWASP ASVS 4.0.3 (ID 5.5.2): Verify that the application correctly restricts XML parsers to only use the most restrictive configuration possible and to ensure that unsafe features such as resolving external entities are disabled to prevent XML eXternal Entity (XXE) attacks.


OWASP Top 10


В OWASP Top 10 2017 для XXE была выделена отдельная категория: A4:2017-XML External Entities (XXE). В OWASP Top 10 2021 отдельная категория для XXE была устранена, и теперь XXE входит в категорию A05:2021-Security Misconfiguration.


0918_XXE_BlogEngine_ru/image2.png



Составляющие XXE в C#


Как мы писали выше, для осуществления XXE нужно минимум 2 составляющих: опасно сконфигурированный парсер и данные от злоумышленника, которые он обрабатывает.


Небезопасные данные


В принципе, здесь всё достаточно просто – часто в приложении есть несколько точек, в которых оно принимает внешние данные. Обрабатывать такие данные нужно со всей строгостью, так как не все пользователи используют ваше приложение по назначению и так, как вы задумывали.


Подобными точками входа являются аргументы консольного приложения, различные поля форм, данные запросов и тому подобное. Первое, самое простое и очевидное, что приходит в голову – консольный ввод.


var taintedVar = Console.ReadLine();


Мы не знаем, что хранится в taintedVar – лежат ли там данные в ожидаемом формате или же переменная содержит какую-нибудь строку для компрометации системы. Доверия к ней быть не должно.


Немного более подробно эта тема раскрыта в статье "OWASP, уязвимости и taint анализ в PVS-Studio C#. Смешать, но не взбалтывать" в разделе "Taint sources". Также с подозрением стоит относиться к параметрам публично-доступных методов. Сами по себе данные в них могут быть как безопасными, так и не очень. Про это писали здесь.


XML-парсеры


Для того чтобы парсер был уязвимым к XXE, он должен:


  • обрабатывать DTD;
  • использовать небезопасный XmlResolver.


Если в XML-парсере не задано ограничение на максимальный размер сущностей (или этот размер велик), это может усилить негативные последствия атаки, так как позволит злоумышленнику извлекать данные большего объёма.


Настройки парсинга


Интересующее нас поведение задаётся с помощью следующих свойств: 


  • ProhibitDtd;
  • DtdProcessing;
  • XmlResolver;
  • MaxCharactersFromEntities


В одних XML-парсерах можно встретить все эти опции, в других – только некоторые. Их смысловое значение от типа к типу не изменяется.


ProhibitDtd


Свойство ProhibitDtd декорировано атрибутом Obsolete и на замену ему пришло свойство DtdProcessing. Тем не менее, оно всё так же может использоваться в старом коде. Значение true запрещает обработку DTD, false – разрешает.


DtdProcessing


Свойство DtdProcessing имеет тип System.Xml.DtdProcessing и может принимать значения ProhibitIgnore и Parse:


  • Prohibit – запрещает обработку DTD. В случае, если при разборе XML-файла парсер встретит DTD, будет выброшено исключение типа XmlException;
  • Ignore – парсер просто пропустит DTD;
  • Parse – парсер будет выполнять разбор DTD.


Сразу отвечу на возможный вопрос. Свойства ProhibitDtd и DtdProcessing, если они встречаются вместе (например, в XmlReaderSettings) связаны друг с другом. Так что, даже если вы запретите обработку DTD в одном свойстве и разрешите в другом, актуальной будет только последняя выставленная опция.


XmlResolver


Свойство XmlResolver отвечает за то, какой объект используется для обработки внешних сущностей. Самый безопасный вариант – отсутствие резолвера вовсе (значение null). В таком случае, даже если включена обработка DTD, внешние сущности раскрываться не будут.


MaxCharactersFromEntities


Ещё одна интересующая нас опция – MaxCharactersFromEntities – отвечает за максимально допустимый размер сущностей. Чем больше значение, тем потенциально больший объём информации получится извлечь при проведении XXE-атаки.


Типы XML-парсеров


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


Ещё раз подчеркну, что опасной считается конфигурация парсера, при которой он:


  • обрабатывает DTD;
  • имеет опасный резолвер (например, XmlUrlResolver в его дефолтном состоянии).


XmlReader


За поведение XmlReader отвечает объект XmlReaderSettings, создаваемый явно или неявно. Тип XmlReaderSettings содержит все настройки, перечисленные нами ранее.


Парсер с опасной конфигурацией может выглядеть так:


var settings = new XmlReaderSettings()
{
  DtdProcessing = DtdProcessing.Parse,
  XmlResolver = new XmlUrlResolver(),
  MaxCharactersFromEntities = 0
};

using (var xmlReader = XmlReader.Create(xmlFileStringReader, settings))
  ....


Здесь разработчик явно разрешил обработку DTD, установил резолвер внешних сущностей, ещё и ограничение на их размер снял.


XmlTextReader


В случае с этим типом мы имеем дело всё с теми же знакомыми свойствами: ProhibitDtdDtdProcessingXmlResolver.


Пример парсера с опасной конфигурацией:


using (var xmlTextReader = new XmlTextReader(xmlFileStringReader))
{
  xmlTextReader.XmlResolver = new XmlUrlResolver();
  xmlTextReader.DtdProcessing = DtdProcessing.Parse;
  ....
}


XmlDocument


В случае с типом XmlDocument единственное интересующее нас свойство — XmlResolver. Парсер с опасной конфигурацией в данном случае может выглядеть так:


XmlDocument xmlDoc = new XmlDocument();
xmlDoc.XmlResolver = new XmlUrlResolver();


xmlDoc в такой конфигурации будет раскрывать внешние сущности, а значит, может считаться опасным.


Настройки парсеров по умолчанию


Выше мы рассматривали примеры, где настройки XML-парсеров задавались явно. Однако все перечисленные типы имеют какие-то настройки по умолчанию, и с ними есть пара интересных моментов.


Первый – в разных версиях .NET эти настройки могут отличаться.


Второй – настройки отличаются от типа к типу. Например, где-то обработка DTD по умолчанию будет выключена, где-то – включена.


То есть в определённых случаях XML-парсер может иметь опасную конфигурацию по умолчанию, даже если опасные настройки не прописаны явно.


Если совместить всё это — разные типы парсеров, отличие настроек по умолчанию в разных типах и версиях .NET — получаем неплохой такой объём информации, который может быть сложно удержать в голове (особенно поначалу).


Выходит, смотря только на код, порой нельзя сказать, сконфигурирован ли XML-парсер устойчивым к XXE или нет:


XmlDocument doc = new XmlDocument();
doc.Load(xmlReader);


Из этого кода непонятно, может ли doc обрабатывать внешние сущности или нет – нужно знать информацию о версии фреймворка.


Значения 'опасных' настроек поменялись между версиями .NET Framework 4.5.1 и .NET Framework 4.5.2. Ниже привожу таблицу, в которой указано, в каких версиях .NET парсеры с настройками по умолчанию устойчивы к XXE, в каких – нет.


Экземпляры типов.NET Framework 4.5.1 и ниже.NET Framework 4.5.2 и выше (включая .NET Core и .NET)XmlReader (XmlReaderSettings)SafeSafeXmlTextReaderVulnerableSafeXmlDocumentVulnerableSafe


Да, XmlReader (созданный на основе настроек – XmlReaderSettings) безопасен в .NET Framework 4.5.1 и ниже за счёт того, что в нём выключена обработка DTD.


Несмотря на то, что в новых версиях фреймворка по умолчанию парсеры настроены безопасно, лучшим решением может быть всё же явно прописывать необходимые настройки. Да, кода станет немного больше, но в то же время он будет более очевиден и устойчив при переносе между разными версиями .NET Framework.


Итак, с базовой теорией разобрались. Дальше мы посмотрим на реальную уязвимость, но перед этим предлагаю заварить чашечку кофе.


Пример уязвимости в BlogEngine.NET


Выше мы разобрали теоретическую составляющую XXE, чуть более конкретно поговорили про эти дефекты безопасности в рамках .NET, рассмотрели, как выглядят небезопасные составляющие уязвимости с точки зрения кода. Теперь пришло время перейти к практике. В этом нам поможет BlogEngine.NET.


0918_XXE_BlogEngine_ru/image3.png



Описание проекта с сайтаBlogEngine is an open source blogging platform since 2007. Easily customizable. Many free built-in Themes, Widgets, and Plugins.


Исходный код проекта доступен на GitHub


Для нас этот проект интересен в первую очередь тем, что в нём было найдено целых 3 уязвимости XXE. Все они были исправлены в BlogEngine.NET v3.3.8.0. Значит, мы для экспериментов возьмём предыдущую версию проекта – v3.3.7.0. При желании вы можете повторить описанные шаги и самостоятельно 'пощупать' реальную XXE.


Для начала выгружаем соответствующую версию проекта — v3.3.7.0. Со сборкой проекта никаких проблем возникнуть не должно – она тривиальна. Я собирал через Visual Studio 2022. 


После сборки запускаем на исполнение проект BlogEngine.NET, и если всё удалось, то увидим сайт примерно следующего вида:


0918_XXE_BlogEngine_ru/image4.png



Если сайт не будет доступен для других машин в той же сети "из коробки", я бы посоветовал немножко заморочиться и настроить это – так XXE 'щупать' будет интереснее.


При поиске уязвимостей у вас могут быть разные входные данные. Например, система может представлять для вас чёрный ящик, и тогда нужно будет собирать сведения о системе, искать точки воздействия на неё и тому подобное. Если же система представляет собой белый ящик, это меняет подход и используемые для достижения цели инструменты (или как минимум расширяет их список).


С Open Source проектами интересная штука выходит, как мне кажется. Вроде бы любой заинтересованный может работать с кодом и внести свой вклад в качество / безопасность. Однако с этим есть нюансы. С другой стороны, больше карт в руки попадёт и злоумышленникам – так как есть доступ к исходному коду, находить уязвимости должно быть легче. Вот только будут ли они зарепорчены?


Но оставим эти философские рассуждения и вернёмся к нашему делу.


Так как проект имеет открытый исходный код, воспользуемся этим преимуществом. Для поиска дефектов безопасности кроме собственных знаний вооружимся PVS-Studio (решение для поиска ошибок и дефектов безопасности). Нам понадобится группа диагностик, связанных с security, – OWASP. О том, как включить соответствующие предупреждения, можно почитать здесь.


В Visual Studio достаточно выставить значение "Show All" для группы OWASP на вкладке "Detectable Errors (C#)": Extensions > PVS-Studio > Options > Detectable Errors (C#).


0918_XXE_BlogEngine_ru/image5.png



После этого нужно убедиться, что у вас включено отображение соответствующих предупреждений. В данном случае нас интересуют предупреждения группы 'OWASP' уровня достоверности 'High'. Следовательно, необходимые кнопки должны быть нажаты (обведены рамочкой).


0918_XXE_BlogEngine_ru/image6.png



Далее нужно запустить анализ решения (Extensions > PVS-Studio > Check > Solution) и дождаться результатов.


По CWE (помним, что XXE соответствует CWE-611) или OWASP ASVS ID (OWASP ASVS 5.5.2) легко найти то, что нас интересует, – 3 предупреждения V5614.


0918_XXE_BlogEngine_ru/image7.png



С точки зрения кода проблемы похожи. Мы разберём наиболее интересную (разнесённую по нескольким методам), а по остальным я просто предоставлю основную информацию.


XMLRPCRequest.cs


Предупреждение: V5614 [CWE-611, OWASP-5.5.2] Potential XXE vulnerability inside method. Insecure XML parser is used to process potentially tainted data from the first argument: 'inputXml'. BlogEngine.Core XMLRPCRequest.cs 41


На самом деле анализатор указывает на 3 строки для того, чтобы предупреждение было легче понять: 'опасный' вызов метода, источник потенциально скомпрометированных данных и место их использования опасно сконфигурированным парсером.


public XMLRPCRequest(HttpContext input)
{
  var inputXml = ParseRequest(input);

  // LogMetaWeblogCall(inputXml);
  this.LoadXmlRequest(inputXml); // Loads Method Call 
                                 // and Associated Variables
}


Из сообщения следует, что inputXml может содержать 'заражённые' данные (см. taint checking), которые используются небезопасно сконфигурированным парсером внутри метода LoadXmlRequest. Таким образом здесь получился достаточно комплексный 'межпроцедурный' кейс: данные приходят из одного метода (ParseRequest) и затем передаются в другой (LoadXmlRequest), где уже и используются.


Начнём с данных – для этого нам понадобится код метода ParseRequest.


private static string ParseRequest(HttpContext context)
{
  var buffer = new byte[context.Request.InputStream.Length];

  context.Request.InputStream.Position = 0;
  context.Request.InputStream.Read(buffer, 0, buffer.Length);

  return Encoding.UTF8.GetString(buffer);
}


Сопроводим код трассой распространения заражения, чтобы было более очевидно, о чём речь.


0918_XXE_BlogEngine_ru/image8.png



Начинается всё со свойства context.Request, имеющего тип HttpRequest. Анализатор считает его источником заражения, так как данные, поступившие как запрос, могут быть скомпрометированы.


Есть различные способы эти данные извлечь, и работа с потоком (свойство InputStream) – один из них. Следовательно, taint-статус 'переходит' и на InputStream


Далее для этого потока вызывается метод System.IO.Stream.Read, который считывает данные из потока InputStream в массив байт – buffer. Как следствие, buffer теперь тоже может содержать заражённые данные. 


После этого вызывается метод Encoding.UTF8.GetString, который конструирует строку из массива байт (buffer). Так как исходные данные для создания строки заражены, сама строка также будет заражена. После конструирования строка возвращается из метода.


Выходит, что возвращаемое методом ParseRequest значение может быть скомпрометировано злоумышленником. По крайней мере в теории. 


Вернёмся к исходному методу:


public XMLRPCRequest(HttpContext input)
{
  var inputXml = ParseRequest(input);

  // LogMetaWeblogCall(inputXml);
  this.LoadXmlRequest(inputXml); // Loads Method Call 
                                 // and Associated Variables
}


Мы разобрались с ParseRequest. Предположим, что переменная inputXml действительно может содержать заражённые данные. Следующий шаг – проанализировать метод LoadXmlRequest, принимающий inputXml в качестве аргумента.


Сам метод достаточно большой (больше 100 строк), так что приведём его сокращённую версию и отметим место, на которое указывает анализатор.


private void LoadXmlRequest(string xml)
{
  var request = new XmlDocument();
  try
  {
    if (!(xml.StartsWith("<?xml") || xml.StartsWith("<method")))
    {
      xml = xml.Substring(xml.IndexOf("<?xml"));
    }

    request.LoadXml(xml);              // <=
  }
  catch (Exception ex)
  {
    throw new MetaWeblogException("01", 
                                  $"Invalid XMLRPC Request. ({ex.Message})");
  }
  ....
}


Как видим, аргумент действительно обрабатывается XML-парсером: request.LoadXml(xml). PVS-Studio здесь считает, что request сконфигурирован так, что является уязвимым к XXE. Наша задача – подтвердить это. Или же опровергнуть и тогда отметить предупреждение как false positive. Здесь нам пригодится теория, которую мы разбирали в начале статьи.


Тип объекта, на который указывает ссылка request – XmlDocument. Парсер имеет дефолтные настройки, а значит, нам понадобится узнать используемую версию .NET. Посмотреть её можно в свойствах проекта.


0918_XXE_BlogEngine_ru/image9.png



Report Page