Désérialisation Java basique avec ObjectInputStream readObject

Tip

Apprenez et pratiquez AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Parcourez le catalogue complet de HackTricks Training pour les parcours d’évaluation (ARTA/GRTA/AzRTA) et Linux Hacking Expert (LHE).

Support HackTricks

Dans ce POST il sera expliqué un exemple utilisant java.io.Serializable et pourquoi redéfinir readObject() peut être extrêmement dangereux si le flux entrant est contrôlé par un attaquant.

Serializable

L’interface Java Serializable (java.io.Serializable) est une interface marqueur que vos classes doivent implémenter si elles doivent être sérialisées et désérialisées. La sérialisation d’objets Java (écriture) se fait avec le ObjectOutputStream et la désérialisation (lecture) se fait avec le ObjectInputStream.

Rappel : Quelles méthodes sont invoquées implicitement lors de la désérialisation ?

  1. readObject() – logique de lecture spécifique à la classe (si implémentée et private).
  2. readResolve() – peut remplacer l’objet désérialisé par un autre.
  3. validateObject() – via des callbacks ObjectInputValidation.
  4. readExternal() – pour les classes implémentant Externalizable.
  5. Les constructeurs ne sont pas exécutés – par conséquent, les chaînes de gadgets reposent exclusivement sur les callbacks précédents.

Toute méthode de cette chaîne qui finit par invoquer des données contrôlées par un attaquant (exécution de commandes, recherches JNDI, réflexion, etc.) transforme la routine de désérialisation en un gadget RCE.

Voyons un exemple avec une classe Person qui est sérialisable. Cette classe redéfinit la fonction readObject, donc lorsque n’importe quel objet de cette classe est désérialisé cette fonction va être exécutée.
Dans l’exemple, la fonction readObject de la classe Person appelle la fonction eat() de son pet et la fonction eat() d’un Dog (pour une raison quelconque) lance un calc.exe. Nous allons voir comment sérialiser et désérialiser un objet Person pour exécuter ce calculateur :

The following example is from https://medium.com/@knownsec404team/java-deserialization-tool-gadgetinspector-first-glimpse-74e99e493649

import java.io.Serializable;
import java.io.*;

public class TestDeserialization {
interface Animal {
public void eat();
}
//Class must implements Serializable to be serializable
public static class Cat implements Animal,Serializable {
@Override
public void eat() {
System.out.println("cat eat fish");
}
}
//Class must implements Serializable to be serializable
public static class Dog implements Animal,Serializable {
@Override
public void eat() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("dog eat bone");
}
}
//Class must implements Serializable to be serializable
public static class Person implements Serializable {
private Animal pet;
public Person(Animal pet){
this.pet = pet;
}
//readObject implementation, will call the readObject from ObjectInputStream  and then call pet.eat()
private void readObject(java.io.ObjectInputStream stream)
throws IOException, ClassNotFoundException {
pet = (Animal) stream.readObject();
pet.eat();
}
}
public static void GeneratePayload(Object instance, String file)
throws Exception {
//Serialize the constructed payload and write it to the file
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
//Read the written payload and deserialize it
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Object obj = in.readObject();
System.out.println(obj);
in.close();
}
public static void main(String[] args) throws Exception {
// Example to call Person with a Dog
Animal animal = new Dog();
Person person = new Person(animal);
GeneratePayload(person,"test.ser");
payloadTest("test.ser");
// Example to call Person with a Cat
//Animal animal = new Cat();
//Person person = new Person(animal);
//GeneratePayload(person,"test.ser");
//payloadTest("test.ser");
}
}

Conclusion (scénario classique)

Comme vous pouvez le voir dans cet exemple très basique, la « vulnérabilité » ici apparaît parce que la méthode readObject() appelle du code contrôlé par l’attaquant. Dans les chaînes de gadgets réelles, des milliers de classes contenues dans des bibliothèques externes (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, etc.) peuvent être abusées – l’attaquant n’a besoin que d’un gadget accessible pour obtenir l’exécution de code.


2023-2025 : Qu’est-ce qui a changé dans les vulnérabilités réelles de désérialisation Java ?

Les cas récents rappellent que les bugs de ObjectInputStream ne se limitent plus à « téléverser un fichier .ser vers un endpoint HTTP hérité » :

  • Broker / queue consumers : Spring-Kafka (CVE-2023-34040) a montré que désérialiser les headers d’exception provenant de topics contrôlés par l’attaquant suffit si le consumer active les flags inhabituels checkDeserExWhen*.
  • Client-side trust of remote servers : le client Java Aerospike (CVE-2023-36480) désérialisait des objets reçus du serveur. La réponse du fournisseur était notable : les clients plus récents ont supprimé le support de la sérialisation/désérialisation Java au runtime au lieu d’essayer de le préserver derrière un filtre faible.
  • “Restricted” streams are often still too broad : pac4j-core (CVE-2023-25581) a tenté de protéger la désérialisation avec RestrictedObjectInputStream, mais l’ensemble de classes acceptées était encore suffisamment vaste pour permettre l’abus de gadgets.

La leçon offensive est que la frontière de confiance dangereuse n’est souvent pas « l’utilisateur téléverse un blob », mais « un composant que le développeur considérait comme de confiance peut injecter des octets dans un stream qui finit par atteindre readObject() ».

Si vous avez besoin de vérifications de reachability peu bruyantes avant de passer du temps sur la recherche complète de gadgets, utilisez les pages Java dédiées pour :

Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner

readObject() anti-patterns that still create gadget entrypoints

Même si votre classe n’est pas en soi un gadget RCE évident, les motifs suivants suffisent à la rendre exploitable lorsque des objets contrôlés par l’attaquant sont intégrés au graphe :

  1. Appeler des méthodes substituables (overridable) ou des méthodes d’interface depuis readObject() (pet.eat() dans le PoC ci‑dessus est l’exemple classique).
  2. Effectuer des lookups, de la reflection, du chargement de classes, de l’évaluation d’expressions ou des opérations JNDI pendant la désérialisation.
  3. Itérer sur des collections ou maps contrôlées par l’attaquant, ce qui peut déclencher hashCode(), equals(), des comparators ou des transformers en tant qu’effets secondaires.
  4. Enregistrer des callbacks ObjectInputValidation qui effectuent un post‑traitement dangereux.
  5. Supposer que « private readObject() » suffit comme protection. Cela ne contrôle que la sémantique d’appel (dispatch) ; cela ne rend pas la désérialisation sûre.

Modern mitigations you should deploy

  1. JEP 290 / Serialization Filtering (Java 9+) Utilisez une allow-list et des limites de graphe explicites :
-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*"
  1. Apply a filter on every untrusted stream, not just globally:
try (var ois = new ObjectInputStream(input)) {
var filter = ObjectInputFilter.Config.createFilter(
"com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;!*"
);
ois.setObjectInputFilter(filter);
return (Message) ois.readObject();
}
  1. JEP 415 (Java 17+) Context-Specific Filter Factories Préférez ceci lorsque la même JVM a plusieurs contextes de désérialisation (RMI, réplication de cache, message consumers, imports réservés aux admins) et que chacun nécessite une allow-list différente.
  2. Keep readObject() boring N’appelez que defaultReadObject() / lectures explicites des champs, puis effectuez des vérifications strictes des invariants. Ne faites pas d’I/O, de logging qui déréférence des objets contrôlés par l’attaquant, de lookups dynamiques, ni d’appels de méthodes sur des sous-objets désérialisés.
  3. If possible, remove Java native serialization from the design Le correctif Aerospike est un bon modèle : lorsque la fonctionnalité n’est pas essentielle, supprimer l’utilisation de readObject() / writeObject() est souvent plus sûr que d’essayer de maintenir des filtres parfaits indéfiniment.

Detection and research workflow

  • ysoserial reste la base pour la validation de gadgets et les probes RCE/URLDNS rapides.
  • marshalsec reste utile lorsque le sink pivote vers le territoire JNDI/LDAP/RMI.
  • GadgetInspector est utile lorsque vous avez les jars cibles et devez chercher des chaînes de gadgets spécifiques à l’application.
  • Java 17 a ajouté l’événement Flight Recorder jdk.Deserialization, utile pour voir où ObjectInputStream est effectivement utilisé et si des filtres sont appliqués.

Quick checklist for secure readObject() implementations

  1. Rendez la méthode private et annotez les hooks de sérialisation avec @Serial pour que les compilateurs puissent détecter les signatures mal déclarées.
  2. Appelez defaultReadObject() en premier, sauf si vous avez une bonne raison de lire manuellement l’intégralité du graphe d’objets.
  3. Considérez chaque objet imbriqué comme contrôlé par l’attaquant jusqu’à validation.
  4. N’appelez jamais de méthodes sur des collaborateurs désérialisés depuis l’intérieur de readObject().
  5. Associez la revue de code à une revue d’ObjectInputFilter ; un « code readObject() qui semble sûr » ne suffit pas si le stream accepte toujours des classes arbitraires.

References

Tip

Apprenez et pratiquez AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Parcourez le catalogue complet de HackTricks Training pour les parcours d’évaluation (ARTA/GRTA/AzRTA) et Linux Hacking Expert (LHE).

Support HackTricks