
Magento Unauthorized Remote Code Execution (CVE-2016-4010)
0x00 Preface
On May 17, foreign security researcher Netanel Rubin publicly disclosed an unauthenticated remote code execution vulnerability in Magento (CVE-2016-4010). This vulnerability actually consists of multiple smaller flaws and allows an attacker to execute arbitrary PHP code on a vulnerable Magento server without authentication. Magento is a very popular e-commerce platform, acquired by eBay in 2011. Some well-known companies such as Samsung, Nikon, Lenovo, and many small e-commerce sites use it. It is reported that Magento is used by over 250,000 online stores, handling transactions worth approximately $60 billion annually.
0x01 Analysis
Prerequisites for exploiting this vulnerability:
Magento's web API allows two types of RPCs: REST RPC and SOAP API. Both offer the same functionality, with the only difference being that the former uses JSON and HTTP requests to pass input, while the latter uses XML.
To expose only certain module APIs, Magento provides developers with a convenient method: declare only the APIs they want to make accessible in the "webapi.xml" file. The webapi.xml file contains all the classes and methods for the Web APIs that need to be exposed, and each method also specifies the required permission. These permissions include:
Of course, this approach, which allows developers to use the webapi.xml file to communicate between the system's frontend and backend (Web API), essentially opens a backdoor directly into the module core.
Additionally, even with "anonymous" permission, we still need a way to dynamically pass values. For example, the "CustomerRepositoryInterface::save()" API function allows us to use a "CustomerInterface" object in the "$customer" variable. The code prototype is as follows:
interface CustomerRepositoryInterface
{
/**
* Create customer.
*/
public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
}
So how can we create an object using the RPC interface? In fact, the answer lies in how Magento configures the SOAP server.
Magento uses the PHP "SoapServer" bundled by default. To be properly configured, "SoapServer" requires a WSDL file that defines all methods, parameters, and custom types used in actual RPC requests. Magento generates different WSDL files for each module that supports XMLRPC functionality and directly sets values 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 determine if the request is valid, checking the request's method, parameters, and types. If the request is valid, the parsed request object is passed to Magento for further processing. One very important point: the "SoapServer" does not interact with Magento in any way; all information about the module's methods and parameters comes from the WSDL file. At this point, the sent request still consists of nested arrays, and no objects are created during the SoapServer parsing phase. To create the required objects, Magento handles the input itself.
To extract parameter names and data types, Magento retrieves the prototype from the request's method (see the code above). For basic data types such as strings, arrays, booleans, etc., the system maps the input to the corresponding type. However, for object types, the resolution is more complicated.
If the parameter's data type is a class instance, Magento will attempt to create an instance using the provided input. Remember, the input at this point is just a dictionary, with keys as property names and values as property values.
First, Magento creates a new instance of the required class. Then, it tries to populate it using the following method:
Magento processes each property the user is trying to set in this manner. Once all properties have been checked, Magento considers the instance fully set and moves to the next parameter. After all parameters have been processed, Magento finally executes the API method.
In summary, Magento allows you to create an object, set its public properties, and then execute any method starting with "Set" through its RPC. And it is this behavior that leads to the vulnerability.
Research found that some API calls allow setting specific information in a shopping cart, such as shipping addresses, products, or even payment methods.
When Magento sets our information in the cart instance, it uses the instance's "save" method to store the newly added data in the database.
Let's take a look at how the "save" method works!
/**
* 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 ensures our object is valid, then serializes all parts that should be serialized and stores them in the database, and finally unserializes the parts that were serialized earlier.
Seems simple, right? Actually, not so. Let's continue to see how Magento determines which parts should be serialized.
/**
* 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 we can see, only those fields present in the hardcoded dictionary "_serializableFields" can be serialized. Most importantly, this method continues to serialize only after ensuring the field's value is an array or an object.
Now, let's see how Magento determines which parts should be unserialized.