cristi075@home:~$

Mildly secure

HTB Cyber Apocalypse 2023 - (Web) Orbital

‘Orbital’ was one of the challenges in the ‘Web’ category at HTB’s Cyber Apocalypse 2023.
Its difficulty was ‘Easy’ and it involved exploiting SQL Injection and a path traversal vulnerability.

Challenge description

For this challenge, we got access to a Docker container running a web app and the source files of that webapp.

This is how the web app looks like: Login webpage

And these are the files that we have: List of challenge files

Code analysis

First, let’s take a look at the code for this app.
The app is built using python & Flask.
The first interesting thing that I seen was in the database.py file.

Identifying the first vulnerability SQL Injection

def login(username, password):
    # I don't think it's not possible to bypass login because I'm verifying the password later.
    user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True)

    if user:
        passwordCheck = passwordVerify(user['password'], password)

        if passwordCheck:
            token = createJWT(user['username'])
            return token
    else:
        return False

The query appends the username without any kind of filtering or sanitization; this makes the query vulnerable to SQL Injection.

The first result of the query is taken and the user-supplied password is checked against the retrieved one (MD5 hashes match).
This will make exploiting this a bit trickier but it shouldn’t be that hard.

Logging extra information

First, let’s make the application print some extra information so that we can build our malicious query easier.
We’ll change the login function like this:

def login(username, password):
    # I don't think it's not possible to bypass login because I'm verifying the password later.
    print('----')
    print('Query:')
    print(f'SELECT username, password FROM users WHERE username = "{username}"')
    user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True)
    print('Result:')
    print(user)
    print('----')

    if user:
        passwordCheck = passwordVerify(user['password'], password)

        if passwordCheck:
            token = createJWT(user['username'])
            return token
    else:
        return False

In order to make the output of those print statements visible, we also have to modify supervisord.conf

For that, we change this line

command=python /app/run.py

Into this

command=python -u /app/run.py

This is a quick solution found here

Next, I build and run the container by running the ./build-docker.sh script.

And try logging in with test/test

Application logs (with extra information)

The SQL Injection attack

Now let’s build our SQL Injection attack.

First, let’s assume we don’t know the account so we append “or 1=1” to the query.
Note: Since we see how the container is built, we know that the account is admin. This is probably the same in the live version of the container.
However, ignoring that information will lead to building a more robust attack vector.

First, we’ll use this simple query

test" OR 1=1#--

Injection testing

Then, we want to control the first value that’s returned.
For that, we add an UNION to the query and append our own data.
The real data from the database will still come first, but we can add ‘ORDER BY username DESC’ to sort the returned data and make our injected result be the first one.

test" OR 1=1 UNION SELECT "test","test" ORDER BY username DESC#--

Injection 1

Success!

Now we’ll need to replace the second injected “test” with the MD5 value of “test” (since that’s what I used as my password here).

test" OR 1=1 UNION SELECT "test","098f6bcd4621d373cade4e832627b4f6" ORDER BY username DESC#--

Injection 2

And we’re in.

Webpage after login

The flag isn’t right on this page, so we have to keep looking.

The path traversal attack

In the routes.py file we see the following code

@api.route('/export', methods=['POST'])
@isAuthenticated
def exportFile():
    if not request.is_json:
        return response('Invalid JSON!'), 400
    
    data = request.get_json()
    communicationName = data.get('name', '')

    try:
        # Everyone is saying I should escape specific characters in the filename. I don't know why.
        return send_file(f'/communications/{communicationName}', as_attachment=True)
    except:
        return response('Unable to retrieve the communication'), 400

communicationName is appended to the path without any kind of sanitization.
This makes that function vulnerable to path traversal.

Let’s try making a crafted request using BurpSuite to read the content of /etc/passwd.
According to the Dockerfile, the working directory is /app. So the path to /etc/passwd should be ../etc/passwd (Full path:/app/../etc/passwd) Reading /etc/passwd

It works without any issue.

Getting the flag

Now let’s use the same technique to get the flag.

First, we have to find out where we can find the flag in the container.
In the dockerfile, we can see the following lines.

# copy flag
COPY flag.txt /signal_sleuth_firmware
COPY files /communications/

So the flag is copied at /signal_sleuth_firmware

Fake flag

By requesting ‘../signal_sleuth_firmware’ we got the flag (the fake one, for now)

Now, let’s use the the same two attacks on the live app.

Real flag

The two attacks work flawlessly for the live app too.
And we get the flag: HTB{T1m3_b4$3d_$ql1_4r3_fun!!!}

Wait … time based?