Analyse d'une désérialisation non sécurisée sur JFrog Artifactory - Partie 1

Lors de tests d’intrusion, nous rencontrons régulièrement la solution Artifactory au sein d’environnements internes.

Artifactory est une solution de gestion de dépôts de binaires qui permet l’interfaçage entre différents composants d’une infrastructure CI/CD. Étant souvent un nœud central dans ce type d’infrastructure, sa compromission est intéressante d’un point de vue offensif, car elle permettrait ainsi de rebondir sur les autres systèmes.

Dans cet article, nous allons explorer les détails d’une vulnérabilité rendue publique en mai 2022, et qui pourrait permettre d’exécuter du code arbitraire sur le serveur sous-jacent.

Introduction

La vulnérabilité CVE-2022-0573 a été rendue publique en mai 2022, et a été identifiée par Matthias Kaiser et Jonni Passki.

Cette vulnérabilité, d’un score CVSS de 8,5 est décrite de la manière suivante :

JFrog Artifactory before 7.36.1 and 6.23.41, is vulnerable to Insecure Deserialization of untrusted data which can lead to DoS, Privilege Escalation and Remote Code Execution when a specially crafted request is sent by a low privileged authenticated user due to insufficient validation of a user-provided serialized object.

Un utilisateur pourrait alors envoyer un objet sérialisé à la solution, qui n’effectuerait pas les contrôles nécessaires sur l’objet envoyé (Désérialisation non sécurisée).

Analyse du code source

Artifactory étant une solution open source, nous pouvons analyser le code source entre une version vulnérable et une version corrigée afin de visualiser les changements effectués.

Cela pourrait permettre de donner une piste sur les fonctionnalités qui seraient affectées par cette vulnérabilité.

Pour cela, nous allons analyser les versions 7.25.7 (vulnérable) et 7.25.9 (corrigée), dont les sources sont accessibles ici : https://releases.jfrog.io/artifactory/bintray-artifactory/org/artifactory/oss/jfrog-artifactory-oss/

Note : Pour comparer deux dossiers et utiliser un IDE classique, nous pouvons utiliser un dépôt git :

Analyse du correctif

Le seul fichier de code source java modifié entre les deux versions est XmlUtils.java. La méthode hardenXStream est ainsi modifiée :

A gauche la version vulnérable, et à droite la version corrigée d’Artifactory

Cette méthode permet de durcir la désérialisation XStream en mettant en place une liste blanche. La modification ici restreint la liste blanche pour passer de la liste blanche par défaut (setupDefaultSecurity) à une liste blanche sur mesure.

XStream est une bibliothèque Java qui a pour objectif de convertir des instances d’objet Java en XML, et l’inverse.

Cela valide bien la description publique de la CVE, le code modifié correspond à une correction sur le mécanisme de sérialisation utilisé par la solution.

Recherche des utilisations de la méthode modifiée

L’objectif est alors de rechercher les utilisations de la méthode, et ainsi de remonter jusqu’à la requête contenant l’objet XML correspondant à un objet Java sérialisé avec XStream.

Cette méthode hardenXStream est utilisée à deux reprises dans le code source :

  • Au sein de la classe MimeTypesReader, qui permet de lire les fichiers de configuration Mime. A première vue, ces fichiers ne sont pas modifiables par des utilisateurs anonymes ou non privilégiés, nous ne pousserons donc pas l’analyse au sein de cette classe.
  • A travers la classe XStreamFactory, qui va générer les classes XStream au sein de la solution.

Nous pouvons lister toutes les utilisations de cette classe XStreamFactory, et de manière plus générale de la méthode fromXML de la librairie XStream qui effectue la déserialisation.

La méthode fromXml de la classe PropertiesXmlProvider utilise ainsi la méthode fromXML de la librairie XStream via XStreamfactory, et on remarque qu’en traçant le graphe d’appel de cette méthode, une méthode exposée est accessible :

De plus, lorsque l’accès anonyme est configuré sur Artifactory, cette méthode est accessible sans authentification. On va alors construire la requête contenant l’objet Java utilisé par cette méthode exposée.

Construction de la requête

Pour cela, nous allons analyser quelles sont les modifications et vérifications effectuées sur le contenu de la requête tout au long de la chaîne, et vérifier que l’objet que l’on transmet arrive effectivement à la méthode FromXML de XStream.

L’analyse de la méthode buildUploadRedirect permet de construire le corps de la requête :

Méthode exposée sur l’API

On peut ainsi identifier la méthode HTTP à utiliser (PUT), et le type de contenu à envoyer au serveur (application/octet-stream).


Note : Nous utilisons ici la gadget chain URLDNS, qui lorsqu’elle est désérialisée effectue une résolution DNS de l’URL fournie, ce qui permet de confirmer la désérialisation arbitraire. Cela provient du comportement de la fonction hashCode de la classe URL.


On poursuit alors le graphe d’appel, sur la méthode handleBuildUploadRedirect :

Modification de la requête avant déploiement

Cette méthode modifie d’abord la requête en appelant la méthode modifyRequestForProperDeployment. Cette méthode permet d’effectuer des contrôles avant le déploiement, et de modifier la requête afin qu’elle soit correctement traitée par Artifactory.

En réalité, aucune modification ou contrôle n’est effectué si on modifie l’en-tête HTTP User-Agent de la requête :

Vérification de l’User-Agent

L’User-Agent commençant par « Artifactory/ » est considéré comme provenant d’une source de confiance, et la requête n’est pas modifiée. On peut ajouter cet en-tête à notre requête afin de contourner les vérifications et modifications appliquées aux requêtes de build :

La méthode upload n’effectue pas de vérifications ou modifications sur le contenu de notre requête, nous pouvons alors passer à la méthode validateRequestAndUpload :

Cette méthode va dispatcher les requêtes de build vers les bons parsers pour les uploader sur l’Artifactory. Ici nous voulons accéder à la méthode validateAndUploadProperties pour avancer dans le graphe.

Déploiement de properties (path déprécié)

Une vérification est ainsi à ajouter à notre requête ici pour pouvoir y accéder, nous devons valider le contrôle de la méthode vérifiant si notre requête correspond à des Properties. Pour cela, il faut ajouter « :properties » à notre nom de fichier afin de valider la vérification.

Nous pouvons alors passer à la méthode validateAndUploadProperties, qui prend le corps de la requête et désérialise l’objet XML vers un objet Java en utilisant la librairie XStream.

Appel de la méthode fromXml qui va effectuer la désérialisation

La requête HTTP à envoyer au serveur est alors la suivante :

En envoyant la requête sur un Artifactory vulnérable, l’objet est désérialisé et une résolution DNS est effectuée. L’objectif est alors d’identifier une gadget chain exploitable sur la solution, qui permettra d’exécuter des commandes sur le serveur.

Recherche de gadget chain

Dans la deuxième partie de cet article, nous détaillerons la recherche de gadget chains. Nous ne rentrerons pas dans les détails sur une gadget chain utilisable pour cette vulnérabilité en particulier, mais expliquerons de manière générale le fonctionnement de ces dernières.

Cas général

Ces gadget chains exploitent le fait que des méthodes sont appelées automatiquement sur les objets lors de la désérialisation. Ainsi, les conditions suivantes sont nécessaires afin d’obtenir une gadget chain :

  • La classe fait partie du classpath de l’application et est statique ou implémente l’interface Serializable ;
  • La classe effectue une action intéressante lors de la désérialisation (exécution de commande, écriture dans un fichier…) ;
  • Ou la classe effectue un appel vers une autre méthode.

En effet, la méthode readObject est par exemple appelée par défaut. Si cette méthode effectue une action intéressante, ou appelle une autre méthode, une chaine peut alors être conçue. Par exemple, dans le cas de la classe HashMap du JDK :

Méthode readObject (tronquée) de la classe HashMap

Ici, lors de la désérialisation d’une HashMap, la méthode hashCode sera appelée sur chaque clé de la Map. Si une classe Person est déclarée comme ci-dessous dans le classpath :

Et que cette classe est instanciée en tant que clé dans une HashMap, nous aurions alors la gadget chain suivante :

On pourrait alors instancier un objet Person, l’insérer en tant que une clé d’une HashMap. Lors de la désérialisation, la commande renseignée dans l’attribut command lors de l’instanciation sera exécutée.

Cas de la désérialisation XStream

Pour XStream, n’importe quelle classe du classpath peut être désérialisée. Nous ne sommes ainsi plus limités par l’utilisation d’une classe statique ou sérialisable. Néanmoins, les méthodes appelées lors de la désérialisation ne le sont plus forcément.

XStream utilise en effet des Converters afin de désérialiser les objets. Dans le cas d’une Map, c’est le MapConverter qui sera utilisé, et l’appel à readObject n’est plus effectué. Cependant, ce Converter va utiliser la méthode put de la classe HashMap, qui va aussi appeler la méthode hashCode sur la clé. C’est ainsi la même chaine, mais plus la même primitive afin de l’exploiter.

À suivre

La recherche de la gadget chain exploitable sur la solution fera faire l’objet d’une deuxième partie sur ce blog.

Références

Arnaud Lagadec

Découvrir d'autres articles

  • Pentest

    Des dés aux données : de l’importance des nombres aléatoires (partie 2/2)

    Lire l'article
  • Pentest

    Des dés aux données : de l’importance des nombres aléatoires (partie 1/2)

    Lire l'article
  • Pentest

    Tour d’horizon des attaques et vulnérabilités sur le gestionnaire de mots de passe KeePass 

    Lire l'article