En août 2019, une vulnérabilité de sécurité dans Magento affectant les versions 2.3.2, 2.3.3 et 2.3.4 a été signalée en utilisant la plateforme HackerOne bug bounty .

Le bogue a eu un impact sur certaines installations de Magento et il nous a permis d’obtenir l’exécution de code à distance en fonction de la façon dont les fichiers PHAR sont désérialisés et en abusant des directives de protocole de Magento.

La vulnérabilité a été corrigée par Adobe le 28 avril 2020. Au moment de la rédaction de cet article, le rapport de vulnérabilité n’a pas encore été rendu public.

Étant donné que cette vulnérabilité peut être exploitée par des utilisateurs avec n’importe quel niveau de privilège, elle a un impact de risque commercial élevé sur les déploiements non corrigés.

1. Analyse de vulnérabilité

Creusons dans les détails! Ci-dessous, nous passerons en revue les versions et les instances de Magento concernées, ce qui a causé la vulnérabilité et comment vous pouvez l’exploiter pour réaliser l’exécution de code à distance.

TL; DR

Pour mieux suivre les étapes suivantes, voici un résumé de la séquence d’attaque :

1. Toutes les instances Magento 2.3.2, 2.3.3 et 2.3.4 ne sont pas affectées. Depuis la version 2.3.1, la prise en charge de PHAR est désactivée par défaut. Dans une installation Magento par défaut, vous pouvez vérifier si le support PHAR est désactivé sous app/bootstrap.php . Si vous trouvez l’extrait de code suivant, votre installation Magento n’est pas affectée :

if (in_array('phar', \stream_get_wrappers())) {
    stream_wrapper_unregister('phar');
}

2. La vulnérabilité réside dans l’interface d’administration et vous pouvez la déclencher en ajoutant une nouvelle image à une page. Les archives PHAR peuvent être intégrées dans des fichiers JPEG – continuez à lire pour voir comment. Cette image sera passée à un getimagesize() qui déclenchera la désérialisation si le nom du fichier commence par « phar:// »

3. Dans ce cas, ce n’est pas le cas et nous devions trouver un moyen de contourner cela. Dans le front-end, l’image src est définie sur {{media url= filename }}. Cela déterminera l’image à traiter par la directive médias de Magento . Le bogue que nous avons trouvé nous a permis de changer le src pour accéder à une autre directive. Nous avons donc utilisé la directive Protocol qui nous a permis d’avoir un contrôle total sur le nom du fichier.

4. Il ne restait plus qu’à trouver des classes pouvant être utilisées dans une chaîne POP pour réaliser RCE. Nous avons utilisé GuzzleHttp/Stream/FnStream et phpseclib\Crypt\Hash .

Contexte : Intégration de PHAR dans les JPEG

L’attaque nécessite de pouvoir placer une archive PHAR sur le serveur, car le wrapper n’est pris en compte que si la ressource à laquelle il fait référence est locale.

Comme vous le savez probablement, les téléchargements de fichiers sont soumis à toutes sortes de limitations. Lorsque vous essayez d’exploiter ce type de vulnérabilité, il est utile de pouvoir créer des fichiers valides à la fois en tant qu’archives PHAR et d’un autre type de fichier, comme JPEG.

Vous pouvez stocker les PHAR dans trois formats de fichiers : phar, tar et zip. Pour cet exemple, examinons le format TAR, car il offre une certaine flexibilité que vous pouvez utiliser.

Utilisons les propriétés suivantes de phar, tar et jpeg pour créer des fichiers polyglottes TAR/JPEG valides :

  • jpeg : vous pouvez insérer des commentaires de longueur arbitraire dans les méta-données
  • tar : les 100 premiers octets de l’archive contiennent le nom du premier fichier de l’archive ; la fin de l’archive est marquée par 1024 0 octets consécutifs
  • phar : n’importe quelle quantité de données peut être préfixée au début du stub.

Vous pouvez ensuite intégrer une archive PHAR dans un JPEG :

  1. Démarrez l’archive avec le marqueur de début JPEG 0xFFD8 , suivi d’un début de commentaire 0xFFFE et de la longueur du commentaire.
  2. Ensuite, suivez le commentaire réel, qui sera notre archive phar, plus 1024 zéros pour marquer la fin de l’archive. Par conséquent, la longueur du commentaire est égale à la longueur du contenu de l’archive + 1024.
  3. Les données d’image viennent après cela.

Un hypothétique polyglotte JPG/PHAR aurait cette structure :

0xFFD8 | 0xFFFE | comment_length | PHAR_données | archive_end | image_data

Analyse pratique

La vulnérabilité réside dans le composant responsable du rendu des images dans l’éditeur WYSIWYG de Magento.

L’éditeur a deux états, affiché et masqué . Chaque fois que vous passez de masqué à affiché , une requête GET est effectuée vers un point de terminaison pour chaque image de la page. Il ressemble à ceci :

magento.url/admin/cms/wysiwyg/directive/___directive/ base64(image.src) /…

Dans le backend, cela atteint vendor/magento/module-cms/Controller/

Adminhtml/wysiwyg/Directive.php.

public function execute()

{

      $directive = $this->getRequest()->getParam('___directive');

      $directive = $this->urlDecoder->decode($directive);

[...snip...]

try   {

                      $filter = $this->_objectManager->create(Filter::class);

                      $imagePath = $filter->filter($directive);

                      $image = …

                       [...snip...]

                       $image->open($imagePath);

                       [...snip...]

      }

}

L’image codée en base64 src mentionnée précédemment est considérée comme la valeur de la variable $directive .

Par défaut, ce src aura la forme {{media url=image-name}} . Ici, les médias sont ce que nous appellerons la directive .

Ceci est important car la prochaine étape dans Directive.php consiste à appliquer un filtrage à __directive : $imagePath = $filter->filter($directive);

Le résultat de ce filtrage sera un chemin d’image dont la valeur dépend de la directive utilisée dans le src. Nous y reviendrons dans un instant.

Après avoir obtenu le chemin de l’image, un nouvel objet Image est créé. En même temps, l’image est ouverte. La fonction qui gère l’ouverture fait un appel à getimagesize(imagePath) qui va désérialiser une archive PHAR .

Passons en revue un exemple pour voir comment cela se passe :

  1. Téléchargez une nouvelle image avec le nom phar.jpg sur notre page Web Magento.
  2. Ajoutez l’image en tant que :
  3. Appuyez sur le bouton pour afficher l’éditeur WYSIWYG. Dans le backend, Directive.php recevra {{media url= »phar.jpg »}} .
  4. Celui-ci sera transmis à la fonction de filtrage, ce qui donnera imagePath .
  5. Utilisez le chemin de l’image que vous obtenez dans l’appel à $image->open($imagePath) ; dans ce cas, $imagePath sera pub/media/phar.jpg , où pub/media est l’emplacement multimédia par défaut dans Magento.

Pour désérialiser le PHAR, vous devez contrôler le début du nom passé à getimagesize() . Comme indiqué dans notre guide technique sur la désérialisation PHAR , la désérialisation ne fonctionne que si le nom du fichier ressemble à « phar://… ».

Vous devez donc trouver un moyen d’obtenir le contrôle total de l’ imagePath résultant .

Directive Protocole

En revenant à Directive.php , nous savons que le chemin de l’image est le résultat de l’appel à $imagePath = $filter->filter($directive) .

L’objet $filter est une instance de la classe Magento\\Cms\\Model\\Template\\Filter, qui – à son tour – hérite de la classe Magento\\Email\\Model\\Template\\Filter. Conservez ces détails car ils vous seront utiles plus tard.

La méthode de filtrage essaiera de faire correspondre $directive à une expression régulière spécifique. En fonction de la correspondance, il effectuera un rappel vers une méthode basée sur la directive présente dans $directive .

Dans le scénario par défaut, avec {{media url=”phar.jpg”}} comme entrée, un appel sera fait à mediaDirective . Cette méthode appartient à Magento\\Cms\\Model\\Template\\Filter . Comme mentionné précédemment, cette classe hérite de Magento\\Email\\Model\\Template\\Filter , qui contient heureusement plus de méthodes directives.

Cela signifie que vous pouvez modifier l’entrée pour accéder à une autre directive. Pour cette analyse approfondie, nous avons utilisé protocolDirective que vous pouvez utiliser pour contrôler le nom résultant de l’image. Pour accéder à cette directive, vous devez fournir une entrée au format suivant {{protocol …}}

Regardons un extrait de code pour cette méthode :

public function protocolDirective($construction)

{

        $params = $this->getParameters($construction[2]);

        $isSecure = 



$this->_storeManager->getStore($store)->isCurrentlySecure();

      $protocol = $isSecure ? 'https' : 'http';

      if  (isset($params['url']))   {

               return $protocol . '://' . $params['url'];

      } elseif (isset($params['http']) && isset($params['https'])) {

            if ($isSecure)    {

                return $params['https'];

            }

             return $params['http'];

      }

      return $protocol;

}

À partir de la première ligne, $params sera un tableau associatif. Si vous regardez ci-dessous, vous pouvez voir qu’il comprend 3 clés pertinentes : url , http et https .

Si $params[‘url’] n’est pas défini ET que $params[‘http’] et $params[‘https’] sont définis, la méthode renverra tout ce qui a été passé comme valeur de http ou https selon que le magasin utilise HTTPS ou non.

Votre charge utile résultante est alors : {{protocol http=”phar://phar.jpg” https=”phar://phar.jpg”}} .

L’utiliser comme src de l’image dans le frontend se traduira par $imagePath = « phar://phar.jpg » qui déclenchera la désérialisation.

Profit

Nous avons maintenant besoin d’une chaîne POP à inclure dans notre PHAR pour exploiter l’application.

Les classes utilisées sont GuzzleHttp/Stream/FnStream et phpseclib\Crypt\Hash .

Le destructeur de GuzzleHttp/Stream/FnStream est utilisé pour démarrer la chaîne d’exploits :

public function __destruct()

{

      if (isset($this->_fn_close)) {

          call_user_func($this->_fn_close);

      }

}

Vous pouvez l’utiliser pour effectuer un rappel à n’importe quelle méthode de n’importe quelle classe actuellement dans la portée. Un candidat pour cela serait une méthode qui – encore une fois – utilise call_ user_func() , mais avec plusieurs paramètres. Cela vous permet d’utiliser des fonctions comme passthru() ou exec() pour exécuter des commandes sur le serveur.

La méthode _computeKey de phpseclib\Crypt\Hash effectue un tel appel :

function _computeKey()

{

          if ($this->key === false) {

               $this->computedKey = false;

               return;

      }

           if (strlen($this->key) <= $this->b) {

$this->computedKey = $this->key;

                return;

      }

           switch ($this->engine) {

              //modified the cases to ease understanding

              case 2:

                     $this->computedKey = mhash($this->hash, $this->key);

                     break;

             case 3:

                     $this->computedKey = hash($this->hash, $this->key, true);

                     break;

             case 1:

                     $this->computedKey = call_user_func($this->hash, $this->key);

      }

}

Pour exécuter le code à l’aide de cette fonction, vous devez remplir trois conditions :

  1. Définissez $key sur une valeur supérieure à $b . Étant donné que $key sera notre commande serveur, vous pouvez définir $b sur un petit nombre, disons 0
  2. Définissez $engine sur 1, de sorte que le commutateur ira au dernier cas.
  3. Définissez $hash sur quelque chose comme « passthru » .

2. Incidence

En abusant des directives de protocole de Magento, tout utilisateur authentifié peut télécharger un fichier image conçu capable d’effectuer une exécution de code à distance dans le contexte de l’utilisateur du serveur Web.

Calendrier de divulgation responsable

  • 29 août 2019 – Vulnérabilité soumise à HackerOne
  • 12 septembre 2019 – Bug trié par HackerOne
  • 8 octobre 2019 – Réponse de l’équipe de sécurité de Magento (Adobe)
  • 27 novembre 2019 – Prime décernée par HackerOne
  • 28 avril 2020 – Correctif publié par Adobe