basic-java-deserialization-objectinputstream-readobject.md (11499B)
1 --- 2 title: "Basic Java Deserialization with ObjectInputStream readObject" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/deserialization/basic-java-deserialization-objectinputstream-readobject.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/deserialization/basic-java-deserialization-objectinputstream-readobject.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Basic Java Deserialization with ObjectInputStream readObject 14 15 This page explains an example using `java.io.Serializable` and why implementing `readObject()` can be extremely dangerous when the incoming stream is attacker-controlled. 16 17 ## Serializable 18 19 The Java `Serializable` interface (`java.io.Serializable`) is a marker interface a class implements to participate in native Java object serialization. `ObjectOutputStream` writes object graphs and `ObjectInputStream` reconstructs them.<sup>[[6]](#references)</sup><sup>[[7]](#references)</sup><sup>[[8]](#references)</sup> 20 21 ### Reminder: Which methods are implicitly invoked during deserialization? 22 23 1. `readObject()` – class-specific read logic (if implemented and *private*). 24 2. `readResolve()` – can replace the deserialized object with another one. 25 3. `validateObject()` – via `ObjectInputValidation` callbacks. 26 4. `readExternal()` – for classes implementing `Externalizable`. 27 5. Constructors and field initializers of serializable classes are not executed. However, the no-argument constructor of the first non-serializable superclass does run, and records use their canonical constructor.<sup>[[6]](#references)</sup> 28 29 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. 30 31 The following example defines a serializable **`Person`** class with a private `readObject()` method, which Java invokes while deserializing an instance of that class.\ 32 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:** 33 34 **The following example is from <https://medium.com/@knownsec404team/java-deserialization-tool-gadgetinspector-first-glimpse-74e99e493649>**<sup>[[3]](#references)</sup> 35 36 ```java 37 import java.io.Serializable; 38 import java.io.*; 39 40 public class TestDeserialization { 41 interface Animal { 42 public void eat(); 43 } 44 //Class must implements Serializable to be serializable 45 public static class Cat implements Animal,Serializable { 46 @Override 47 public void eat() { 48 System.out.println("cat eat fish"); 49 } 50 } 51 //Class must implements Serializable to be serializable 52 public static class Dog implements Animal,Serializable { 53 @Override 54 public void eat() { 55 try { 56 Runtime.getRuntime().exec("calc"); 57 } catch (IOException e) { 58 e.printStackTrace(); 59 } 60 System.out.println("dog eat bone"); 61 } 62 } 63 //Class must implements Serializable to be serializable 64 public static class Person implements Serializable { 65 private Animal pet; 66 public Person(Animal pet){ 67 this.pet = pet; 68 } 69 //readObject implementation, will call the readObject from ObjectInputStream and then call pet.eat() 70 private void readObject(java.io.ObjectInputStream stream) 71 throws IOException, ClassNotFoundException { 72 pet = (Animal) stream.readObject(); 73 pet.eat(); 74 } 75 } 76 public static void GeneratePayload(Object instance, String file) 77 throws Exception { 78 //Serialize the constructed payload and write it to the file 79 File f = new File(file); 80 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); 81 out.writeObject(instance); 82 out.flush(); 83 out.close(); 84 } 85 public static void payloadTest(String file) throws Exception { 86 //Read the written payload and deserialize it 87 ObjectInputStream in = new ObjectInputStream(new FileInputStream(file)); 88 Object obj = in.readObject(); 89 System.out.println(obj); 90 in.close(); 91 } 92 public static void main(String[] args) throws Exception { 93 // Example to call Person with a Dog 94 Animal animal = new Dog(); 95 Person person = new Person(animal); 96 GeneratePayload(person,"test.ser"); 97 payloadTest("test.ser"); 98 // Example to call Person with a Cat 99 //Animal animal = new Cat(); 100 //Person person = new Person(animal); 101 //GeneratePayload(person,"test.ser"); 102 //payloadTest("test.ser"); 103 } 104 } 105 ``` 106 107 ### Conclusion (classic scenario) 108 109 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. 110 111 --- 112 113 ## 2023-2025: What changed in real-world Java deserialization bugs? 114 115 Recent cases are a good reminder that `ObjectInputStream` bugs are no longer just “upload a `.ser` file to a legacy HTTP endpoint”: 116 117 * **Broker / queue consumers**: Spring-Kafka (`CVE-2023-34040`) showed that deserializing exception headers from attacker-controlled topics is enough if the consumer enables the unusual `checkDeserExWhen*` flags.<sup>[[4]](#references)</sup> 118 * **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.<sup>[[5]](#references)</sup> 119 * **“Restricted” streams are often still too broad**: `pac4j-core` (`CVE-2023-25581`) tried to protect deserialization with `RestrictedObjectInputStream`, but the accepted class set was still large enough to make gadget abuse possible.<sup>[[2]](#references)</sup> 120 121 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()`”. 122 123 If you need low-noise reachability checks before spending time on full gadget research, use the dedicated Java pages for: 124 125 [Java Dns Deserialization And Gadgetprobe](/hacktricks/pentesting-web/deserialization/java-dns-deserialization-and-gadgetprobe) 126 127 ## `readObject()` anti-patterns that still create gadget entrypoints 128 129 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: 130 131 1. Calling overridable methods or interface methods from `readObject()` (`pet.eat()` in the PoC above is the classic example). 132 2. Performing lookups, reflection, class loading, expression evaluation, or JNDI operations during deserialization. 133 3. Iterating over attacker-controlled collections or maps, which may trigger `hashCode()`, `equals()`, comparators, or transformers as side effects. 134 4. Registering `ObjectInputValidation` callbacks that perform dangerous post-processing. 135 5. Assuming “private `readObject()`” is enough protection. It only controls dispatch semantics; it does **not** make deserialization safe. 136 137 ## Modern mitigations you should deploy 138 139 1. **JEP 290 / Serialization Filtering (Java 9+)** 140 Use an allow-list and explicit graph limits: 141 ```bash 142 -Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*" 143 ``` 144 2. **Apply a filter on every untrusted stream, not just globally**: 145 ```java 146 try (var ois = new ObjectInputStream(input)) { 147 var filter = ObjectInputFilter.Config.createFilter( 148 "com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;!*" 149 ); 150 ois.setObjectInputFilter(filter); 151 return (Message) ois.readObject(); 152 } 153 ``` 154 3. **JEP 415 (Java 17+) Context-Specific Filter Factories**<sup>[[1]](#references)</sup> 155 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. 156 4. **Keep `readObject()` boring** 157 Only call `defaultReadObject()` / 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. 158 5. **If possible, remove Java native serialization from the design** 159 The Aerospike fix is a good model: when the feature is not essential, deleting `readObject()` / `writeObject()` usage is often safer than trying to maintain perfect filters forever. 160 161 ## Detection and research workflow 162 163 * `ysoserial` remains the baseline for gadget validation and quick RCE/URLDNS probes. 164 * `marshalsec` is still useful when the sink pivots into JNDI/LDAP/RMI territory. 165 * `GadgetInspector` is useful when you have the target jars and need to look for application-specific gadget chains. 166 * Java 17 added the `jdk.Deserialization` Flight Recorder event, which is useful for seeing where `ObjectInputStream` is actually used and whether filters are being applied. 167 168 ## Quick checklist for secure `readObject()` implementations 169 170 1. Make the method `private` and annotate serialization hooks with `@Serial` so compilers can catch mis-declared signatures. 171 2. Call `defaultReadObject()` first unless you have a strong reason to manually read the full object graph. 172 3. Treat every nested object as attacker-controlled until validated. 173 4. Never invoke methods on deserialized collaborators from inside `readObject()`. 174 5. Pair the code review with an `ObjectInputFilter` review; “safe-looking `readObject()` code” is not enough if the stream still accepts arbitrary classes. 175 176 ## References 177 178 - [1] [OpenJDK JEP 415: Context-Specific Deserialization Filters](https://openjdk.org/jeps/415) 179 - [2] [GitHub Security Lab: GHSL-2022-085 / CVE-2023-25581 (`pac4j-core` deserialization leading to RCE)](https://securitylab.github.com/advisories/GHSL-2022-085_pac4j/) 180 - [3] [Java Deserialization Tool: GadgetInspector First Glimpse](https://medium.com/@knownsec404team/java-deserialization-tool-gadgetinspector-first-glimpse-74e99e493649) 181 - [4] [Spring Security Advisory: CVE-2023-34040 (Spring for Apache Kafka)](https://spring.io/security/cve-2023-34040) 182 - [5] [Aerospike Security Advisory: CVE-2023-36480 - Aerospike Java Client vulnerable to unsafe deserialization of server responses](https://github.com/aerospike/aerospike-client-java/security/advisories/GHSA-jj95-55cr-9597) 183 - [6] [Oracle – Java Object Serialization Specification: Object Input Classes](https://docs.oracle.com/en/java/javase/17/docs/specs/serialization/input.html) 184 - [7] [Jenkov – Java `ObjectOutputStream`](https://jenkov.com/tutorials/java-io/objectoutputstream.html) 185 - [8] [Jenkov – Java `ObjectInputStream`](https://jenkov.com/tutorials/java-io/objectinputstream.html)