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