参考来源:http://netanelrub.in/2016/05/17/magento-unauthenticated-remote-code-execution/
The vulnerability (CVE-2016-4010) allows an attacker to execute PHP code at the vulnerable Magento server unauthenticated. This vulnerability actually consists of many small vulnerabilities
Magento is an extremely popular eCommerce platform with a 30% share in the eCommerce market.
It is used by major corporations, such as Rosetta Stone, Nike, BevMo and Dyson, and by small online merchants alike. All in all, Magento is used in 250,000 online shops, handling approximately 50 Billion dollars a year.
These statistics, along with the fact Magento stores almost all customer information, makes it an extremely sensitive target, and the main reason I audited it once more.
The vulnerability assumes one of the RPCs (REST or SOAP) is enabled. As both are enabled by default, and one of them is actually required by the system, this assumption will not be a problem in the absolute majority of installations.
In this document I will use the SOAP API, as XML is more readable in this case.
This vulnerability works on both the Community Edition and Enterprise Edition of the system.
I recommend all Magento administrators to update their installations to the 2.0.6 patch.
### Vulnerable Versions
Magento EE & CE before 2.0.6.
### Technical Description
**The Good**
Since the last time I audited Magento’s code, a lot has changed. Most of the code has been re-written, the directories structure changed, and the security mechanisms improved.
So, naturally, I was quite happy auditing this massive codebase again.
The first thing I noticed while going through the code was the vast improvement to the system’s OOP structure. Almost every class in the system implemented at least one interface, inherited from a parent class, and used some advanced magic-method, such as ‘__call()’ or ‘__get()’ to better expose private properties.
Of course, while being a huge step-forward, this extreme example of OOP implementation also complicated things quite a bit – both for me as a researcher, and for Magento developers.
Now, when a developer is writing a new class, it is forced to inherit, or implement, a different class or interface, written by another developer. While this happens quite a lot in the software development world, the security implications of this work-methodology has greatly increased in this particular system – a result of the very dynamic code flow, and, unfortunately, the very dynamic API.
Magento, at its core, is a system built by different “modules”. These so called “modules” are basically different directories containing code for different functionalities the system offers. For example, there’s a module responsible for the payment, one that’s responsible for the virtual cart and one responsible for the customers.
In each module there’s a special directory named “API”. This directory contains all the functionality the module exports to other modules. That way the payment module can communicate with the cart module, the customer module with the authorization module, or the shipping module with the sales module, naming a few examples.
The “API” directory is made out of different PHP files, each containing one PHP class, responsible for exposing some of the module functionality to the rest of the system. Most of the modules API functionality is restricted to other system controlled modules only, as most of it is sensitive.
But, some of the API calls has also been made accessible to another, more user controlled, module – the notorious Web API.
Magento’s Web API is allowing two different RPCs – a REST RPC, and a SOAP API. Both RPCs provide the same functionality, the only difference between the two is that one is using JSON and the HTTP query string to handle its input, while the other uses XML envelopes.
As both are enabled by default, I will use SOAP API in this document as I find it more understandable.
**The Bad**
Without a doubt in my mind, I can safely say Magento features one of the most comprehensive APIs I have ever seen in a PHP CMS, or really any kind of CMS at all.
In order to expose just part of each module’s API, Magento has provided module developers a convenient way of declaring which parts of their module’s API they want to make accessible to the Web API – the “webapi.xml” file.
The “webapi.xml” file contains all the classes and methods exposed to the Web API, in a neat, organized, XML structure. Each exposed method also specifies the specific privilege level it requires.
These privileges can range from “anonymous” – allowing anyone (even guests) to access the method, to “self” – allowing access only to registered customers, and to specific, admin-only, privileges, such as the “Magento_Backend::admin” privilege – allowing access only to administrators with capabilities of editing the server configuration values.
While granting module developers a convenient way of communicating between the front-end of the system and its back-end, the Web API, using the “webapi.xml” file, also opens another door leading directly into the module’s core.
As we will assume the role of guests in the store, we will only be able to access methods requiring the “anonymous” privilege. This will greatly narrow our attack surface, but will remove the requirement for any special preconditions.
Surprisingly, even with “anonymous” privileges we could still use very dynamic input types. I’m not referring to the regular XMLRPC input types alone, such as arrays or base64 decoded strings, I’m also referring to different objects available in the system. For example, the “CustomerRepositoryInterface::save()” API function allows us to use a “CustomerInterface” object in the “$customer” variable, as can be seen in the following prototype:
```
interface CustomerRepositoryInterface
{
/**
* Create customer.
*/
public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
}
```
How can objects be created using the RPC interface? Interestingly enough, the answer to that question relies in how Magento configured their SOAP server.
Magento uses the default SOAP server that is bundled by default with PHP – “SoapServer”. In order to be properly configured, “SoapServer” requires a WSDL file, which describes all the methods, arguments, and custom types used in the particular RPC request.
Magento generates a different WSDL file for every module supporting XMLRPC functionality, setting its data directly from the module’s “webapi.xml” file.
When an RPC request is parsed by the server, the server uses the data found in the WSDL file to decide whether the request is valid or not, checking the requested method, its arguments, and their types. If the request is valid, it passes the parsed request object back to Magento for further parsing.
It is important to note that “SoapServer” does not interact with Magento in any way – all the information it has about the module’s methods and arguments comes from the WSDL file alone.
At this point, the request sent is still made out of nested arrays – no objects were created during SoapServer’s parsing phase.
In order to create the required objects, Magento continues to handle the input itself.
Magento obtains the prototype (can be seen in the previous code example) of the method requested in order to extract its argument names and data types.
For basic cases, such as strings, arrays, booleans and so on, the system just casts the input into the appropriate type. But for objects, the solution is a bit trickier.
If the argument data type is an instance of a class, Magento will try and create that instance using the supplied input. Remember that at this point the input is just a dictionary – its keys are the properties names and values are the properties values.
First, Magneto will create a new instance of the required class. Then, it will try and populate it using the following algorithm:
1. Get a property name (from the input dictionary keys)
2. Look for a public method named “Set[NAME]()”, where [NAME] is the property name
3. If there is such a method, execute it using the property value as an argument.
4. If there isn’t, ignore the property and continue to the next one
Magento will follow this algorithm for every potential property the user is trying to set. When all properties have been checked, Magento will assume the instance is ready and it will move to the next argument.
When all arguments are dealt with, Magento will then finally execute the API method.
Let me clarify that part – Magento lets you create an object, set its public properties, and execute any method starting with the “Set” prefix through its RPC.
Surprisingly, this behavior will be Magento’s downfall.
### The ugly
Some API calls allow us to set specific information in our shopping cart. Such information can be our shipping address, products, and even our payment method.
When Magento sets our information safely inside the shopping cart instance, it uses the “save” function of that instance in order to store the newly inserted data in the database.
Let’s have a look at how the “save” function looks like:
```
/**
* Save object data
*/
public function save(\Magento\Framework\Model\AbstractModel $object)
{
...
// If the object is valid and can be saved
if ($object->isSaveAllowed()) {
// Serialize whatever fields need serializing
$this->_serializeFields($object);
...
// If the object already exists in the DB, update it
if ($this->isObjectNotNew($object)) {
$this->updateObject($object);
// Otherwise, create a new record
} else {
$this->saveNewObject($object);
}
// Unserialize the fields we serialized
$this->unserializeFields($object);
}
...
return $this;
}
// AbstractDb::save()
```
Magento makes sure our object is valid, serializes any fields that should be serialized, stores it in the database, and finally it unserializes the fields it previously serialized.
Sounds simple, right?
Nope.
Let’s have a look at how Magento decides which fields it should serialize:
```
/**
* Serialize serializable fields of the object
*/
protected function _serializeFields(\Magento\Framework\Model\AbstractModel $object)
{
// Loops through the '_serializableFields' property
// (containing hardcoded fields that should be serialized)
foreach ($this->_serializableFields as $field => $parameters) {
// Get the field's value
$value = $object->getData($field);
// If it's an array or an object, serialize it
if (is_array($value) || is_object($value)) {
$object->setData($field, serialize($value));
}
}
}
// AbstractDb::_serializeFields()
```
As can be seen, only fields appearing in the hard-coded dictionary “_serializableFields” can be serialized. On top of that, the method makes sure the field’s value is an array or an object before it continues to serialize it.
Now, let’s have a look at how Magento decided which fields it should **unserialize**:
```
/**
* Unserialize serializeable object fields
*/
public function unserializeFields(\Magento\Framework\Model\AbstractModel $object)
{
// Loops through the '_serializableFields' property
// (containing hardcoded fields that should be serialized)
foreach ($this->_serializableFields as $field => $parameters) {
// Get the field's value
$value = $object->getData($field);
// If it's not an array or an object, unserialize it
if (!is_array($value) && !is_object($value)) {
$object->setData($field, unserialize($value));
}
}
}
// AbstractDb::unserializeFields ()
```
Well, it’s pretty much the same. The only difference is that this time Magento makes sure the field’s value is not an array or an object.
Because of these two checks, we can exploit an object injection attack – simply by setting a regular string to a serializable field.
When we set a regular string to a serializable field, the system will not serialize it before the object is stored in the database, as it is not an object or an array. But when the system will try to unserialize it, right after the database queries has been executed, it will be unserialized, as it is not an object or an array.
This small, almost invisible, condition, makes the difference between a vulnerable and a secure system.
The only questions remaining are which fields are considered “serializable”, and how can we set them.
The first question is easy – we just need to search which classes define the “_serializableFields” property.
Quickly enough, I found several classes that define serializable fields, but none of them could be created using our beloved XMLRPC.
To be honest, one of these classes – “Payment” (which is responsible for gathering information about the payment details), does appear in one of the API methods, but not as an argument, so I couldn’t create or control its instance properties.
On top of that, its serializable field – “additional_information”, can only be set as an array using the regular “Set[PROPERTY_NAME]” technique as an extra security measure, so not only we can’t create it, we wouldn’t be able to set it as a string even if we could.
But it can be set in another tricky way.
When Magento sets the properties of the argument instance, it doesn’t actually set its properties. Instead, it stores them in a dictionary named “_data”. This dictionary is then being used in almost all cases where an instance property is needed.
In our case, this means that our serializable field – “additional_information”, is actually being stored inside that dictionary rather than as a regular property.
As a result, if we could control the “_data” dictionary entirely, we could bypass the “additional_information” field array restriction, as we will set it manually rather than call “Set[PROPERTY_NAME]”.
But how can we control this sensitive dictionary?
Well, one of the things Magento does before saving our “Payment” instance is to edit its properties. Magento does that by treating our API input as payment information that is needed to be stored in the “Payment” instance. This can be seen in the following API method:
```
/**
* Adds a specified payment method to a specified shopping cart.
*/
public function set($cartId, \Magento\Quote\Api\Data\PaymentInterface $method)
{
$quote = $this->quoteRepository->get($cartId); // Get the cart instance
$payment = $quote->getPayment(); // Get the payment instance
// Get the data from the user input
$data = $method->getData();
// Check for additional data
if (isset($data['additional_data'])) {
$data = array_merge($data, (array)$data['additional_data']);
unset($data['additional_data']);
}
// Import the user input to the Payment instance
$payment->importData($data);
...
}
// PaymentMethodManagement::set()
```
As can be seen, the “Payment” data is retrieved with a call to “$method->getData()”, which returns the “_data” property from the “$method” variable. Keep in mind that because “$method” is an argument in an API method, we are in control of it.
When Magneto calls “getData()” on our “$method” argument, the “_data” property of that argument is returned, containing all the payment information we inserted.
Later, it calls “importData()” with our “_data” property as input, replacing the “Payment” instance “_data” property with our “_data” property.
This is a huge step forward. We can now replace the sensitive “_data” property in the “Payment” instance with our user controlled “_data” property, which means we can now set the “additional_information” field.
The problem is that for our unserialize() to work we need that field set to a string, but the “Set[PROPERT_NAME]” method only allows it to be an array.
Surprisingly, though, the solution lies two lines of code before the “importData()” call.
Magento, in its constant effort of becoming the most dynamic CMS ever created, allows developers to add their own payment methods, requiring their own data and information. In order to do that, Magento uses our user-controlled “additional_data” field.
The “additional_data” field is a dictionary containing more data for the payment method, completely controlled by the user. In order for that custom data to be part of the original data, **Magento merges the “additional_data” dictionary with the original “data” dictionary**, effectively allowing the “additional_data” dictionary to override any value in the “data” dictionary, basically letting it completely override it.
This means that now, after the two dictionaries merged, the user controlled “additional_data” dictionary now became the argument’s “_data” dictionary, and because of “importData()”, **it now becomes the “Payment” instance sensitive “_data” property**.
Which means we can now completely control the serializable field “additional_information” and exploit an **Object Injection Attack**.
### And the unserializable
Now, that we can unserialize any string we wanted, it’s time to exploit the Object Injection.
First, we will need an object with a “__wakeup()” or “__destruct()” method, so it will be automatically called when the object is unserialized or destroyed. We require such methods because even though we can control the object properties, we can’t call any of its methods. This is why we have to rely on PHP’s magical methods, which are called automatically when certain events occur.
The first object we will use will be an instance of the “Credis_Client” class, which contains these methods:
```
/*
* Called automaticlly when the object is destrotyed.
*/
public function __destruct()
{
if ($this->closeOnDestruct) {
$this->close();
}
}
/*
* Closes the redis stream.
*/
public function close()
{
if ($this->connected && ! $this->persistent) {
...
$result = $this->redis->close();
}
...
}
// Credis_Client::__destruct(), close()
```
As can be seen, the class does have a simple __”destruct()” method (that will be automatically called by PHP when the object is destroyed), which simply calls the “close()” method.
“close()”, on the other hand, is more interesting – if “close()” recognizes that there is an active connection to a Redis server, it tries to close it by calling the “close()” method on the “redis” property.
Because “unserialize()” allows us to control all the object properties, we control the “redis” property too. Because of that, we can set any object we’d like into that property (not just a Redis one), and affectively, call any “close()” method in any class in the system.
This greatly expands our attack surface. There are several “close()” methods in Magento, and because “close()” methods are usually responsible for terminating streams, closing file handles, and storing object data, we can expect some interesting calls in some of them.
And, as expected, I did find some interesting calls. Let’s have a look at the “close()” method of the “Transaction” class:
```
/**
* Close this transaction
*/
public function close($shouldSave = true)
{
...
if ($shouldSave) {
$this->save();
}
...
}
/**
* Save object data
*/
public function save()
{
$this->_getResource()->save($this);
return $this;
}
// Magento\Sales\Model\Order\Payment\Transaction::__destruct(), close()
```
Both of these methods are pretty simple. The “close()” method calls the “save()” method, which then calls the “save()” method of the “_resource” property.
Using the same logic we used before, because we control the “_resource” property we can control its class too, so we can call any “save()” method in any class we’d like.
That’s a huge step forward. As can be guessed, “save()” methods are usually responsible for storing some kind of data in some kind of storage – the file system, a database, and so on. All we have to do now is look for a “save()” method which uses the file system as its storage utility.
And, quickly enough, I found one:
```
/**
* Try to save configuration cache to file
*/
public function save()
{
...
// save stats
file_put_contents($this->getStatFileName(), $this->getComponents());
...
}
// Magento\Framework\Simplexml\Config\Cache\File::save()
```
All this method does is store what’s inside the “components” field into a file. As the file path is retrieved from the “stat_file_name” field, and because we control these two fields, we effectively control both the file path and its content, **which results in an arbitrary file write vulnerability.**
All we have to do now is find a good path to write our file in, one that has to be writeable by the server and accessible via the web.
One such path is the “/pub” directory, available in all Magento installations. Magento uses that directory in order to store any images or public files an administrator uploaded, and as such, it has to be writeable by the server.
More importantly, because that directory usually stores images, which needs to be retrieved by the browser, it is accessible through the regular web interface.
Finally, we just need to write some PHP code, give our file name a “.php” extension, and we’re good to go. **We’ve executed arbitrary PHP code on the server unauthenticated.**
暂无评论