daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)