Basic Java Deserialization with ObjectInputStream readObject

Tip

Lerne & übe AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lerne & übe GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lerne & übe Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Durchsuche den vollständigen HackTricks Training-Katalog nach den Assessment-Tracks (ARTA/GRTA/AzRTA) und Linux Hacking Expert (LHE).

Support HackTricks

In diesem Beitrag wird ein Beispiel erklärt, das java.io.Serializable verwendet und warum das Überschreiben von readObject() extrem gefährlich sein kann, wenn der eingehende Stream vom Angreifer kontrolliert wird.

Serializable

Das Java-Interface Serializable (java.io.Serializable) ist ein Marker-Interface, das Ihre Klassen implementieren müssen, wenn sie serialisiert und deserialisiert werden sollen. Das Serialisieren (Schreiben) von Java-Objekten erfolgt mit dem ObjectOutputStream und das Deserialisieren (Lesen) mit dem ObjectInputStream.

Reminder: Which methods are implicitly invoked during deserialization?

  1. readObject() – klassenspezifische Lese-Logik (falls implementiert und private).
  2. readResolve() – kann das deserialisierte Objekt durch ein anderes ersetzen.
  3. validateObject() – über ObjectInputValidation-Callbacks.
  4. readExternal() – für Klassen, die Externalizable implementieren.
  5. Konstruktoren werden nicht ausgeführt – daher verlassen sich Gadget-Ketten ausschließlich auf die vorherigen Callbacks.

Jede Methode in dieser Kette, die am Ende vom Angreifer kontrollierte Daten aufruft (Befehlsausführung, JNDI-Lookups, Reflection, etc.), verwandelt die Deserialisierungsroutine in ein RCE-Gadget.

Lets see an example with a class Person which is serializable. This class overwrites the readObject function, so when any object of this class is deserialized this function is going to be executed.
Im Beispiel ruft die readObject-Funktion der Klasse Person die Funktion eat() ihres Haustiers auf, und die Funktion eat() eines Dog (aus irgendeinem Grund) startet eine calc.exe. Wir werden sehen, wie man ein Person-Objekt serialisiert und deserialisiert, um diesen Taschenrechner auszuführen:

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");
}
}

Fazit (klassisches Szenario)

Wie in diesem sehr einfachen Beispiel zu sehen ist, entsteht die „Schwachstelle“ hier, weil die readObject()-Methode anderen attacker-controlled Code aufruft. In real-world gadget chains können Tausende von Klassen in externen Bibliotheken (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, etc.) missbraucht werden – der attacker benötigt nur ein erreichbares gadget, um Code-Ausführung zu erlangen.


2023-2025: Was hat sich bei realen Java-Deserialisierungs-Bugs geändert?

Aktuelle Fälle erinnern daran, dass ObjectInputStream-Bugs nicht mehr nur darauf beschränkt sind, eine .ser-Datei an einen veralteten HTTP-Endpunkt hochzuladen:

  • Broker / queue consumers: Spring-Kafka (CVE-2023-34040) zeigte, dass das Deserialisieren von exception headers aus attacker-controlled topics ausreicht, wenn der Consumer die ungewöhnlichen checkDeserExWhen*-Flags aktiviert.
  • Client-side trust of remote servers: the Aerospike Java client (CVE-2023-36480) deserialisierte Objekte, die vom Server empfangen wurden. Die Reaktion des Vendors war bemerkenswert: neuere Clients entfernten Java runtime serialization/deserialization support anstatt zu versuchen, diese hinter einem schwachen Filter zu erhalten.
  • “Restricted” streams are often still too broad: pac4j-core (CVE-2023-25581) versuchte, Deserialisierung mit RestrictedObjectInputStream zu schützen, aber die akzeptierte Klassemenge war immer noch groß genug, um gadget abuse zu ermöglichen.

Die offensive Lehre ist, dass die gefährliche Vertrauensgrenze oft nicht „user uploads a blob“, sondern „eine Komponente, die der Entwickler als vertrauenswürdig ansah, kann Bytes in einen Stream injizieren, die schließlich readObject() erreichen“.

Wenn Sie niedrig-noise reachability checks benötigen, bevor Sie Zeit in vollständige gadget research investieren, verwenden Sie die dedizierten Java-Seiten für:

Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner

readObject() anti-patterns that still create gadget entrypoints

Auch wenn Ihre Klasse selbst kein offensichtliches RCE-Gadget ist, reichen die folgenden Muster aus, um sie ausnutzbar zu machen, wenn attacker-controlled Objekte im Graph eingebettet sind:

  1. Aufruf von überschreibbaren Methoden oder Interface-Methoden aus readObject() (pet.eat() im PoC oben ist das klassische Beispiel).
  2. Durchführen von Lookups, Reflection, Class Loading, Expression Evaluation oder JNDI-Operationen während der Deserialisierung.
  3. Iterieren über attacker-controlled Collections oder Maps, was hashCode(), equals(), Comparators oder Transformers als Nebeneffekte auslösen kann.
  4. Registrieren von ObjectInputValidation-Callbacks, die gefährliche Post-Processing-Schritte durchführen.
  5. Die Annahme, dass „private readObject()“ ausreichenden Schutz bietet. Es kontrolliert nur die Dispatch-Semantik; es macht Deserialisierung nicht sicher.

Moderne Mitigations, die Sie einsetzen sollten

  1. JEP 290 / Serialization Filtering (Java 9+) Verwenden Sie eine Allow-Liste und explizite Graph-Limits:
-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 Bevorzugen Sie dies, wenn dieselbe JVM mehrere Deserialisierungs-Kontexte (RMI, cache replication, message consumers, admin-only imports) hat und jeder eine andere Allow-Liste benötigt.
  2. Keep readObject() boring Rufen Sie nur defaultReadObject() / explizite Feldlesungen auf und führen Sie dann strenge Invariant-Checks durch. Führen Sie keine I/O durch, kein Logging, das attacker-controlled Objekte dereferenziert, keine dynamischen Lookups oder Method Calls auf deserialisierten Sub-Objekten.
  3. If possible, remove Java native serialization from the design Der Aerospike-Fix ist ein gutes Modell: Wenn das Feature nicht essentiell ist, ist das Entfernen der Verwendung von readObject() / writeObject() häufig sicherer, als zu versuchen, für immer perfekte Filter zu pflegen.

Detection and research workflow

  • ysoserial bleibt der Ausgangspunkt für gadget validation und schnelle RCE/URLDNS-Probes.
  • marshalsec ist weiterhin nützlich, wenn der sink in JNDI/LDAP/RMI territory pivotiert.
  • GadgetInspector ist nützlich, wenn Sie die target jars haben und nach application-specific gadget chains suchen müssen.
  • Java 17 fügte das jdk.Deserialization Flight Recorder-Event hinzu, das nützlich ist, um zu sehen, wo ObjectInputStream tatsächlich verwendet wird und ob Filter angewendet werden.

Kurze Checkliste für sichere readObject()-Implementierungen

  1. Machen Sie die Methode private und annotieren Sie Serialization-Hooks mit @Serial, damit Compiler falsch deklarierte Signaturen erkennen können.
  2. Rufen Sie defaultReadObject() zuerst auf, es sei denn, Sie haben einen guten Grund, den gesamten Objektgraph manuell zu lesen.
  3. Behandeln Sie jedes verschachtelte Objekt als attacker-controlled, bis es validiert ist.
  4. Rufen Sie niemals Methoden auf deserialisierten collaborators von innerhalb von readObject() auf.
  5. Kombinieren Sie den Code-Review mit einer ObjectInputFilter-Überprüfung; „safe-looking readObject() code“ reicht nicht aus, wenn der Stream weiterhin beliebige Klassen akzeptiert.

Referenzen

Tip

Lerne & übe AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lerne & übe GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lerne & übe Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Durchsuche den vollständigen HackTricks Training-Katalog nach den Assessment-Tracks (ARTA/GRTA/AzRTA) und Linux Hacking Expert (LHE).

Support HackTricks