Skip to main content

Sérialisation et désérialisation en Java : comprendre la vulnérabilité de désérialisation Java

Écrit par

18 décembre 2020

0 minutes de lecture

Note de la rédaction — 11 juillet 2022

Cet article a été mis à jour pour tenir compte des changements apportés aux versions récentes de Java et des vulnérabilités exploitables par désérialisation. Nous avons également ajouté une conférence récente présentant des exploits de désérialisation Java lors d’une démonstration en direct.


La sérialisation Java est un mécanisme qui transforme un objet en flux d’octets. À l’inverse, la désérialisation Java permet de recréer un objet à partir d’un flux d’octets. La sérialisation Java — et tout particulièrement la désérialisation — est connue comme « le cadeau qui ne cesse de donner », car elle a entraîné de nombreux problèmes de sécurité et vulnérabilités au fil des ans.

Qu’est-ce que la sérialisation en Java ?

Nous utilisons Java pour créer des objets. Ceux-ci sont stockés en mémoire, puis supprimés par le ramasse-miettes dès qu’ils ne sont plus utilisés. Pour transférer un objet et, par exemple, le stocker sur un disque ou l’envoyer sur un réseau, nous devons le transformer en flux d’octets. Pour cela, la classe de cet objet doit implémenter l’interface Serializable. Comme indiqué précédemment, la sérialisation permet de convertir l’état d’un objet en flux d’octets. Ce flux d’octets ne contient pas le code lui-même.

Qu’est-ce que la désérialisation en Java ?

La désérialisation est exactement l’inverse de la sérialisation. Elle consiste à partir d’un flux d’octets pour recréer l’objet précédemment sérialisé dans son état d’origine. Toutefois, vous devez disposer de la définition de l’objet pour le recréer correctement.

Comment fonctionne la sérialisation Java ?

La sérialisation Java utilise la réflexion pour récupérer toutes les données nécessaires dans les champs de l’objet, y compris les champs privés et finaux. Si un champ contient un objet, celui-ci est sérialisé de manière récursive. Même si vous disposez de getters et de setters, ces fonctions ne sont pas utilisées lors de la sérialisation d’un objet en Java.

Comment fonctionne la désérialisation Java ?

Lorsqu’un flux d’octets est reconverti en objet, le constructeur n’est pas utilisé. Un objet vide est simplement créé, puis la réflexion sert à écrire les données dans ses champs. Comme pour la sérialisation, les champs privés et finaux sont également inclus.

Qu’est-ce qu’une vulnérabilité de désérialisation Java ?

Une vulnérabilité de désérialisation Java est une faille de sécurité qui survient lorsqu’un utilisateur malveillant tente d’introduire dans le système un objet sérialisé modifié afin de compromettre le système ou ses données. Pensez à une vulnérabilité d’exécution de code arbitraire qui peut être déclenchée lors de la désérialisation d’un objet sérialisé. Pour mieux comprendre les vulnérabilités de désérialisation Java, nous devons d’abord voir comment fonctionne la désérialisation en Java.

Comprendre les vulnérabilités de désérialisation Java

En Java, un objet sérialisé est un tableau d’octets contenant des informations sur son état. Il contient le nom de l’objet auquel il fait référence ainsi que les données de ses champs. En ouvrant un objet sérialisé stocké avec un éditeur hexadécimal, vous pouvez facilement repérer et modifier ces informations.

Nous savons déjà que la désérialisation Java n’utilise pas le constructeur pour créer un objet : elle charge plutôt les champs par réflexion. Cela signifie que les contrôles de validation effectués dans le constructeur ne sont jamais appelés lors de la recréation de l’objet. Pensez, par exemple, à la vérification que la date de début précède la date de fin lors de la description d’une période. Lorsqu’un objet Java est désérialisé, ce nouvel objet peut se retrouver dans un état non valide.

Examinons l’exemple suivant de vulnérabilité de désérialisation Java, dans lequel nous sérialisons un objet d’une classe sérialisable ValueObject :

public class ValueObject implements Serializable {

   private String value;
   private String sideEffect;

   public ValueObject() {
       this("empty");
   }

   public ValueObject(String value) {
       this.value = value;
       this.sideEffect = java.time.LocalTime.now().toString();
   }
}

ValueObject vo1 = new ValueObject("Hi");
FileOutputStream fileOut = new FileOutputStream("ValueObject.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(vo1);
out.close();
fileOut.close();

"seriLors de la lecture du fichier contenant l’objet sérialisé, ValueObject.ser, avec un éditeur hexadécimal, le résultat s’affiche comme suit :

Fenêtre de terminal affichant un vidage hexadécimal vert avec des données d’objet Java sérialisé et des décalages de fichier

Nous pouvons maintenant facilement modifier la valeur de la chaîne. Par exemple, remplaçons Hi par Hallo dans le résultat affiché par notre éditeur hexadécimal.

Fenêtre de terminal affichant un dump hexadécimal vert contenant des données de sérialisation Java et des octets mis en évidence formant « Hallo ».
FileInputStream fileIn = new FileInputStream("ValueObject2.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
ValueObject vo2 = (ValueObject) in.readObject();

Lors de la désérialisation du fichier binaire modifié, nous constatons que la valeur value de l’objet a changé. Nous remarquons également que l’horodatage n’a pas changé, ce qui prouve que le constructeur n’a jamais été appelé. Si une application accepte des objets sérialisés, il est relativement facile d’en modifier les valeurs. En altérant ces objets, nous pouvons créer des objets non valides, compromettre l’intégrité des données, ou pire encore.

Exécution de code arbitraire, gadgets et chaînes

La modification des données d’un objet est déjà nuisible. Toutefois, elle peut également entraîner l’exécution de code si le bon ensemble d’objets est désérialisé. Avant d’aborder ce sujet, voyons ce que sont les gadgets et les chaînes.

Gadgets

Un gadget — au sens utilisé par Lawrence et Frohoff dans leur présentation Marschalling Pickle à AppSecCali 2015 — est une classe ou une fonction contenant du code exécutable déjà présent dans le processus vulnérable. Ce code existant peut être réutilisé à des fins malveillantes. Dans les objets sérialisables Java, certaines méthodes spéciales — comme la méthode privée readObject() — sont appelées par réflexion lors de la désérialisation.

Examinons le gadget simplifié ci-dessous :

public class Gadget implements Serializable {

   private Runnable command;

   public Gadget(Command command) {
       this.command = command;
   }

   private final void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
       in.defaultReadObject();
       command.run();
   }
}

Cette classe gadget remplace la méthode readObject par défaut. Ainsi, chaque fois qu’un objet de la classe Gadget est désérialisé, la commande de l’objet Runnable est exécutée. Si une classe de commande ressemble à l’exemple ci-dessous, il est facile de modifier cet objet sérialisé et d’effectuer une injection de code.

public class Command implements Runnable, Serializable {

   private String command;

   public Command(String command) {
       this.command = command;
   }

   @Override
   public void run() {
       try {
           Runtime.getRuntime().exec(command);
       } catch (IOException e) {
           throw new RuntimeException(e);
       }
   }

Notez également que si une application accepte des objets sérialisés, l’objet est désérialisé avant d’être converti vers le type souhaité. Cela signifie que même si la conversion échoue, la désérialisation est déjà terminée et la méthode readObject() est exécutée.

FileInputStream fileIn = new FileInputStream("Gadget.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
var obj = (ValueObject)in.readObject();

Attaque par chaîne de gadgets lors de la désérialisation

Une attaque par désérialisation classique repose sur une chaîne de gadgets savamment conçue. Un attaquant recherche un gadget pour lancer une attaque, puis enchaîne plusieurs exécutions qui aboutissent à l’exécution de code arbitraire, par exemple :

Gadget -> readObject() -> command.run() -> Runtime.getRuntime().exec()

Pour un exemple plus concret, examinons l’implémentation de java.util.HashMap. Cette classe possède une implémentation personnalisée de la méthode readObject(), qui déclenche la fonction hashcode() de chaque clé.

Bibliothèques

Il est utile de savoir que les chaînes de gadgets disponibles dans votre application ne sont pas nécessairement liées à votre code. Comme nous importons de nombreuses portions de code depuis des bibliothèques et des frameworks, le nombre de classes importées par vos dépendances (transitives) influe sur la possibilité d’utiliser certaines chaînes de gadgets. Même si la création d’une chaîne de gadgets malveillante est difficile et demande beaucoup de travail, les vulnérabilités de désérialisation Java représentent un risque de sécurité réel et dangereux.

La vulnérabilité Log4Shell de décembre 2021 en est un excellent exemple. Il s’agissait essentiellement d’une vulnérabilité d’exécution de code à distance qui aurait pu provenir du framework de sérialisation natif de Java. Dans les anciennes versions de Java, nous faisions confiance au code distant par défaut et la classe n’avait pas besoin de figurer dans notre classpath. Dans les versions plus récentes de Java, même les plus récentes, la vulnérabilité Log4Shell peut toutefois exploiter une chaîne de gadgets présente dans votre classpath, et potentiellement entraîner une exécution de code à distance, ou pire encore.

Comment prévenir une vulnérabilité de désérialisation Java ?

La meilleure façon de prévenir une vulnérabilité de désérialisation Java consiste à éviter complètement la sérialisation Java. Si votre application n’accepte pas d’objets sérialisés, ils ne peuvent pas vous nuire.

Toutefois, si vous devez implémenter l’interface `serializable` en raison de l’héritage, vous pouvez remplacer readObject(), comme ci-dessous, pour empêcher la désérialisation effective.

private final void readObject(ObjectInputStream in) throws java.io.IOException {
   throw new java.io.IOException("Deserialized not allowed");
}

Si votre application s’appuie sur des objets sérialisés, vous pouvez envisager d’inspecter votre ObjectInputStream avant la désérialisation. La bibliothèque Apache Commons IO peut vous aider. Elle fournit un ValidatedObjectInputStream qui vous permet d’autoriser explicitement les objets que vous souhaitez désérialiser. Vous pouvez ainsi empêcher complètement la désérialisation de types inattendus.

FileInputStream fileIn = new FileInputStream("Gadget.ser");
ValidatingObjectInputStream in = new ValidatingObjectInputStream(fileIn);
in.accept(ValueObject.class);
var obj = (ValueObject)in.readObject();

Une autre option consiste à utiliser les filtres de sérialisation natifs de Java. Cette fonctionnalité a été implémentée dans JEP 290 et publiée avec Java 9 (mars 2018). Vous pouvez définir précisément des filtres pour bloquer ou autoriser certaines classes. En vous basant sur un modèle, vous pouvez créer un filtre qui limite les classes pouvant être désérialisées.

ObjectInputStream in = new ObjectInputStream(fileIn);
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("io.snyk.package.Object;!*");
in.setObjectInputFilter(filesOnlyFilter);

Avec la publication de JEP 415 dans Java 17 (septembre 2021), ce système de filtres a été amélioré pour tenir compte du contexte. Vous pouvez ainsi contrôler le comportement d’un filtre de manière adaptée à votre contexte.

Dans l’article Nouvelles fonctionnalités de Java 17 pour améliorer la sécurité et la sérialisation, nous examinons plus en détail ces nouvelles fonctionnalités et expliquons les filtres Java, leur mise en œuvre et l’importance de ces filtres propres au contexte.

Un outil comme ysoserial est également très utile pour détecter les vulnérabilités de désérialisation Java. Il génère des charges utiles permettant de découvrir des chaînes de gadgets dans les bibliothèques Java courantes qui, dans les bonnes conditions, peuvent exploiter les applications Java qui désérialisent des objets de manière non sécurisée.

Notez que les vulnérabilités de désérialisation Java ne se limitent pas à l’implémentation de la sérialisation personnalisée de Java. Bien que ce soit le sujet de cet article, les mêmes vulnérabilités existent dans les frameworks de sérialisation ou de marshaling qui se chargent de cette tâche à votre place. Si un framework crée comme par magie des POJO à partir de XML, de JSON, de YAML ou de formats similaires, il utilise probablement la réflexion de la même manière que celle décrite ci-dessus. Les mêmes problèmes se posent donc.

Pour prévenir ce type de vulnérabilité de désérialisation Java dans vos bibliothèques externes, analysez gratuitement vos bibliothèques avec Snyk Open Source, le plus tôt et le plus souvent possible. Pour en savoir plus sur ce type de vulnérabilité, consultez également la leçon Snyk Learn sur la désérialisation non sécurisée.

Conférence : « Exploits de désérialisation en Java : pourquoi s’en soucier ? »

J’ai récemment présenté une conférence sur les exploits de désérialisation à Devoxx UK, la plus grande et la plus prestigieuse conférence de la communauté Java au Royaume-Uni.

Dans cette présentation, j’explique le fonctionnement natif des vulnérabilités de désérialisation en Java et la création des chaînes d’attaque. Les hackers qualifient la désérialisation en Java de « cadeau qui ne cesse de donner ». J’ai donc créé cette présentation et une démonstration en direct pour montrer l’impact potentiel sur la sécurité.

Bien sûr, les récents problèmes de sécurité liés à la vulnérabilité Log4Shell en font également partie. J’ai expliqué comment Log4Shell peut servir de point de départ à une chaîne de gadgets de désérialisation, dans laquelle le gadget terminal exécute du code arbitraire.

D’autres types de désérialisation, comme JSON, XML et YAML, peuvent également vous exposer à des risques. Pour en savoir plus, consultez l’article Problèmes de désérialisation JSON en Java avec Jackson ObjectMapper. Cette présentation approfondit le sujet et montre les conséquences concrètes.

Deserialization exploits in Java: why should I care? by Brian Vermeer

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

Lire la suite

Blog

Les modèles de pointe ont trouvé les vulnérabilités. Seul l’attaquant a trouvé les chaînes d’exploitation.

L’analyse statique a détecté les failles, mais seuls des tests d’attaque en conditions réelles ont prouvé comment elles pouvaient être enchaînées pour provoquer des compromissions. Comparaison d’Evo COS, de Claude Security et de Claude Code Security.

feature insights context
Blog

Les attaques autonomes sont déjà là. La défense doit suivre leur rythme.

Les attaquants autonomes réduisent la fenêtre de défense. Découvrez comment la découverte, la correction, la validation et la prévention continues peuvent aider les équipes de sécurité à suivre le rythme.

Blog

Pourquoi les agents de codage IA créent-ils sans cesse des failles de contrôle d’accès ?

Les agents de codage IA peuvent générer une logique d’autorisation qui compile et passe la revue, tout en exposant les données d’un tenant à un autre. Découvrez pourquoi les failles de contrôle d’accès sont difficiles à détecter et comment les prévenir.