
Magento Exécution de code à distance non autorisée (CVE-2016-4010)
0x00 Introduction
Le 17 mai, le chercheur en sécurité Netanel Rubin a divulgué une vulnérabilité d'exécution de code à distance non authentifiée dans Magento (CVE-2016-4010). Cette vulnérabilité regroupe en réalité plusieurs failles mineures et permet à un attaquant d'exécuter du code PHP sans authentification sur un serveur Magento vulnérable. Magento est une plateforme e-commerce très populaire, rachetée par eBay en 2011. Des entreprises connues telles que Samsung, Nikon, Lenovo, ainsi que de nombreuses petites boutiques en ligne l'utilisent. On estime que Magento est utilisé par 250 000 boutiques en ligne, représentant un volume d'affaires annuel de 60 milliards de dollars.
0x01 Analyse
Conditions d'exploitation de la vulnérabilité :
L'API web de Magento permet deux types de RPC : REST RPC et SOAP API. Les deux offrent les mêmes fonctionnalités, la seule différence étant que la première utilise JSON et des requêtes HTTP pour transmettre les entrées, tandis que la seconde utilise XML.
Pour n'exposer que certaines parties des modules, Magento offre aux développeurs un moyen pratique : déclarer uniquement les API des modules auxquels ils souhaitent donner accès dans le fichier webapi.xml. Ce fichier contient toutes les classes et méthodes d'API web qui doivent être exposées, et chaque méthode spécifie les droits nécessaires. Ces droits incluent :
anonymous – méthodes accessibles à tout le monde.self – uniquement aux utilisateurs enregistrés et aux administrateurs disposant de droits spécifiques, par exemple Magento_Backend::admin est réservé aux administrateurs pouvant modifier la configuration du serveur.Bien sûr, cette méthode qui permet aux développeurs d'utiliser le fichier webapi.xml pour communiquer entre le front-end et le back-end (API web) ouvre en réalité une porte dérobée directement dans le cœur du module.
De plus, même avec un accès anonymous, nous avons besoin d'un moyen de transmettre dynamiquement des valeurs. Il s'agit d'utiliser différents objets dans le système. Par exemple, la fonction API CustomerRepositoryInterface::save() nous permet d'utiliser un objet CustomerInterface dans la variable $customer. Le prototype du code est le suivant :
interface CustomerRepositoryInterface
{
/**
* Créer un client.
*/
public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
}
Comment créer un objet via l'interface RPC ? En réalité, la réponse réside dans la façon dont Magento configure le serveur SOAP.
Magento utilise le serveur SOAP par défaut fourni avec PHP (SoapServer). Pour une configuration correcte, SoapServer a besoin d'un fichier WSDL qui définit toutes les méthodes, les paramètres et les types personnalisés utilisés dans les requêtes RPC réelles. Magento génère différents fichiers WSDL pour chaque module prenant en charge les fonctionnalités XMLRPC et définit directement les valeurs provenant du fichier webapi.xml du module.
Lorsqu'une requête RPC est analysée par le serveur, celui-ci utilise les données trouvées dans le fichier WSDL pour déterminer si la requête est valide, en vérifiant la méthode, les paramètres et les types. Si la requête est valide, l'objet de requête analysé est transmis à Magento pour un traitement ultérieur. Un point très important : SoapServer n'interagit en aucune façon avec Magento ; toutes les informations sur les méthodes et paramètres du module proviennent du fichier WSDL. À ce stade, la requête est toujours constituée de tableaux imbriqués ; aucun objet n'est créé lors de la phase d'analyse de SoapServer. Pour créer les objets nécessaires, Magento traite lui-même les entrées.
Pour extraire les noms de paramètres et leurs types, Magento récupère le prototype de la méthode de la requête (voir le code précédent). Pour certains types de base (chaîne, tableau, booléen, etc.), le système associe l'entrée au type correspondant. Mais pour les types objet, la résolution est plus complexe.
Si le type de données d'un paramètre est une instance de classe, Magento tente de créer une instance à partir de l'entrée fournie. Rappelons qu'à ce stade, l'entrée n'est qu'un dictionnaire dont les clés sont les noms de propriétés et les valeurs les valeurs des propriétés.
Tout d'abord, Magento crée une nouvelle instance de la classe requise. Ensuite, il tente de la remplir en utilisant la méthode suivante :
Set[Nom], où [Nom] est le nom de la propriété.Magento traite ainsi chaque propriété que l'utilisateur tente de définir. Une fois toutes les propriétés vérifiées, Magento considère l'instance comme prête et traite le paramètre suivant. Lorsque tous les paramètres sont traités, Magento exécute finalement la méthode API.
En résumé, Magento vous permet de créer un objet, de définir ses propriétés publiques, puis d'exécuter n'importe quelle méthode commençant par Set via son RPC. C'est ce comportement qui entraîne la vulnérabilité.
Des recherches ont montré que certains appels API permettent de définir des informations spécifiques dans le panier, comme l'adresse de livraison, les articles, voire le mode de paiement.
Lorsque Magento définit nos informations dans l'instance du panier, il utilise la méthode save de l'instance pour stocker les nouvelles données dans la base de données.
Voyons comment fonctionne la méthode save :
/**
* Enregistrer les données de l'objet
*/
public function save(\Magento\Framework\Model\AbstractModel $object)
{
...
// Si l'objet est valide et peut être enregistré
if ($object->isSaveAllowed()) {
// Sérialiser les champs qui doivent l'être
$this->_serializeFields($object);
...
// Si l'objet existe déjà dans la BD, le mettre à jour
if ($this->isObjectNotNew($object)) {
$this->updateObject($object);
// Sinon, créer un nouvel enregistrement
} else {
$this->saveNewObject($object);
}
// Désérialiser les champs que nous avons sérialisés
$this->unserializeFields($object);
}
...
return $this;
}
// AbstractDb::save()
Magento s'assure que notre objet est valide, sérialise les parties qui doivent l'être et les stocke dans la base de données, puis désérialise les parties précédemment sérialisées.
Cela semble simple, n'est-ce pas ? En réalité, ce n'est pas le cas. Continuons à voir comment Magento détermine quelles parties doivent être sérialisées.
/**
* Sérialiser les champs sérialisables de l'objet
*/
protected function _serializeFields(\Magento\Framework\Model\AbstractModel $object)
{
// Parcourt la propriété '_serializableFields'
// (contenant les champs codés en dur qui doivent être sérialisés)
foreach ($this->_serializableFields as $field => $parameters) {
// Récupère la valeur du champ
$value = $object->getData($field);
// Si c'est un tableau ou un objet, le sérialiser
if (is_array($value) || is_object($value)) {
$object->setData($field, serialize($value));
}
}
}
// AbstractDb::_serializeFields()
Comme nous le voyons, seuls les champs figurant dans le dictionnaire codé en dur _serializableFields peuvent être sérialisés. Le plus important est que cette méthode ne sérialise que si la valeur du champ est un tableau ou un objet.
Examinons maintenant comment Magento détermine quelles parties doivent être désérialisées.