Skip to content

Instantly share code, notes, and snippets.

Last active November 29, 2022 17:45
Show Gist options
  • Save jamesbercegay/a8f169059c6184e76b12d98d887542b3 to your computer and use it in GitHub Desktop.
Save jamesbercegay/a8f169059c6184e76b12d98d887542b3 to your computer and use it in GitHub Desktop.
I have done some preliminary research into this bug and so far it does not seem like a backdoor. Just some really weird logic when handling routes, and rendering templates.
As to why widgetConfig[code] executes via a POST request, it is because of the following code located in /includes/vb5/frontend/applicationlight.php
$serverData = array_merge($_GET, $_POST);
if (!empty($this->application['handler']) AND method_exists($this, $this->application['handler']))
$app = $this->application['handler'];
call_user_func(array($this, $app), $serverData);
return true;
$this->application['handler'] is set via some logic in the class constructor that matches the string value of $_REQUEST['routestring'] to an array of "quick routes".
/**Standard constructor. We only access applications through init() **/
protected function __construct()
if (empty($_REQUEST['routestring']))
return false;
if (isset(self::$quickRoutes[$_REQUEST['routestring']]))
$this->application = self::$quickRoutes[$_REQUEST['routestring']];
return true;
foreach (self::$quickRoutePrefixMatch AS $prefix => $route)
if (substr($_REQUEST['routestring'], 0, strlen($prefix)) == $prefix)
$this->application = $route;
return true;
return false;
The quick routes array looks like this:
* @var array Quick routes that match the beginning of the route string
protected static $quickRoutePrefixMatch = array(
'ajax/apidetach' => array(
'handler' => 'handleAjaxApiDetached',
'static' => false,
'requirePost' => true,
), // note, keep this before ajax/api. More specific routes should come before
// less specific ones, to allow the prefix check to work correctly, see constructor.
'ajax/api' => array(
'handler' => 'handleAjaxApi',
'static' => false,
'requirePost' => true,
'ajax/render' => array(
'handler' => 'callRender',
'static' => false,
'requirePost' => true,
After that the handler from the quick routes is executed, and the process of setting up the template rendering begins.
* Renders a template from an ajax call
* @param array Array of server data (from $_POST and/or $_GET, see execute())
protected function callRender($serverData)
$routeInfo = explode('/', $serverData['routestring']);
if (count($routeInfo) < 3)
throw new vB5_Exception_Api('ajax', 'render', array(), 'invalid_request');
$this->router = new vB5_Frontend_Routing();
'action' => 'actionRender',
'arguments' => $serverData,
'template' => $routeInfo[2],
// this use of $_GET appears to be fine,
// since it's setting the route query params
// not sending the data to the template
// render
'queryParameters' => $_GET,
$this->sendAsJson(vB5_Template::staticRenderAjax($routeInfo[2], $serverData));
The template code that is executed is as follows:
<template name="widget_php" templatetype="template" date="1452807873" username="vBulletin" version="5.2.1 Alpha 2"><![CDATA[<vb:if condition="empty($widgetConfig) AND !empty($widgetinstanceid)">
{vb:data widgetConfig, widget, fetchConfig, {vb:raw widgetinstanceid}}
<vb:if condition="!empty($widgetConfig)">
{vb:set widgetid, {vb:raw widgetConfig.widgetid}}
{vb:set widgetinstanceid, {vb:raw widgetConfig.widgetinstanceid}}
<div class="b-module{vb:var widgetConfig.show_at_breakpoints_css_classes} canvas-widget default-widget custom-html-widget" id="widget_{vb:raw widgetinstanceid}" data-widget-id="{vb:raw widgetid}" data-widget-instance-id="{vb:raw widgetinstanceid}">
{vb:template module_title,
widgetConfig={vb:raw widgetConfig},
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:action evaledPHP, bbcode, evalCode, {vb:raw widgetConfig.code}}
{vb:raw $evaledPHP}
<vb:else />
<vb:if condition="$user['can_use_sitebuilder']">
<span class="note">{vb:phrase click_edit_to_config_module}</span>
Here is the backtrace for anyone interested.
0 eval() called at [/var/www/vbulletin/includes/vb5/frontend/controller/bbcode.php:227]
1 vB5_Frontend_Controller_Bbcode::evalCode(debug_print_backtrace();exit;) called at [/var/www/vbulletin/includes/vb5/template/runtime.php:1039]
2 vB5_Template_Runtime::parseAction(bbcode, evalCode, debug_print_backtrace();exit;) called at [/var/www/vbulletin/includes/vb5/template.php(369) : eval()'d code:24]
3 eval() called at [/var/www/vbulletin/includes/vb5/template.php:369]
4 vB5_Template->render(1, 1) called at [/var/www/vbulletin/includes/vb5/template.php:688]
5 vB5_Template::staticRender(widget_php, Array ([routestring] => ajax/render/widget_php,[widgetConfig] => Array ([code] => debug_print_backtrace();exit;)), 1, 1) called at [/var/www/vbulletin/includes/vb5/template.php:701]
6 vB5_Template::staticRenderAjax(widget_php, Array ([routestring] => ajax/render/widget_php,[widgetConfig] => Array ([code] => debug_print_backtrace();exit;))) called at [/var/www/vbulletin/includes/vb5/frontend/applicationlight.php:302]
7 vB5_Frontend_ApplicationLight->callRender(Array ([routestring] => ajax/render/widget_php,[widgetConfig] => Array ([code] => debug_print_backtrace();exit;))) called at [/var/www/vbulletin/includes/vb5/frontend/applicationlight.php:186]
8 vB5_Frontend_ApplicationLight->execute() called at [/var/www/vbulletin/index.php:41]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment