La désérialisation insecure est une vulnérabilité qui permet à un attaquant de provoquer une exécution de code à distance (RCE), un déni de service ou une escalade de privilèges en fournissant un flux de données malicieux à une application qui le reconstruit en objets applicatifs sans vérification préalable. Elle est classée A08:2021 Software and Data Integrity Failures dans le Top 10 OWASP, CWE-502 (Deserialization of Untrusted Data) dans le catalogue MITRE, et représente 3 à 6 % des CVE critiques annuelles 2020-2024 selon le CISA KEV Catalog. La vulnérabilité touche de manière aiguë Java (ObjectInputStream, JSF ViewState, RMI, JMS), .NET (BinaryFormatter, LosFormatter, SoapFormatter, NetDataContractSerializer), PHP (unserialize), Python (pickle, YAML.load), Ruby (Marshal), et de manière plus circonstancielle Node.js (node-serialize, serialize-to-js). L'exploitation repose sur des gadget chains : des séquences de classes présentes dans le classpath cible qui, enchaînées lors de la désérialisation, produisent un effet RCE sans exploitation d'une vulnérabilité dans une classe particulière. Les outils publics ysoserial (Java), ysoserial.net (.NET), phpggc (PHP) cataloguent plus de 80 gadget chains opérationnelles 2024-2025. Cet article détaille le mécanisme complet, les exploitations concrètes par langage avec code, les CVE historiques (Log4Shell CVE-2021-44228, Spring4Shell CVE-2022-22965, Apache Commons Collections), les stratégies de détection (CodeQL, Semgrep, PortSwigger scanner) et les mitigations par ordre de robustesse.
1. Qu'est-ce que la désérialisation et pourquoi elle est dangereuse
La sérialisation est la conversion d'un objet mémoire (avec son état, ses références, ses méthodes) en flux d'octets ou chaîne, pour stockage, transmission réseau, ou reprise d'exécution. La désérialisation est l'opération inverse : reconstruction de l'objet depuis le flux. Tant que le flux est trusté (produit par l'application elle-même, stocké dans un emplacement contrôlé), l'opération est bénigne. Quand le flux est sous contrôle attaquant (body HTTP, cookie, message queue publique, fichier uploadé), chaque appel de désérialisation devient un vecteur d'exploitation potentiel.
1.1 Le point de bascule : exécution pendant la reconstruction
Le danger vient de ce que les frameworks de sérialisation exécutent du code métier pendant la reconstruction de l'objet : constructeurs, méthodes spéciales (readObject en Java, __wakeup et __destruct en PHP, __reduce__ et __setstate__ en Python, OnDeserialization en .NET). Ces méthodes peuvent accéder aux systèmes de fichiers, lancer des processus, établir des connexions réseau. Un flux malicieux peut référencer des classes présentes dans le classpath qui, via leurs méthodes automatiquement appelées, produisent un effet d'exécution.
1.2 Formats binaires vs formats de données
| Format | Exécute du code au parse ? | Risque RCE sur input non trusté |
|---|---|---|
| JSON (json.loads, JSON.parse, Jackson sans polymorphisme) | Non | Très faible |
| CBOR, MessagePack | Non | Très faible |
| Protocol Buffers | Non | Faible (schema-bound) |
| XML (sans DTD externes) | Non | Modéré (XXE si DTD activé) |
| YAML safe_load | Non | Faible |
| Java ObjectInputStream | Oui | Critique |
| .NET BinaryFormatter | Oui | Critique (déprécié) |
| Python pickle | Oui | Critique |
| PHP unserialize | Oui | Critique |
| Ruby Marshal | Oui | Critique |
| YAML yaml.load (non safe) | Oui | Critique |
| node-serialize (npm) | Oui | Critique |
La règle de sélection 2025 : JSON par défaut, binaire applicatif uniquement si performance ou compatibilité l'exige, et signé HMAC côté serveur si source externe tolérée. Voir le principe n°8 dans Principes de secure coding.
2. Mécanisme d'exploitation : les gadget chains
Une gadget chain est une séquence de classes légitimes présentes dans le classpath cible dont l'enchaînement produit un effet non prévu lors de la désérialisation. L'attaquant ne fournit pas de bytecode nouveau — il fournit un graphe d'objets dont la simple reconstruction déclenche l'appel en cascade de méthodes qui aboutissent à une RCE.
2.1 Anatomie d'une gadget chain Java
Exemple CommonsCollections1 (CC1), première gadget chain Java publique (Frohoff et Lawrence, AppSecCali 2015) :
Gadget chain CommonsCollections1 — mécanique simplifiée
────────────────────────────────────────────────────────
1. Attaquant envoie un HashMap<AnnotationInvocationHandler, ...> sérialisé
2. readObject() du HashMap déclenche le rehashing à la désérialisation
─► appel automatique de equals() / hashCode() sur les clés
3. AnnotationInvocationHandler.equals() invoque un Proxy
─► Proxy.invoke() appelle une méthode sur un objet Map
4. Le Map est un TransformedMap wrapping LazyMap
─► lazyMap.get() déclenche le Transformer associé
5. Le Transformer est un ChainedTransformer
─► enchaîne ConstantTransformer → InvokerTransformer
─► InvokerTransformer utilise Java Reflection pour appeler
Runtime.getRuntime().exec("id") ← RCE
Total : ~15-20 lignes de sérialisation, ~0 vulnérabilité dans une classeAucune des classes impliquées n'est vulnérable individuellement. La vulnérabilité émerge de leur combinaison activée par la désérialisation automatique. D'où l'impossibilité de « patcher une gadget chain » autrement que par blocage du vecteur de désérialisation lui-même.
2.2 Outils publics de génération de gadget chains
| Outil | Écosystème | Chaînes catalogées | Maintenu |
|---|---|---|---|
| ysoserial | Java | CC1-11, Spring1-2, JSON1, ROME, Hibernate1, JBossInterceptors, etc. (~30) | Oui (communauté) |
| ysoserial.net | .NET | TypeConfuseDelegate, TextFormattingRunProperties, etc. (~15) | Oui |
| phpggc | PHP | Laravel, Symfony, Guzzle, Monolog, WordPress (~60 chaînes) | Oui (très actif) |
| marshalsec | Java (JNDI + désérialisation) | Focus injection JNDI | Oui |
| pickle-payload | Python | Générateurs pickle arbitraires | Actif |
L'existence de ces catalogues publics signifie qu'aucune application Java/.NET/PHP avec dépendances courantes ne peut tolérer de désérialiser un flux non trusté sans une protection explicite.
3. Java : ObjectInputStream, JSF ViewState, RMI
Java reste l'écosystème le plus exposé historiquement à la désérialisation insecure, par la présence ubiquitaire d'Apache Commons Collections, Spring, Hibernate, Jackson en polymorphisme dans les classpaths.
3.1 Surface d'attaque Java typique
| Surface | Où la désérialisation intervient |
|---|---|
ObjectInputStream.readObject() | Ports RMI, JMX, JMS, protocoles custom, caches distribués |
| JSF ViewState | Champ javax.faces.ViewState côté client, signé ou non |
| Spring RemoteInvocation | HTTP Invoker, RMI Spring, JMX |
| Apache ActiveMQ / Artemis | Messages sérialisés entre brokers / clients |
| Jackson polymorphism activé | @JsonTypeInfo(use = Id.CLASS) sans allowlist |
| Hazelcast / Infinispan / EhCache | Caches distribués sérialisant objets |
| Apache Struts ContentType | CVE-2017-5638 (Equifax) — OGNL injection via désérialisation |
3.2 Exemple vulnérable et exploitation
// ❌ Code vulnérable typique — endpoint qui désérialise un body
@PostMapping("/api/state")
public ResponseEntity<String> restoreState(HttpServletRequest req) throws IOException {
try (ObjectInputStream ois = new ObjectInputStream(req.getInputStream())) {
Object state = ois.readObject(); // ❌ RCE si Commons Collections dans classpath
applyState(state);
return ResponseEntity.ok("restored");
} catch (ClassNotFoundException e) {
return ResponseEntity.badRequest().body("unknown class");
}
}Exploitation avec ysoserial :
# Génération payload CommonsCollections1 qui lance "id"
java -jar ysoserial.jar CommonsCollections1 "id" > payload.bin
# Envoi vers endpoint vulnérable
curl -X POST --data-binary @payload.bin \
-H "Content-Type: application/octet-stream" \
https://target.example/api/state3.3 Mitigation Java
// ✅ JEP-290 ObjectInputFilter (Java 9+, backporté sur 8u121+)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.mycompany.state.AppState;" + // classes autorisées explicites
"java.util.ArrayList;" +
"java.util.HashMap;" +
"!*" // tout le reste rejeté
);
try (ObjectInputStream ois = new ObjectInputStream(req.getInputStream())) {
ois.setObjectInputFilter(filter);
Object state = ois.readObject();
applyState(state);
}Encore mieux : supprimer la désérialisation binaire et passer à Jackson JSON avec DTO explicite :
// ✅ Solution racine — JSON schema-bound, pas de polymorphisme
@PostMapping(value = "/api/state", consumes = "application/json")
public ResponseEntity<String> restoreState(@Valid @RequestBody AppStateDto dto) {
applyState(dto.toDomain());
return ResponseEntity.ok("restored");
}4. Python : pickle, YAML.load, marshal
Python pickle est le format de sérialisation natif et l'un des plus simples à exploiter. Sa documentation officielle Python 3.12 déclare explicitement : « It is possible to construct malicious pickle data which will execute arbitrary code during unpickling. Never unpickle data received from an untrusted or unauthenticated source. ».
4.1 Anatomie d'un payload pickle RCE
Le mécanisme repose sur la méthode magique __reduce__ d'une classe custom, qui retourne un callable et ses arguments, appelés à la désérialisation :
# Génération d'un payload pickle qui exécute "id" à la désérialisation
import pickle, os
class Exploit:
def __reduce__(self):
return (os.system, ("id",))
payload = pickle.dumps(Exploit())
# Export du payload base64
import base64
print(base64.b64encode(payload).decode())
# Côté victime :
# pickle.loads(payload) ─► os.system("id") s'exécute4.2 Surface Python vulnérable typique
| Contexte | Risque |
|---|---|
| Cookies pickle-encodés (legacy Flask, Django sessions signés) | Critique si signature absente ou clé divulguée |
Files uploads pickle.load(f) | Critique |
| Redis / memcached déserialisant pickle automatiquement | Critique si cache accessible attaquant |
| Celery tasks avec serializer='pickle' | Critique si broker exposé |
yaml.load() sans Loader=SafeLoader | Critique |
torch.load() (PyTorch checkpoints) | Critique (utilise pickle sous le capot) |
4.3 Mitigation Python
# ✅ YAML safe
import yaml
config = yaml.safe_load(untrusted_input) # jamais yaml.load(...) nu
# ✅ JSON pour les sessions / caches
import json
session_data = json.loads(cookie_value)
# ✅ Celery configuration
# dans celery config :
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
CELERY_ACCEPT_CONTENT = ["json"]
# ✅ PyTorch checkpoints : weights_only=True (PyTorch 2.1+)
model = torch.load("checkpoint.pt", weights_only=True)4.4 Restricted Unpickler (mitigation partielle)
Si pickle est inévitable (interopérabilité ML, interprocess IPC fiable) :
import pickle, io
ALLOWED = {
("numpy.core.multiarray", "_reconstruct"),
("numpy", "ndarray"),
("numpy", "dtype"),
("my_app.models", "Checkpoint"),
}
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if (module, name) not in ALLOWED:
raise pickle.UnpicklingError(f"blocked: {module}.{name}")
return super().find_class(module, name)
def safe_loads(data: bytes):
return RestrictedUnpickler(io.BytesIO(data)).load()5. PHP : unserialize, magic methods, phpggc
PHP unserialize déclenche les méthodes magiques __wakeup, __destruct, __toString, __call sur les objets reconstruits. La surface d'exploitation dépend du code chargé dans l'application (Laravel, Symfony, WordPress, Joomla, Drupal, Guzzle). L'outil phpggc (maintenu activement par ambionics) catalogue plus de 60 gadget chains sur les frameworks PHP courants.
5.1 Exemple simplifié
// ❌ Application PHP qui désérialise un cookie non signé
session_start();
$user = unserialize($_COOKIE['user_data']); // ❌ RCE via gadget chain
// ...
// ✅ JSON + signature HMAC
$signed = $_COOKIE['user_data'];
[$payload_b64, $sig] = explode('.', $signed, 2);
$expected = hash_hmac('sha256', $payload_b64, $HMAC_KEY);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit();
}
$user = json_decode(base64_decode($payload_b64), true);5.2 Option allowed_classes (PHP 7+)
// ✅ Restriction classes désérialisables (mitigation partielle, pas suffisante seule)
$user = unserialize($data, ['allowed_classes' => ['AppUser']]);Attention : même avec allowed_classes, les méthodes magiques des classes listées peuvent elles-mêmes être exploitées si l'application le permet. La solution racine reste la migration vers JSON.
6. .NET : BinaryFormatter et successeurs, ysoserial.net
Microsoft a déprécié BinaryFormatter dans .NET 5 (2020) et déclaré la désérialisation binaire « dangereuse par nature » dans la documentation officielle. Mais les applications .NET legacy et beaucoup d'intégrations enterprise l'utilisent encore en 2024-2025.
6.1 Formatters .NET dangereux
| Formatter | Statut 2025 | Risque |
|---|---|---|
| BinaryFormatter | Déprécié (obsolete warning dès .NET 5, retiré .NET 9 par défaut) | Critique |
| SoapFormatter | Déprécié | Critique |
| LosFormatter (WebForms ViewState) | Legacy, encore utilisé | Critique si MAC validation désactivée |
| NetDataContractSerializer | Déconseillé | Critique |
| JavaScriptSerializer avec TypeResolver | Problématique | Critique si TypeResolver custom |
| JSON.NET (Newtonsoft) avec TypeNameHandling | Critique si TypeNameHandling.All ou .Auto sans SerializationBinder | Critique |
6.2 Exemple exploit ysoserial.net
# Génération payload TextFormattingRunProperties qui lance calc.exe
ysoserial.exe -g TextFormattingRunProperties -f BinaryFormatter \
-c "calc.exe" > payload.bin6.3 Mitigation .NET moderne
// ✅ System.Text.Json — format JSON pur, pas de polymorphisme non maîtrisé
using System.Text.Json;
public record UserDto(Guid Id, string DisplayName);
var user = JsonSerializer.Deserialize<UserDto>(
untrustedJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
// ✅ Si Newtonsoft.Json requis : TypeNameHandling strictement None
var settings = new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.None, // ✅ défaut sûr
// Jamais TypeNameHandling.All ou .Auto sans SerializationBinder custom
};7. Node.js et Ruby : surface plus étroite mais réelle
7.1 Node.js
Les modules node-serialize et serialize-to-js sont vulnérables par conception (CVE-2017-5941 node-serialize RCE). Le JSON natif et la plupart des libs (Jackson JS, cbor-x, msgpack) sont sûrs. Risque ponctuel sur des systèmes héritant de vieux modules npm.
7.2 Ruby
Marshal.load est équivalent à pickle Python — RCE sur input non trusté. Plusieurs CVE Rails historiques (CVE-2013-0156, CVE-2013-0333) impliquent la désérialisation de sessions Marshal. Rails 4+ désactive Marshal par défaut pour les sessions (JSON), mais les caches Memcached / Redis Marshal-encodés restent une surface si exposés.
# ❌ Marshal sur donnée externe
data = Marshal.load(Base64.decode64(params[:state]))
# ✅ JSON + signature
payload = JSON.parse(verified_signed_message(params[:state]))8. CVE notables 2021-2024
Sélection des CVE désérialisation les plus impactantes des 4 dernières années :
| CVE | Année | Produit | CVSS | Impact |
|---|---|---|---|---|
| CVE-2021-44228 (Log4Shell) | 2021 | Apache Log4j 2 | 10.0 | RCE via JNDI + désérialisation |
| CVE-2022-22965 (Spring4Shell) | 2022 | Spring Core Framework | 9.8 | RCE via ClassLoader + désérialisation |
| CVE-2023-20860 | 2023 | Spring Framework | 7.5 | DoS / SSRF via désérialisation |
| CVE-2023-22527 | 2024 | Atlassian Confluence | 10.0 | RCE via OGNL + désérialisation |
| CVE-2024-23897 | 2024 | Jenkins CLI | 9.8 | Arbitrary file read enabling deserialization |
| CVE-2024-1597 | 2024 | PostgreSQL JDBC | 10.0 | SQL injection enabling Java deserialization |
| CVE-2024-45195 | 2024 | Apache OFBiz | 9.8 | RCE via désérialisation non-authentifiée |
Le CISA KEV Catalog (Known Exploited Vulnerabilities) comptait ~90 CVE désérialisation en exploitation active au premier trimestre 2025, majoritairement Java et .NET.
9. Détection dans un codebase
9.1 Règles Semgrep ciblées
# semgrep rule — détecte les usages Python pickle risqués
rules:
- id: python-pickle-loads-untrusted
pattern-either:
- pattern: pickle.loads($DATA)
- pattern: pickle.load($F)
- pattern: cPickle.loads($DATA)
- pattern: yaml.load($DATA)
message: >
Désérialisation dangereuse sur input potentiellement non trusté.
Utiliser json.loads, yaml.safe_load, ou un Unpickler restreint.
severity: ERROR
languages: [python]
metadata:
cwe: CWE-502
owasp: A08:20219.2 CodeQL queries GitHub Security Lab
GitHub Security Lab maintient un pack de queries CodeQL spécialisées :
java/unsafe-deserialization— détectereadObjectsansObjectInputFilter.python/unsafe-deserialization— détectepickle.loads,yaml.loadsur sources non trustées.csharp/unsafe-deserialization—BinaryFormatter,JavaScriptSerializeravec resolver.javascript/unsafe-deserialization—node-serialize.unserializesur entrée externe.
9.3 Test en pentest black-box
- Burp Java Deserialization Scanner : détecte les cookies base64 ou paramètres qui décodent en Java serialized data (marqueur
AC ED 00 05/rO0AB). - Payload ysoserial en mode OOB via PortSwigger Collaborator : toute exécution côté serveur produit une requête DNS/HTTP vers le domaine collaborator, détectable sans sortie visible.
- phpggc + Burp repeater pour tester manuellement les points d'injection PHP.
10. Mitigation par ordre de robustesse
Stratégies de défense classées par efficacité décroissante.
10.1 Supprimer la désérialisation binaire (solution racine)
Remplacer ObjectInputStream / pickle / BinaryFormatter / unserialize par JSON ou CBOR avec schéma strict (Jackson DataBind sans polymorphisme, Pydantic, System.Text.Json, Zod). C'est la seule solution structurellement robuste. Toutes les autres mitigations sont des contournements à limites documentées.
10.2 Signer et vérifier le flux (si binaire inévitable)
Quand la désérialisation binaire est contractuellement imposée (legacy, protocole externe, interopérabilité broker) :
import hmac, hashlib, pickle
SECRET = load_secret("app-pickle-hmac-key") # jamais en code
def sign(payload: bytes) -> bytes:
sig = hmac.new(SECRET, payload, hashlib.sha256).digest()
return sig + payload
def verify_and_load(signed: bytes):
sig, payload = signed[:32], signed[32:]
expected = hmac.new(SECRET, payload, hashlib.sha256).digest()
if not hmac.compare_digest(sig, expected):
raise SecurityError("signature_invalid")
return pickle.loads(payload) # maintenant safe car flux authentifiéLa signature HMAC garantit que seul le serveur (détenteur du secret) peut produire un flux acceptable. L'attaquant ne peut pas forger de payload valide.
10.3 Filtres de classes
ObjectInputFilter (Java JEP-290), SerializationBinder (.NET), allowed_classes (PHP), Restricted Unpickler (Python). Couvre les cas connus, peut laisser passer des gadget chains sur classes non anticipées. Mitigation complémentaire, pas substitutive.
10.4 Isolation du processus
Exécuter la désérialisation dans un container séparé, un process sandboxed (seccomp, AppArmor, gVisor), avec minimal file system et capabilities. Si une RCE survient malgré tout, elle reste contenue. Mitigation défense-en-profondeur, utile sur les systèmes legacy qui ne peuvent pas migrer.
10.5 Network isolation et RMI/JMX
Ne jamais exposer de ports RMI, JMX, JMS Java sur Internet. Restreindre au réseau interne avec authentification forte. Patterns historiques de CVE Java désérialisation montrent que 70 % des exploitations publiques visent ces protocoles exposés.
Points clés à retenir
- Définition : RCE provoquée par la reconstruction d'un objet depuis un flux non trusté, via l'exécution automatique de méthodes spéciales (readObject, __wakeup, reduce, OnDeserialization).
- Mécanisme : gadget chains — enchaînement de classes légales présentes dans le classpath, exploitables via ysoserial / ysoserial.net / phpggc.
- Formats dangereux : Java ObjectInputStream, .NET BinaryFormatter, Python pickle, PHP unserialize, Ruby Marshal, YAML.load non-safe. Formats sûrs : JSON, CBOR, Protobuf.
- CVE majeures récentes : Log4Shell CVE-2021-44228, Spring4Shell CVE-2022-22965, Confluence CVE-2023-22527, Jenkins CLI CVE-2024-23897, PostgreSQL JDBC CVE-2024-1597.
- Mitigation hiérarchisée : 1) supprimer la désérialisation binaire (JSON + schéma), 2) signer HMAC si binaire imposé, 3) ObjectInputFilter / SerializationBinder, 4) isolation process, 5) network isolation.
- Détection codebase : CodeQL (GitHub Security Lab queries), Semgrep (rules CWE-502), Snyk Code, Burp Java Deserialization Scanner.
- Jackson / Newtonsoft piège : polymorphisme JSON (TypeNameHandling.Auto ou activateDefaultTyping) = désérialisation dangereuse en JSON aussi.
Pour le contexte général des principes de secure coding, voir Principes de secure coding. Pour le parcours d'apprentissage défensif complet, Roadmap secure coding et Roadmap AppSec Engineer. Pour la validation offensive de ces vulnérabilités, OWASP Testing Guide expliqué et Méthodologie pentest web.







