
Magento Неавторизованное удаленное выполнение кода (CVE-2016-4010)
0x00 Введение
17 мая зарубежный исследователь безопасности Нетанель Рубин (Netanel Rubin) опубликовал информацию об уязвимости удаленного выполнения кода в Magento без авторизации (CVE-2016-4010). Эта уязвимость фактически включает в себя несколько мелких уязвимостей и позволяет атакующему выполнять PHP-код на уязвимом сервере Magento без авторизации. Magento — очень популярная платформа электронной коммерции, которая была приобретена eBay в 2011 году. Её используют такие известные компании, как Samsung, Nikon, Lenovo, а также множество мелких интернет-магазинов. Сообщается, что Magento используется 250 000 онлайн-магазинов, а ежегодный оборот составляет 60 миллиардов долларов США.
0x01 Анализ
Условия эксплуатации уязвимости:
Веб-API Magento поддерживает два типа RPC: REST RPC и SOAP API. Оба предоставляют одинаковую функциональность; единственное различие в том, что первый использует JSON и HTTP-запросы для передачи входных данных, а второй — XML.
Чтобы предоставить доступ только к API части модулей, Magento предлагает разработчикам удобный способ: объявить в файле webapi.xml только те API модулей, к которым они хотят предоставить доступ. Файл webapi.xml содержит все классы и методы веб-API, которые должны быть опубликованы. Для каждого метода также указаны необходимые права доступа. К этим правам относятся:
anonymous — методы, доступные любому.self — доступ только для зарегистрированных пользователей и определённых административных прав, например, право Magento_Backend::admin предоставляет доступ только администраторам, имеющим право редактировать конфигурацию сервера.Конечно, такой способ, позволяющий разработчикам использовать файл webapi.xml для связи между фронтендом и бэкендом системы (веб-API), фактически открывает прямой бэкдор в ядро модуля.
Кроме того, даже имея права anonymous, нам всё равно нужен способ динамической передачи значений. Здесь речь идёт о различных объектах, которые можно использовать в системе. Например, функция API CustomerRepositoryInterface::save() позволяет использовать объект CustomerInterface в переменной $customer. Прототип кода выглядит так:
interface CustomerRepositoryInterface
{
/**
* Create customer.
*/
public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
}
Так как же использовать RPC-интерфейс для создания объектов? На самом деле ответ кроется в том, как Magento настраивает SOAP-сервер.
Magento использует SOAP-сервер, встроенный в PHP (SoapServer). Для правильной настройки SoapServer требуется WSDL-файл, в котором определены все методы, параметры и пользовательские типы, используемые в реальных RPC-запросах. Magento генерирует отдельные WSDL-файлы для каждого модуля, поддерживающего функциональность XMLRPC, и напрямую устанавливает значения из файла webapi.xml модуля.
Когда RPC-запрос обрабатывается сервером, сервер использует данные из WSDL-файла для определения валидности запроса: проверяет метод, параметры и типы. Если запрос корректен, то разобранный объект запроса передаётся Magento для дальнейшего анализа. Важно отметить, что SoapServer никак не взаимодействует с Magento; вся информация о методах и параметрах модуля берётся из WSDL-файла. На этом этапе отправленный запрос по-прежнему представляет собой вложенный массив, и на этапе разбора SoapServer объекты не создаются. Чтобы создать необходимые объекты, Magento продолжает обработку входных данных самостоятельно.
Для извлечения имён параметров и типов данных Magento получает прототип из метода запроса (см. код выше). Для простых типов данных, таких как строки, массивы, булевы значения и т.д., система сопоставляет входные данные с соответствующим типом. Но для объектных типов решение сложнее.
Если тип данных параметра является экземпляром класса, Magento попытается создать экземпляр, используя предоставленные входные данные. Помните, что входные данные — это всего лишь словарь, ключами которого являются имена свойств, а значениями — значения свойств.
Сначала Magento создаёт новый экземпляр требуемого класса. Затем оно пытается заполнить его, используя следующий подход:
Set[Name], где [Name] — имя свойства.Magento будет обрабатывать каждое свойство, которое пытается установить пользователь. Когда все свойства будут проверены, Magento считает экземпляр готовым и переходит к следующему параметру. Когда все параметры будут обработаны таким образом, Magento наконец выполнит метод API.
В итоге, Magento позволяет вам создать объект, установить его публичные свойства и, через его RPC, выполнить любой метод, начинающийся с Set. Именно такое поведение и привело к возникновению уязвимости.
Исследования показали, что некоторые вызовы API позволяют устанавливать определённую информацию в корзине покупок. Эта информация может включать наш почтовый адрес, товары и даже способ оплаты.
Когда Magento устанавливает нашу информацию в экземпляре корзины, оно использует метод save экземпляра для сохранения добавленных данных в базу данных.
Давайте посмотрим, как работает метод save!
/**
* Save object data
*/
public function save(\Magento\Framework\Model\AbstractModel $object)
{
...
// If the object is valid and can be saved
if ($object->isSaveAllowed()) {
// Serialize whatever fields need serializing
$this->_serializeFields($object);
...
// If the object already exists in the DB, update it
if ($this->isObjectNotNew($object)) {
$this->updateObject($object);
// Otherwise, create a new record
} else {
$this->saveNewObject($object);
}
// Unserialize the fields we serialized
$this->unserializeFields($object);
}
...
return $this;
}
// AbstractDb::save()
Magento проверяет, что наш объект корректен, затем сериализует все части, которые должны быть сериализованы, сохраняет в базу данных и, наконец, десериализует ранее сериализованные части.
Выглядит просто, не так ли? На самом деле нет. Давайте посмотрим, как Magento определяет, какие части должны быть сериализованы.
/**
* Serialize serializable fields of the object
*/
protected function _serializeFields(\Magento\Framework\Model\AbstractModel $object)
{
// Loops through the '_serializableFields' property
// (containing hardcoded fields that should be serialized)
foreach ($this->_serializableFields as $field => $parameters) {
// Get the field's value
$value = $object->getData($field);
// If it's an array or an object, serialize it
if (is_array($value) || is_object($value)) {
$object->setData($field, serialize($value));
}
}
}
// AbstractDb::_serializeFields()
Как мы видим, сериализуются только те поля, которые присутствуют в жёстко заданном словаре _serializableFields. Самое главное, этот метод продолжает сериализацию только после проверки того, что значение поля является массивом или объектом.
Теперь посмотрим, как Magento определяет, какие части должны быть десериализованы.
/**
* Unserialize serializeable object fields
*/
public function unserializeFields(\Magento\Framework\Model\AbstractModel $object)
{
// Loops through the '_serializableFields' property
// (containing hardcoded fields that should be serialized)
foreach ($this->_serializableFields as $field => $parameters) {
// Get the field's value
$value = $object->getData($field);
// If it's not an array or an object, unserialize it
if (!is_array($value) && !is_object($value)) {
$object->setData($field, unserialize($value));
}
}
}
// AbstractDb::unserializeFields ()
Выглядит очень похоже. Единственное отличие: на этот раз Magento должно убедиться, что значение поля не является массивом или объектом. Из-за этих двух проверок мы сможем реализовать атаку внедрения объектов, просто установив в сериализуемом поле строку определённого формата. Когда мы это сделаем, система не будет сериализовать это поле перед сохранением объекта в базу данных, поскольку оно не является объектом или массивом. Однако когда система попытается его десериализовать (после выполнения запроса к базе данных), оно будет десериализовано, потому что оно не является ни объектом, ни массивом.
Но именно это почти незаметное условие и создаёт уязвимость. Оставшаяся задача — выяснить, какие поля считаются «сериализуемыми» и как их установить.
Конечно, первый вопрос прост — нужно просто найти класс, содержащий свойство _serializableFields. Вскоре в классе Payment был обнаружен метод API, но не в качестве параметра, поэтому нельзя создать экземпляр или управлять его свойствами. Что ещё важнее, его сериализуемое поле additional_information может быть установлено только как массив, и используется техника Set[PROPERTY_NAME] в качестве дополнительной меры безопасности, так что не только нельзя создать, но и если бы могли, то не смогли бы установить строку.
Но интересно, что его можно установить другим, «хитрым» способом. Когда Magento устанавливает свойства экземпляра параметра, на самом деле оно не устанавливает свойства напрямую, а сохраняет их в словаре с именем _data. Этот словарь используется, когда к свойству экземпляра обращаются. Для нас это означает, что наше сериализуемое поле additional_information фактически сохраняется во внутреннем словаре, а не как обычное свойство.
Поэтому, если мы сможем полностью контролировать словарь _data, мы легко обойдём ограничение массива для поля additional_information, поскольку сможем установить его вручную, не вызывая Set[PROPERTY_NAME].
Но как нам контролировать этот чувствительный словарь?
Перед сохранением нашего экземпляра Payment Magento выполняет одну операцию: редактирует его свойства. Magento интерпретирует наш ввод API как платёжную информацию, которая должна быть сохранена в экземпляре Payment, как показано ниже:
/**
* Adds a specified payment method to a specified shopping cart.
*/
public function set($cartId, \Magento\Quote\Api\Data\PaymentInterface $method)
{
$quote = $this->quoteRepository->get($cartId); // Get the cart instance
$payment = $quote->getPayment(); // Get the payment instance
// Get the data from the user input
$data = $method->getData();
// Check for additional data
if (isset($data['additional_data'])) {
$data = array_merge($data, (array)$data['additional_data']);
unset($data['additional_data']);
}
// Import the user input to the Payment instance
$payment->importData($data);
...
}
// PaymentMethodManagement::set()
Как мы видим, данные Payment извлекаются из параметра $method путём вызова $method->getData(), который возвращает свойство _data. Помните, что поскольку $method является параметром метода API, мы можем его контролировать.
Когда Magento вызывает getData() для нашего параметра $method, возвращается свойство _data параметра, содержащее всю добавленную нами платёжную информацию. Затем, используя _data в качестве входных данных, вызывается importData(), который заменяет свойство _data экземпляра Payment нашим свойством _data. Теперь мы можем использовать контролируемое нами свойство _data для замены чувствительного свойства _data экземпляра Payment, что означает, что теперь мы можем установить поле additional_information.
Чтобы unserialize() сработало, нужно, чтобы поле можно было установить в строку, но метод Set[PROPERTY_NAME] допускает только массивы. Решение находится в двух строках кода перед вызовом importData(). Magento позволяет разработчикам добавлять свои собственные способы оплаты, предоставляя собственные данные и информацию. Для этого Magento использует поле additional_data. Это поле представляет собой словарь, содержащий дополнительные данные метода оплаты, полностью контролируемый пользователем. Чтобы сделать настраиваемый контент частью исходных данных, Magento объединяет словарь additional_data с исходным словарём data, фактически разрешая словарю additional_data перезаписывать любые значения в словаре data, то есть полностью перезаписывать. Это означает, что после слияния двух словарей контролируемый пользователем словарь additional_data становится свойством _data параметра, а из-за importData() он также становится чувствительным свойством _data экземпляра . Другими словами, теперь мы полностью контролируем сериализуемое поле и можем реализовать атаку внедрения объектов.
Поскольку мы можем десериализовать любую нужную нам строку, настало время провести атаку внедрения объектов.
Во-первых, нам нужен объект с методом __wakeup() или __destruct(), чтобы он автоматически вызывался при десериализации или уничтожении объекта. Это связано с тем, что, хотя мы и можем контролировать свойства объекта, мы не можем вызывать его методы. Поэтому мы должны полагаться на магические методы PHP, которые автоматически вызываются при наступлении определённых событий.
Первым объектом, который мы будем использовать, будет экземпляр класса Credis_Client, содержащий следующий метод:
/*
* Called automaticlly when the object is destrotyed.
*/
public function __destruct()
{
if ($this->closeOnDestruct) {
$this->close();
}
}
/*
* Closes the redis stream.
*/
public function close()
{
if ($this->connected && ! $this->persistent) {
...
$result = $this->redis->close();
}
...
}
// Credis_Client::__destruct(), close()
Мы видим, что у этого класса есть простой метод __destruct (который автоматически вызывается PHP при уничтожении объекта), вызывающий метод close(). Интересно, что метод close(), если обнаруживает активное соединение с сервером Redis, вызывает метод close() объекта, хранящегося в свойстве redis, чтобы закрыть его.
Поскольку unserialize() позволяет нам контролировать все свойства объекта, мы можем контролировать и свойство redis. Мы можем установить в это свойство любой объект (не только Redis) и вызвать любой метод close() в любом классе системы. Это значительно расширяет нашу поверхность атаки. В Magento существует множество методов close(), и поскольку они обычно используются для завершения потоков, закрытия файловых дескрипторов и сохранения данных объектов, мы должны найти интересные вызовы.
Как и ожидалось, мы нашли следующий метод close() в классе Transaction:
/**
* Close this transaction
*/
public function close($shouldSave = true)
{
...
if ($shouldSave) {
$this->save();
}
...
}
/**
* Save object data
*/
public function save()
{
$this->_getResource()->save($this);
return $this;
}
// Magento\Sales\Model\Order\Payment\Transaction::__destruct(), close()
Выглядит просто: метод close() вызывает метод save(), который, в свою очередь, вызывает метод save() свойства _resource. По той же логике, поскольку мы контролируем свойство _resource, мы можем контролировать и его класс, следовательно, вызывать любой метод save() любого класса, какой захотим.
Мы сделали большой шаг вперёд. Как и предполагалось, метод save() обычно используется для сохранения различных данных в различных хранилищах (файловая система, база данных и т.д.). Теперь нам нужно найти метод save(), который использует файловую систему в качестве хранилища.
Вскоре я нашёл такой:
/**
* Try to save configuration cache to file
*/
public function save()
{
...
// save stats
file_put_contents($this->getStatFileName(), $this->getComponents());
...
}
// Magento\Framework\Simplexml\Config\Cache\File::save()
Этот метод фактически сохраняет данные поля components в файл. Поскольку путь к файлу берётся из поля stat_file_name, и поскольку мы контролируем оба этих параметра, мы фактически контролируем путь к файлу и его содержимое. Это приводит к уязвимости произвольной записи файлов.
Теперь нам просто нужно найти доступный для записи и доступный через веб-сервер путь для записи файла. Во всех установках Magento есть каталог /pub, который используется для хранения изображений или файлов, загруженных администратором. Это подходящий путь для эксплуатации.
Наконец, мы просто записываем на сервер PHP-веб-шелл и можем выполнять произвольный PHP-код на сервере Magento без авторизации.
0x02 Эксплуатация
Настройка тестовой среды
Примечание: возможные проблемы можно посмотреть здесь:
Эксплуатация уязвимости
Ссылка на публичный эксплойт на exploit-db: https://www.exploit-db.com/exploits/39838/
Метод использования:
Найдите уязвимый сайт на Magento.
Онлайн-проверка версии Magento: http://magentoversion.com/
Добавьте товар в корзину.

Перейдите в корзину и нажмите «Оформить заказ».

Заполните почтовый адрес, проверьте POST-запрос /rest/default/V1/guest-carts/[guestCartId]/shipping-information и получите [guestCartID].


Сохраните эксплойт выше как magento_exp.php и выполните: php magento_exp.php [Magento_URL] [guestCartID] ([путь записи веб-шелла])

Массовое обнаружение
Изучив эксплойт, мы выяснили, что для его использования необходимо выполнение следующих условий:

Поэтому был написан простой скрипт для массовой проверки, который можно использовать вместе с эксплойтом:
#!/usr/bin/env python
import urllib
import sys
import socket
timeout = 5
socket.setdefaulttimeout(timeout)
input = sys.argv[1] # Файл, содержащий URL сайтов на Magento
output = sys.argv[2] # Файл для сохранения результатов, например: output.txt
def logFile(str):
f = open(output,'a')
f.write(str+"\n")
f.close()
def checkVul(url):
try:
html = urllib.urlopen(url).read()
if "guest-carts" in html:
print url,"is vulnerable!"
logFile(url)
else:
print url,"is not vulnerable!"
except Exception:
pass
if __name__ == '__main__':
inp = open(input,'r')
for i in inp:
url=i.strip()
#print url
checkVul(url)
print "All Done!"
Результат выполнения:

0x03 Защита
Обновите Magento до последней версии (2.0.6). Ссылка для скачивания: https://www.magentocommerce.com/download
Ссылки
Paymentadditional_information