Basic Java Deserialization with ObjectInputStream readObject
Tip
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
In this POST it’s going to be explained an example using java.io.Serializable and why overriding readObject() can be extremely dangerous if the incoming stream is attacker-controlled.
Serializable
The Java Serializable interface (java.io.Serializable) is a marker interface your classes must implement if they are to be serialized and deserialized. Java object serialization (writing) is done with the ObjectOutputStream and deserialization (reading) is done with the ObjectInputStream.
Reminder: Which methods are implicitly invoked during deserialization?
readObject()– class-specific read logic (if implemented and private).readResolve()– can replace the deserialized object with another one.validateObject()– viaObjectInputValidationcallbacks.readExternal()– for classes implementingExternalizable.- Constructors are not executed – therefore gadget chains rely exclusively on the previous callbacks.
Any method in that chain that ends up invoking attacker-controlled data (command execution, JNDI lookups, reflection, etc.) turns the deserialization routine into an 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.
In the example, the readObject function of the class Person calls the function eat() of his pet and the function eat() of a Dog (for some reason) calls a calc.exe. We are going to see how to serialize and deserialize a Person object to execute this calculator:
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 (classic scenario)
As you can see in this very basic example, the “vulnerability” here appears because the readObject() method is calling other attacker-controlled code. In real-world gadget chains, thousands of classes contained in external libraries (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, etc.) can be abused – the attacker only needs one reachable gadget to get code execution.
2023-2025: What changed in real-world Java deserialization bugs?
Recent cases are a good reminder that ObjectInputStream bugs are no longer just “upload a .ser file to a legacy HTTP endpoint”:
- Broker / queue consumers: Spring-Kafka (
CVE-2023-34040) showed that deserializing exception headers from attacker-controlled topics is enough if the consumer enables the unusualcheckDeserExWhen*flags. - Client-side trust of remote servers: the Aerospike Java client (
CVE-2023-36480) deserialized objects received from the server. The vendor response was notable: newer clients removed Java runtime serialization/deserialization support instead of trying to preserve it behind a weak filter. - “Restricted” streams are often still too broad:
pac4j-core(CVE-2023-25581) tried to protect deserialization withRestrictedObjectInputStream, but the accepted class set was still large enough to make gadget abuse possible.
The offensive lesson is that the dangerous trust boundary is often not “user uploads a blob”, but “some component the developer considered trusted can inject bytes into a stream that eventually reaches readObject()”.
If you need low-noise reachability checks before spending time on full gadget research, use the dedicated Java pages for:
Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner
readObject() anti-patterns that still create gadget entrypoints
Even if your class itself is not an obvious RCE gadget, the following patterns are enough to make it exploitable when attacker-controlled objects are embedded in the graph:
- Calling overridable methods or interface methods from
readObject()(pet.eat()in the PoC above is the classic example). - Performing lookups, reflection, class loading, expression evaluation, or JNDI operations during deserialization.
- Iterating over attacker-controlled collections or maps, which may trigger
hashCode(),equals(), comparators, or transformers as side effects. - Registering
ObjectInputValidationcallbacks that perform dangerous post-processing. - Assuming “private
readObject()” is enough protection. It only controls dispatch semantics; it does not make deserialization safe.
Modern mitigations you should deploy
- JEP 290 / Serialization Filtering (Java 9+)
Use an allow-list and explicit graph limits:-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*" - 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(); } - JEP 415 (Java 17+) Context-Specific Filter Factories
Prefer this when the same JVM has multiple deserialization contexts (RMI, cache replication, message consumers, admin-only imports) and each one needs a different allow-list. - Keep
readObject()boring
Only calldefaultReadObject()/ explicit field reads, then perform strict invariant checks. Do not do I/O, logging that dereferences attacker-controlled objects, dynamic lookups, or method calls on deserialized sub-objects. - If possible, remove Java native serialization from the design
The Aerospike fix is a good model: when the feature is not essential, deletingreadObject()/writeObject()usage is often safer than trying to maintain perfect filters forever.
Detection and research workflow
ysoserialremains the baseline for gadget validation and quick RCE/URLDNS probes.marshalsecis still useful when the sink pivots into JNDI/LDAP/RMI territory.GadgetInspectoris useful when you have the target jars and need to look for application-specific gadget chains.- Java 17 added the
jdk.DeserializationFlight Recorder event, which is useful for seeing whereObjectInputStreamis actually used and whether filters are being applied.
Quick checklist for secure readObject() implementations
- Make the method
privateand annotate serialization hooks with@Serialso compilers can catch mis-declared signatures. - Call
defaultReadObject()first unless you have a strong reason to manually read the full object graph. - Treat every nested object as attacker-controlled until validated.
- Never invoke methods on deserialized collaborators from inside
readObject(). - Pair the code review with an
ObjectInputFilterreview; “safe-lookingreadObject()code” is not enough if the stream still accepts arbitrary classes.
References
- OpenJDK JEP 415: Context-Specific Deserialization Filters
- GitHub Security Lab: GHSL-2022-085 / CVE-2023-25581 (
pac4j-coredeserialization leading to RCE)
Tip
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.


