JustPaste.it

Хакер - Кавычкой по «Джумле». Учимся проводить SQL-инъекцию второго порядка на примере уязвимости в Joomla

nopaywall

https://t.me/nopaywall

Joomla — вторая по популярности система управления сайтами после WordPress. Уязвимость, о которой мы поговорим в этой статье, позволяет атакующему без особых проблем одним запросом поднять свои привилегии в CMS до суперпользователя, от которого до выполнения кода один шаг.

Проблема затрагивает все существующие версии Joomla вплоть до 3.8.3. О деталях сообщил исследователь из компании RIPS Technologies. Уязвимость получила идентификатор CVE-2018-6376. В версии 3.8.4 она была исправлена разработчиками CMS, о чем сообщается в анонсе от 30 января.

Проэксплуатировать уязвимость можно, обладая привилегиями менеджера в системе или выше. Сам баг довольно интересный, и его изучение позволит нам поговорить о технике инъекций second order SQL.

 

 

Стенд

Для тестирования эксплоита первым делом готовим стенд. Разумеется, у Joomla есть официальный репозиторий на сайте Docker Hub, где ты можешь найти всевозможные версии CMS. Но использовать их я не буду и подготовлю свой контейнер, поскольку делается это всего несколькими командами.

Сначала запускаем контейнер с Debian 9.

docker run -ti -p80:80 --rm --name=joomlavh --hostname=joomlavh debian /bin/bash

Ставим все необходимое: MySQL, Apache 2 и PHP 7.0.

apt-get update && apt-get install -y mysql-server apache2 php php7.0-xml php7.0-mysqli nano wget

Теперь сама CMS. Нам нужна версия 3.8.3.

cd /var/www/html/ wget https://downloads.joomla.org/cms/joomla3/3-8-3/Joomla_3-8-3-Stable-Full_Package.tar.gz?format=gz tar xzf Joomla* chown -R www-data:www-data .

И последнее: стартуем нужные сервисы и создаем базу данных для будущей установки приложения.

service mysql start && service apache2 start mysql -u root -e "CREATE DATABASE joomla; GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY 'megapass';"

После этого необходимо перейти на свежеподнятый веб-сервер и установить CMS.

Установка Joomla 3.8.3 Установка Joomla 3.8.3

Создай аккаунт с правами Manager, через него будем тестировать эксплоит.

 

Подробнее об уязвимости

Сколько всего порядков SQL-инъекций существует?

  • Два. Инжект первого порядка выполняется сразу, а второго требует дополнительных запросов
  • Выделяют два порядка плюс латеральные инжекты, при которых подменяются значения переменных среды
  • Не ограничено по числу использованных запросов

Из анонса уязвимости можно узнать, что корень проблемы — в отсутствии приведения типов переменной в информационных сообщениях темплейта Hathor (Hathor postinstall message).

Анонс уязвимости в Developer Network Анонс уязвимости в Developer Network

Hathor — это название одного из предустановленных бэкенд-шаблонов административной панели, с которым поставляется Joomla.

Настройка шаблонов панели администрирования в Joomla Настройка шаблонов панели администрирования в Joomla

По умолчанию используется темплейт isis (на всякий случай: он назван в честь богини Исиды, а не запрещенной в РФ организации), но пользователю разрешено вручную менять его в настройках профиля. Находится эта страничка по адресу /administrator/index.php?option=com_admin&view=profile&layout=edit.

Настройка профиля текущего пользователя Настройка профиля текущего пользователя

Теперь настало время зарыться в дебри исходников. Но где искать? Отправную точку, как обычно, ищем в коммите, который решает проблему.

Коммит с патчем для SQLi Коммит с патчем для SQLi

Как видишь, нам необходимо заглянуть в файл hathormessage.php.

/administrator/templates/hathor/postinstall/hathormessage.php
19: function hathormessage_postinstall_condition() 20: { ... 22: $user = JFactory::getUser(); ... 39: // Get the current user admin style 40: $adminstyle = $user->getParam('admin_style', ''); 41: 42: if ($adminstyle != '') 43: { 44: $query = $db->getQuery(true) 45: ->select('template') 46: ->from($db->quoteName('#__template_styles')) 47: ->where($db->quoteName('id') . ' = ' . $adminstyle[0]) 48: ->where($db->quoteName('client_id') . ' = 1'); ... 50: // Get the template name associated to the admin style 51: $template = $db->setquery($query)->loadResult();

После патча переменная $adminstyle приводится к int, значит, она нам и нужна. Строка 40 говорит нам о том, что значение переменной — это результат работы метода getParam из класса User.

/libraries/src/User/User.php
024: class User extends \JObject 025: { ... 318: public function getParam($key, $default = null) 319: { 320: return $this->_params->get($key, $default); 321: }

Параметры ($this->_params) текущего пользователя — это экземпляр класса Registry.

/libraries/src/User/User.php
233: public function __construct($identifier = 0, UserWrapper $userHelper = null) 234: { ... 242: // Create the user parameters object 243: $this->_params = new Registry;

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

Давай включим сниффер и сохраним данные профиля. В пойманном пакете будет атрибут с названием jform[params][admin_style]. Так как в запрос попадает первый элемент массива, то добавляем каноничную кавычку.

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

Теперь переходим на главную страницу контрольной панели и наблюдаем ошибку SQL-синтаксиса.

Ошибка SQL в панели управления Joomla Ошибка SQL в панели управления Joomla

Поставим что-нибудь поосмысленнее:

0 and updatexml(0x3a, concat(1, (select user())), 1)# Успешная SQL-инъекция в Joomla 3.8.3 Успешная SQL-инъекция в Joomla 3.8.3

Вот и SQL-инъекция типа second order!

Почему такая SQL-инъекция называется инъекцией второго порядка?

  • Она происходит в два этапа: сначала выполняется инжект данных, а затем к ним обращаются другим запросом
  • Для ее выполнения требуется две уязвимости
  • В такой инъекции используется обратный (второй) порядок команд

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

Небезопасные данные в базе данных Небезопасные данные в базе данных

Теперь разберемся, как появляются эти сообщения. Заглянем в индексный файл текущего темплейта. В коде вызывается обработчик message.

/administrator/templates/isis/index.php
300: <div class="span12"> ... 302: <jdoc:include type="message" />

Он как раз используется для вывода предупреждений, сообщений об ошибках и отчетов об успешно выполненных действиях.

Дерево DOM-объектов, которые отвечают за сообщения в панели управления Дерево DOM-объектов, которые отвечают за сообщения в панели управления
/administrator/templates/isis/html/layouts/joomla/system/message.php
12: $msgList = $displayData['msgList']; ... 17: <?php if (is_array($msgList) && $msgList) : ?> 18: <?php foreach ($msgList as $type => $msgs) : ?> 19: <div class="alert <?php echo isset($alert[$type]) ? $alert[$type] : 'alert-' . $type; ?>"> ... 20: <button type="button" class="close" data-dismiss="alert">&times;</button> 21: <?php if (!empty($msgs)) : ?> 22: <h4 class="alert-heading"><?php echo JText::_($type); ?></h4> 23: <?php foreach ($msgs as $msg) : ?> 24: <div class="alert-message"><?php echo $msg; ?></div> 25: <?php endforeach; ?> 26: <?php endif; ?>

Данные сообщений берутся из элемента msgList массива $displayData. Этот массив заполняется при вызове метода render класса MessageRenderer, который, как следует из названия, отвечает за формирование HTML-кода для последующего вывода пользователю.

/libraries/src/Document/Renderer/Html/MessageRenderer.php
22: class MessageRenderer extends DocumentRenderer 23: { ... 35: public function render($name, $params = array(), $content = null) 36: { 37: $msgList = $this->getData(); 38: $displayData = array( 39: 'msgList' => $msgList, 40: 'name' => $name, 41: 'params' => $params, 42: 'content' => $content, 43: ); ... 60: return LayoutHelper::render('joomla.system.message', $displayData);

Данные системных сообщений возвращает метод getData.

/libraries/src/Document/Renderer/Html/MessageRenderer.php
70: private function getData() 71: { 72: // Initialise variables. 73: $lists = array(); 74: 75: // Get the message queue 76: $messages = \JFactory::getApplication()->getMessageQueue(); 77: 78: // Build the sorted message list 79: if (is_array($messages) && !empty($messages)) 80: { 81: foreach ($messages as $msg) 82: { 83: if (isset($msg['type']) && isset($msg['message'])) 84: { 85: $lists[$msg['type']][] = $msg['message']; 86: } 87: } 88: } 89: 90: return $lists; 91: }

Сообщения берутся из очереди, которую CMS пытается запросить с помощью метода getMessageQueue.

/libraries/src/Application/CMSApplication.php
465: public function getMessageQueue($clear = false) 466: { 467: // For empty queue, if messages exists in the session, enqueue them. 468: if (!$this->_messageQueue) 469: { 470: $session = \JFactory::getSession(); 471: $sessionQueue = $session->get('application.queue', array()); 472: 473: if ($sessionQueue) 474: { 475: $this->_messageQueue = $sessionQueue; 476: $session->set('application.queue', array()); 477: } 478: } 479: 480: $messageQueue = $this->_messageQueue; 481: 482: if ($clear) 483: { 484: $this->_messageQueue = array(); 485: } 486: 487: return $messageQueue; 488: }

Добавление в очередь производится из разных участков кода на этапе выполнения. Сейчас нас интересует модуль com_cpanel. Он, собственно, и отвечает за работу с административной панелью и вызывается при выполнении метода display из базового контроллера CMS.

/libraries/src/MVC/Controller/BaseController.php
614: public function display($cachable = false, $urlparams = array()) 615: { ... 621: $view = $this->getView($viewName, $viewType, '', array('base_path' => $this->basePath, 'layout' => $viewLayout)); ... 672: $view->display();

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

/administrator/components/com_cpanel/views/cpanel/view.html.php
17: class CpanelViewCpanel extends JViewLegacy 18: { ... 33: public function display($tpl = null) 34: {

Внутри компонента com_cpanel идет выборка сообщений, поступающих после установки.

/administrator/components/com_cpanel/views/cpanel/view.html.php
50: try 51: { 52: $messages_model = FOFModel::getTmpInstance('Messages', 'PostinstallModel')->eid(700); 53: $messages = $messages_model->getItemList(); 54: } 55: catch (RuntimeException $e) 56: { 57: $messages = array(); 58: 59: // Still render the error message from the Exception object 60: JFactory::getApplication()->enqueueMessage($e->getMessage(), 'error'); 61: } 62: 63: $this->postinstall_message_count = count($messages);

Сообщения получаются при помощи надстройки FOF (FrameworkOnFramework). Сначала она получает экземпляр класса PostinstallModelMessages из модуля com_postinstall, а затем, при помощи метода getItemList, выполняется вызов метода buildQuery уже непосредственно в контексте модели PostinstallModelMessages. Этот метод формирует SQL-запрос для выборки сообщений из таблицы.

/libraries/fof/model/model.php
1215: public function &getItemList($overrideLimits = false, $group = '') 1216: { 1217: if (empty($this->list)) 1218: { 1219: $query = $this->buildQuery($overrideLimits);
/administrator/components/com_postinstall/models/messages.php
17: class PostinstallModelMessages extends FOFModel 18: { ... 28: public function buildQuery($overrideLimits = false) 29: { 30: $query = parent::buildQuery($overrideLimits); 31: 32: $db = $this->getDbo(); 33: 34: // Add a forced extension filtering to the list 35: $eid = $this->getState('eid', 700); 36: $query->where($db->qn('extension_id') . ' = ' . $db->q($eid)); 37: 38: // Force filter only enabled messages 39: $published = $this->getState('published', 1, 'int'); 40: $query->where($db->qn('enabled') . ' = ' . $db->q($published)); 41: 42: return $query; 43: }

Затем полученный запрос выполняется и делается выборка из таблицы postinstall_messages. Сам запрос выглядит так:

SELECT `#__postinstall_messages`.* FROM `#__postinstall_messages` WHERE `extension_id` = '700' AND `enabled` = '1'

После получения результатов выборки выполняется метод _getList.

/libraries/fof/model/model.php
1225: $this->list = $this->_getList((string) $query, $limitstart, $limit, $group); 1226: } 1227: else 1228: { 1229: $this->list = $this->_getList((string) $query, 0, 0, $group); 1230: } 1231: } 1232: 1233: return $this->list;

Внутри него происходит вызов onProcessList.

/libraries/fof/model/model.php
1950: protected function &_getList($query, $limitstart = 0, $limit = 0, $group = '') 1951: { 1952: $this->_db->setQuery($query, $limitstart, $limit); 1953: $result = $this->_db->loadObjectList($group); 1954: 1955: $this->onProcessList($result); 1956: 1957: return $result; 1958: }

Метод onProcessList пробегается по всем найденным в базе элементам и выполняет из файла, указанного в поле condition_file, функцию, которая находится в поле condition_method.

Запись из таблицы postinstall_messages. Выделенные поля указывают на файл и функцию, которая будет выполнена Запись из таблицы postinstall_messages. Выделенные поля указывают на файл и функцию, которая будет выполнена
/administrator/components/com_postinstall/models/messages.php
124: protected function onProcessList(&$resultArray) 125: { ... 132: foreach ($resultArray as $key => $item) 133: { 134: // Filter out messages based on dynamically loaded programmatic conditions. 135: if (!empty($item->condition_file) && !empty($item->condition_method)) 136: { 137: jimport('joomla.filesystem.file'); 138: 139: $file = FOFTemplateUtils::parsePath($item->condition_file, true); 140: 141: if (JFile::exists($file)) 142: { 143: require_once $file; 144: 145: $result = call_user_func($item->condition_method);

Вот мы и вернулись к тому, с чего начали, — вызову уязвимой функции hathormessage_postinstall_condition.

 

Демонстрация уязвимости (видео)

 

 

Планируешь ли ты использовать полученную информацию на практике?

  • Да. Спрошу Shodan и Censys, кто еще не обновил Joomla, и попрактикуюсь!
  • Да, сделаю апдейт и заодно проверю свой сайт на другие уязвимости
  • Нет, но захотелось почитать про SQL-инъекции
 

Выводы

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

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

Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall