
Ejecución remota de código no autorizada en Magento (CVE-2016-4010)
0x00 Prefacio
El 17 de mayo, el investigador de seguridad Netanel Rubin publicó una vulnerabilidad de ejecución remota de código no autenticada en Magento (CVE-2016-4010). Esta vulnerabilidad engloba en realidad varios fallos menores y permite a un atacante ejecutar código PHP de forma no autenticada en un servidor Magento vulnerable. Magento es una plataforma de comercio electrónico muy popular que fue adquirida por eBay en 2011. Algunas empresas conocidas, como Samsung, Nikon, Lenovo, así como numerosas pequeñas tiendas en línea, la utilizan. Se dice que Magento es usado por 250 000 tiendas en línea, moviendo alrededor de 60 000 millones de dólares al año.
0x01 Análisis
Condiciones para explotar la vulnerabilidad:
La API web de Magento permite dos tipos distintos de RPC: REST RPC y SOAP API. Ambos ofrecen la misma funcionalidad; la única diferencia es que el primero usa JSON y peticiones HTTP para transferir la entrada, mientras que el segundo usa XML.
Para exponer únicamente las API de algunos módulos, Magento ofrece a los desarrolladores un método práctico: declarar en el archivo "webapi.xml" solamente las API de los módulos a los que desean poder acceder. El archivo webapi.xml contiene todas las clases y métodos de las Web API que necesitan ser públicas, y cada método especifica también el permiso concreto que requiere. Estos permisos incluyen:
Por supuesto, esta forma de permitir a los desarrolladores usar el archivo webapi.xml para comunicarse entre el frontend y el backend del sistema (Web API) abre en realidad una puerta trasera directa al núcleo de los módulos.
Además, incluso teniendo el permiso "anonymous", todavía necesitamos una forma de pasar valores dinámicamente. Aquí nos referimos a los distintos objetos que se pueden usar en el sistema; por ejemplo, la función de API "CustomerRepositoryInterface::save()" nos permite usar un objeto "CustomerInterface" en la variable "$customer". El prototipo del código es el siguiente:
interface CustomerRepositoryInterface
{
/**
* Create customer.
*/
public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
}
Entonces, ¿cómo podemos crear objetos usando la interfaz RPC? De hecho, la respuesta a esta pregunta está en cómo Magento configura el servidor SOAP.
Magento usa un servidor SOAP que incorpora por defecto el "SoapServer" de PHP. Para configurarse correctamente, "SoapServer" necesita un archivo WSDL donde se definen todos los métodos, parámetros y los tipos personalizados usados en las peticiones RPC reales. Magento genera distintos archivos WSDL para cada módulo con soporte de funcionalidad XMLRPC, y asigna directamente los valores provenientes del archivo webapi.xml del módulo.
Cuando el servidor procesa una petición RPC, usa los datos del archivo WSDL para determinar si la petición es válida, comprobando el método, los parámetros y los tipos de la petición. Si la petición es válida, pasa el objeto de petición analizado a Magento para su posterior procesamiento. Un punto muy importante es que "SoapServer" no interactúa con Magento de ninguna manera; toda la información sobre los métodos y parámetros de los módulos proviene del archivo WSDL. En ese momento, la petición enviada sigue estando compuesta por matrices anidadas; durante la fase de análisis del SoapServer no se crea ningún objeto. Para crear los objetos necesarios, Magento continúa procesando la entrada por su cuenta.
Para extraer los nombres de los parámetros y los tipos de datos, Magento obtiene el prototipo del método de la petición (ver el código anterior). Para algunos tipos de datos básicos, como cadenas, matrices, booleanos, etc., el sistema asigna la entrada al tipo correspondiente. Pero para los tipos de objeto, la solución es más problemática.
Si el tipo de datos de un parámetro es una instancia de una clase, Magento intentará crear una instancia usando la entrada proporcionada. Recuerde que, en este momento, la entrada es solo un diccionario cuyas claves son nombres de atributos y cuyos valores son valores de atributos.
Primero, Magento creará una nueva instancia de la clase requerida. Luego, intentará rellenarla usando el siguiente método:
Magento procesará de esta manera cada atributo que el usuario esté intentando establecer. Cuando se hayan revisado todos los atributos, Magento considerará que la instancia está configurada y procesará el siguiente parámetro. Cuando todos los parámetros hayan sido procesados de esta forma, Magento ejecutará finalmente el método de la API.
En resumen, Magento le permite crear un objeto, establecer sus propiedades públicas y, finalmente, ejecutar a través de su RPC cualquier método que comience con "Set". Y es precisamente este comportamiento el que dio lugar a la vulnerabilidad en Magento.
La investigación descubrió que algunas llamadas a la API permiten establecer información concreta en el carrito de compra, información que puede ser nuestra dirección de envío, productos e incluso nuestra forma de pago.
Cuando Magento establece nuestra información en la instancia del carrito, usa el método "save" de la instancia para almacenar los datos recién agregados en la base de datos.
A continuación, veamos cómo funciona el método "save":
/**
* 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 se asegura de que nuestro objeto sea válido, luego serializa todas las partes que deben serializarse y las almacena en la base de datos, y finalmente deserializa las partes previamente serializadas.
Parece sencillo, ¿verdad? Pues no lo es. Sigamos viendo cómo determina Magento qué partes deben serializarse.
/**
* 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()