Skip to content

Commit 4be4a33

Browse files
authored
Merge pull request #2068 from HackTricks-wiki/research_update_src_pentesting-web_deserialization_basic-java-deserialization-objectinputstream-readobject_20260329_130858
Research Update Enhanced src/pentesting-web/deserialization/...
2 parents 2d263ea + f11333b commit 4be4a33

1 file changed

Lines changed: 52 additions & 35 deletions

File tree

src/pentesting-web/deserialization/basic-java-deserialization-objectinputstream-readobject.md

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -100,57 +100,74 @@ As you can see in this very basic example, the “vulnerability” here appears
100100

101101
---
102102

103-
## 2023-2025: What’s new in Java deserialization attacks?
103+
## 2023-2025: What changed in real-world Java deserialization bugs?
104104

105-
* 2023 – CVE-2023-34040: Spring-Kafka deserialization of error-record headers when `checkDeserExWhen*` flags are enabled allowed arbitrary gadget construction from attacker-published topics. Fixed in 3.0.10 / 2.9.11. ¹
106-
* 2023 – CVE-2023-36480: Aerospike Java client trusted-server assumption broken – malicious server replies contained serialized payloads that were deserialized by the client → RCE. ²
107-
* 2023 – CVE-2023-25581: `pac4j-core` user profile attribute parsing accepted `{#sb64}`-prefixed Base64 blobs and deserialized them despite a `RestrictedObjectInputStream`. Upgrade ≥ 4.0.0.
108-
* 2023 – CVE-2023-4528: JSCAPE MFT Manager Service (port 10880) accepted XML-encoded Java objects leading to RCE as root/SYSTEM.
109-
* 2024 – Multiple new gadget chains were added to ysoserial-plus(mod) including Hibernate5, TomcatEmbed, and SnakeYAML 2.x classes that bypass some old filters.
105+
Recent cases are a good reminder that `ObjectInputStream` bugs are no longer just “upload a `.ser` file to a legacy HTTP endpoint”:
106+
107+
* **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.
108+
* **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.
109+
* **“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.
110+
111+
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()`”.
112+
113+
If you need low-noise reachability checks before spending time on full gadget research, use the dedicated Java pages for:
114+
115+
{{#ref}}
116+
java-dns-deserialization-and-gadgetprobe.md
117+
{{#endref}}
118+
119+
## `readObject()` anti-patterns that still create gadget entrypoints
120+
121+
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:
122+
123+
1. Calling overridable methods or interface methods from `readObject()` (`pet.eat()` in the PoC above is the classic example).
124+
2. Performing lookups, reflection, class loading, expression evaluation, or JNDI operations during deserialization.
125+
3. Iterating over attacker-controlled collections or maps, which may trigger `hashCode()`, `equals()`, comparators, or transformers as side effects.
126+
4. Registering `ObjectInputValidation` callbacks that perform dangerous post-processing.
127+
5. Assuming “private `readObject()`” is enough protection. It only controls dispatch semantics; it does **not** make deserialization safe.
110128

111129
## Modern mitigations you should deploy
112130

113131
1. **JEP 290 / Serialization Filtering (Java 9+)**
114-
*Add an allow-list or deny-list of classes:*
132+
Use an allow-list and explicit graph limits:
115133
```bash
116-
# Accept only your DTOs and java.base, reject everything else
117-
-Djdk.serialFilter="com.example.dto.*;java.base/*;!*"
134+
-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*"
118135
```
119-
Programmatic example:
136+
2. **Apply a filter on every untrusted stream, not just globally**:
120137
```java
121-
var filter = ObjectInputFilter.Config.createFilter("com.example.dto.*;java.base/*;!*" );
122-
ObjectInputFilter.Config.setSerialFilter(filter);
123-
```
124-
2. **JEP 415 (Java 17+) Context-Specific Filter Factories** – use a `BinaryOperator<ObjectInputFilter>` to apply different filters per execution context (e.g., per RMI call, per message queue consumer).
125-
3. **Do not expose raw `ObjectInputStream` over the wire** – prefer JSON/Binary encodings without code execution semantics (Jackson after disabling `DefaultTyping`, Protobuf, Avro, etc.).
126-
4. **Defense-in-Depth limits** – Set maximum array length, depth, references:
127-
```bash
128-
-Djdk.serialFilter="maxbytes=16384;maxdepth=5;maxrefs=1000"
138+
try (var ois = new ObjectInputStream(input)) {
139+
var filter = ObjectInputFilter.Config.createFilter(
140+
"com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;!*"
141+
);
142+
ois.setObjectInputFilter(filter);
143+
return (Message) ois.readObject();
144+
}
129145
```
130-
5. **Continuous gadget scanning** – run tools such as `gadget-inspector` or `serialpwn-cli` in your CI to fail the build if a dangerous gadget becomes reachable.
146+
3. **JEP 415 (Java 17+) Context-Specific Filter Factories**
147+
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.
148+
4. **Keep `readObject()` boring**
149+
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.
150+
5. **If possible, remove Java native serialization from the design**
151+
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.
131152

132-
## Updated tooling cheat-sheet (2024)
153+
## Detection and research workflow
133154

134-
* `ysoserial-plus.jar` – community fork with > 130 gadget chains:
135-
```bash
136-
java -jar ysoserial-plus.jar CommonsCollections6 'calc' | base64 -w0
137-
```
138-
* `marshalsec` – still the reference for JNDI gadget generation (LDAP/RMI).
139-
* `gadget-probe` – fast black-box gadget discovery against network services.
140-
* `SerialSniffer` – JVMTI agent that prints every class read by `ObjectInputStream` (useful to craft filters).
141-
* **Detection tip** – enable `-Djdk.serialDebug=true` (JDK 22+) to log filter decisions and rejected classes.
155+
* `ysoserial` remains the baseline for gadget validation and quick RCE/URLDNS probes.
156+
* `marshalsec` is still useful when the sink pivots into JNDI/LDAP/RMI territory.
157+
* `GadgetInspector` is useful when you have the target jars and need to look for application-specific gadget chains.
158+
* 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.
142159

143160
## Quick checklist for secure `readObject()` implementations
144161

145-
1. Make the method `private` and add the `@Serial` annotation (helps static analysis).
146-
2. Never call user-supplied methods or perform I/O in the method – only read fields.
147-
3. If validation is needed, perform it **after** deserialization, outside of `readObject()`.
148-
4. Prefer implementing `Externalizable` and do explicit field reads instead of default serialization.
149-
5. Register a hardened `ObjectInputFilter` even for internal services (compromise-resilient design).
162+
1. Make the method `private` and annotate serialization hooks with `@Serial` so compilers can catch mis-declared signatures.
163+
2. Call `defaultReadObject()` first unless you have a strong reason to manually read the full object graph.
164+
3. Treat every nested object as attacker-controlled until validated.
165+
4. Never invoke methods on deserialized collaborators from inside `readObject()`.
166+
5. Pair the code review with an `ObjectInputFilter` review; “safe-looking `readObject()` code” is not enough if the stream still accepts arbitrary classes.
150167

151168
## References
152169

153-
1. Spring Security Advisory – CVE-2023-34040 Java Deserialization in Spring-Kafka (Aug 2023)
154-
2. GitHub Security LabGHSL-2023-044: Unsafe Deserialization in Aerospike Java Client (Jul 2023)
170+
- [OpenJDK JEP 415: Context-Specific Deserialization Filters](https://openjdk.org/jeps/415)
171+
- [GitHub Security Lab: GHSL-2022-085 / CVE-2023-25581 (`pac4j-core` deserialization leading to RCE)](https://securitylab.github.com/advisories/GHSL-2022-085_pac4j/)
155172

156173
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)