Уязвимость vBulletin CVE-2020-17496

Уязвимость vBulletin CVE-2020-17496

Год назад я писал об уязвимости в форуме 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.

Затем запустим контейнер, на котором будет располагаться веб-сервер и сам форум. Не забываем слинковать его с БД.

В качестве сервера я буду использовать Apache. Поэтому установим его и PHP с необходимыми модулями.

Включаем модуль mod-rewrite и запускаем Apache.

Теперь нужно установить vBulletin. Продукт коммерческий, и я здесь не стану рассматривать, как его получить. Все тесты будем проводить на последней уязвимой версии — 5.5.6. Распаковываем ее в директорию /var/www/html и устанавливаем.

Начало установки vBulletin
Начало установки vBulletin

Если ты хочешь вместе со мной более подробно рассмотреть уязвимость и покопаться в сорцах, то неплохо бы настроить отладку. Я буду использовать связку Xdebug + PhpStorm.

Устанавливаем и активируем Xdebug. Делать это лучше после того, как vBulletin будет установлен, у меня были проблемы во время инсталляции, пришлось отключить.

Включаем удаленную отладку и указываем IP-адрес сервера. Обрати на него внимание, а также на пути к файлам — у тебя это все может быть другим.

Теперь перезагружаем веб-сервер.

В PhpStorm включаем ожидание коннекта от отладчика. Добавляем параметр XDEBUG_SESSION_START=phpstorm к запросу, если хотим, чтобы дебаггер сработал.

Включаем прослушивание соединений от Xdebug в PhpStorm
Включаем прослушивание соединений от Xdebug в PhpStorm

Стенд готов, и можно переходить к разбору уязвимости.

Обработка URI

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

.htaccess
Проверяется, существует ли файл, и если нет, то указанный URI передается в качестве параметра routestring.

Как и большинство современных фреймворков, форум поддерживает автозагрузку классов через spl_autoload_register.

index.php
includes/vb5/autoloader.php
Затем начинается проверка переданного роута. Вызывается метод isQuickRoute.

index.php
includes/vb5/frontend/applicationlight.php
В переменной $quickRoutePrefixMatch хранятся префиксы роутов, которые должны обрабатываться при помощи quickRoute.
Проверка роута в vBulletin 5.5.6
Проверка роута в vBulletin 5.5.6

Возвращение к истокам. Работа с виджетами, CVE-2019-16759 и ее патч

Обратимся к эксплоиту для прошлогодней уязвимости CVE-2019-16759.

Здесь в качестве routestring передается ajax/render/widget_php. Префикс как раз подходит под условие quickRoute. После этого вызывается $app->execute().

index.php
Это главный метод, который передает управление на нужные участки кода, чтобы обработать запрос пользователя. В нашем случае вызывается обработчик callRender. Он запускает формирование ответа пользователю.

includes/vb5/frontend/applicationlight.php
Вызов обработчика callRender в vBulletin
Вызов обработчика callRender в vBulletin
includes/vb5/frontend/applicationlight.php
Далее в коде идет первый патч, который исправляет прошлогоднюю RCE.

includes/vb5/frontend/applicationlight.php
Если имя запрошенного шаблона widget_php, то возвращается пустой массив. Пришло время поговорить о виджетах и их шаблонах. В vBulletin есть система виджетов (модулей), которые могут отображать разную информацию на сайте. Таким образом, страница сайта может состоять из некоторого количества таких вот блоков-виджетов со своими стилями и данными. Похожая штука сейчас есть в каждой уважающей себя CMS, так как это удобный и гибкий инструмент кастомизации.

Шаблоны всех виджетов описываются в файле vbulletin-style.xml. При установке форума они записываются в базу данных.

core/install/vbulletin-style.xml
Шаблоны не написаны на чистом PHP, а используют свой синтаксис, который сначала обрабатывается шаблонизатором. Он возвращает результат как строку кода на PHP, который затем проходит процесс «рендеринга». Во время этого данные попадают в функцию eval.

Так вот, среди вороха этих виджетов имеется widget_php. Этот модуль позволяет отображать результаты выполнения произвольного кода на PHP.

core/install/vbulletin-style.xml
Здесь нас встречает еще одно последствие патча уязвимости. Обрати внимание на атрибут version. Это версия последнего обновления шаблона (5.5.5 Alpha 4). До патча часть кода с выполнением PHP выглядела несколько иначе.

vBulletin 5.5.3/core/install/vbulletin-style.xml
Об этом поговорим немного позже, здесь лишь осталось сказать, что с шаблонами работает класс vB_Template.

Теперь возвращаемся к эксплоиту CVE-2019-16759. Предположим, что у нас непатченная версия форума и скрипт выполняется дальше.

includes/vb5/frontend/applicationlight.php
Теперь управление передается в класс vB5_Template. Вызывается метод staticRenderAjax, а из него попадаем в более общий staticRender.

includes/vb5/template.php
Следующий шаг — это сопоставление переменных в шаблоне виджета с теми, что были переданы в запросе пользователем. Напоминаю, что я передавал параметр widgetConfig[code]=system('ls');.

includes/vb5/template.php
Сопоставление параметров из запроса переменным в шаблоне виджета
Сопоставление параметров из запроса переменным в шаблоне виджета
core/install/vbulletin-style.xml
После подгрузки необходимых классов мы попадаем в метод рендеринга шаблона.

includes/vb5/template.php
Здесь мы встречаем очередную часть кода, которая патчит уязвимость, — метод cleanRegistered.

includes/vb5/template.php
includes/vb5/template.php
Здесь из зарегистрированных переменных шаблона удаляется widgetConfig, чтобы нельзя было напрямую из запроса изменять конфигурацию виджета. Как раз через эту переменную я передаю пейлоад на PHP.

Метод cleanRegistered для исправления уязвимости CVE-2019-16759
Метод cleanRegistered для исправления уязвимости CVE-2019-16759

Предположим, что этого метода у нас нет. Дальше инициализируется кеш vBulletin, и управление переходит к getTemplate.

includes/vb5/template.php
includes/vb5/template/cache.php
Этот метод сначала пытается найти уже сгенерированный код шаблона в кеше, и если такового не обнаруживается, то в дело вступает fetchTemplate.

includes/vb5/template/cache.php
Вся магия происходит в этом вызове:
Из псевдокода шаблона получается готовый код на PHP.

includes/api/interface/collapsed.php
core/vb/api/template.php
core/vb/library/template.php
Выполнение метода callApi. Получение шаблона виджета
Выполнение метода callApi. Получение шаблона виджета

В методе fetchBulk шаблон виджета подгружается из базы данных.

Загрузка шаблона виджета widget_php из базы данных
Загрузка шаблона виджета widget_php из базы данных
core/vb/library/template.php
Результат записывается в кеш.

includes/vb5/template/cache.php
В случае с виджетом widget_php прошедший через шаблонизатор код выглядит так.

widget_php_rendered
Сгенерированный шаблон виджета widget_php
Сгенерированный шаблон виджета widget_php

В строке 26 можно увидеть конструкцию vB5_Template_Runtime::evalPhp. Однако до патча эта часть кода выглядела несколько иначе. Как я упоминал, сам шаблон виджета имел другой вид.

vBulletin 5.5.3/core/install/vbulletin-style.xml
Эта конструкция обрабатывалась контроллером vB5_Frontend_Controller_Bbcode. В итоге вызывался обычный eval.

vBulletin 5.5.3/includes/vb5/frontend/controller/bbcode.php
В новой версии форума разработчики пересмотрели логику работы виджета. Добавили другой метод — vB5_Template_Runtime::evalPhp, который, по сути, также выполняет код, переданный в параметре widgetConfig['code'], с той лишь разницей, что сначала проверяет имя шаблона, где происходит попытка вызвать метод. И если он отличается от widget_php, то возвращается пустая строка.

includes/vb5/template/runtime.php
Такое решение должно усилить безопасность и запретить любым другим шаблонам передавать потенциально небезопасные данные в функцию eval.

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

includes/vb5/template.php
Выполнение PHP-кода шаблона widget_php
Выполнение PHP-кода шаблона widget_php

Пейлоад отрабатывает, и в ответе от сервера можно видеть результат выполнения функции system('ls').

Успешная эксплуатация уязвимости CVE-2019-16759 в vBulletin
Успешная эксплуатация уязвимости CVE-2019-16759 в vBulletin

Таким образом, возможность выполнять код через widget_php осталась, только теперь атакующий не может делать это напрямую. Это приводит нас к поиску обходных путей и новой уязвимости.

Детали CVE-2020-17496

Хочу обратить внимание, что виджеты не только могут не быть самостоятельными элементами, но и бывают вложенными. В один виджет может быть вложено несколько дочерних. То есть можно обрабатывать и отображать результаты работы других виджетов. Такая логика работы отлично вписывается в идею обхода ограничений, которые были добавлены патчем для CVE-2019-16759. Нужно только найти виджет, в шаблоне которого будет возможность вызывать дочерние. И Амир обнаружил такой — widget_tabbedcontainer_tab_panel.

core/install/vbulletin-style.xml
Виджет обрабатывает массив subWidgets, в котором ищет ключ template и подгружает шаблон указанного в нем виджета.

core/install/vbulletin-style.xml
А с помощью ключа config можно передавать параметры в дочерний шаблон (обрати внимание на атрибут widgetConfig).

core/install/vbulletin-style.xml
Давай проверим это на каком-нибудь простеньком виджете.

core/install/vbulletin-style.xml
Здесь в качестве параметра можно передать view_all_text. Этот текст будет отображен в шаблоне как текст ссылки. Отправляем запрос.
При рендеринге widget_tabbedcontainer_tab_panel в том месте, где будет дочерний виджет, вставляется плейсхолдер. Шаблон приобретает следующий вид.
Затем вызывается метод replacePlaceholders, который, как видно из названия, проходит по шаблону, ищет плейсхолдеры, вызывает необходимые модули и вставляет результаты их работы в нужное место.

includes/vb5/template.php
Рендеринг вложенных виджетов в шаблоне
Рендеринг вложенных виджетов в шаблоне

Здесь используется точно такой же набор вызовов. Метод fetchTemplate получает шаблон виджета.

includes/vb5/template/cache.php
Затем в него передаются переменные. Так параметры из нашего POST-запроса попадают в шаблон.

includes/vb5/template/cache.php
Передача переменных в дочерний шаблон
Передача переменных в дочерний шаблон

И снова рендеринг, но уже дочернего модуля.

includes/vb5/template/cache.php
Таким образом, та часть патча, где проверяется имя модуля, остается далеко позади.

includes/vb5/frontend/applicationlight.php
Но это еще не все, так как в этот раз и в ветку с методом cleanRegistered мы не попадаем. Это происходит из-за того, что вызов render был инициирован не родительским методом и переменная isParentTemplate установлена в false.

При рендеринге дочернего виджета cleanRegistered не вызывается и переменная widgetConfig не очищается
При рендеринге дочернего виджета cleanRegistered не вызывается и переменная widgetConfig не очищается
includes/vb5/template.php
Это значит, что widgetConfig будет в целости и сохранности. Еще одна часть фикса уязвимости миновала.

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

includes/vb5/template/cache.php
Дочерний виджет добавляется в родительский
Дочерний виджет добавляется в родительский

На выходе получается что-то вроде такого:

Вызов произвольного дочернего виджета из родительского в vBulletin
Вызов произвольного дочернего виджета из родительского в vBulletin

Но это всё игрушки. Теперь пора взяться за серьезные вещи. Берем прошлогодний эксплоит и переделываем его прямой вызов на дочерний другого виджета.

Отправляем полученный результат на widget_tabbedcontainer_tab_panel.

В ответ получаем результат выполненной на сервере команды.

Успешная эксплуатация RCE в vBulletin
Успешная эксплуатация RCE в vBulletin

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

Выводы

Сегодня мы затронули разные аспекты работы форумного движка vBulletin. Посмотрели на реализацию механизма виджетов и на их слабые стороны. На самом деле текущая реализация вызывает много вопросов с точки зрения безопасности. Парсинг псевдокода в PHP и выполнение его через функцию eval создает много потенциально узких мест. Например, любая неотфильтрованная или некорректно отфильтрованная переменная в шаблоне приведет к еще одной RCE. Нужно внимательно следить за корректностью формирования кода шаблона, фильтрация XSS превращается в настоящую головную боль.

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

Как временную меру могу посоветовать отключить рендеринг PHP в виджетах. Как ты, возможно, заметил, в шаблоне встречалась проверка опции disable_php_rendering.

Для этого нужно зайти панель администратора, в раздел основных настроек, и включить опцию Disable PHP, Static HTML, and Ad Module rendering.

Временная мера для CVE-2020-17496. Отключение модулей, исполняющих PHP-код
Временная мера для CVE-2020-17496. Отключение модулей, исполняющих PHP-код

Это, конечно, может поломать что-то на твоем форуме, зато его не поломает кто-то со стороны. По крайней мере, не с помощью этого эксплоита!

ВКонтакте
OK
Telegram
WhatsApp
Viber

Добавить комментарий

Ваш адрес email не будет опубликован.