File Upload - CSCG2022

Category: Web
Difficulty: Easy
Author: Kolja


We are given a zip file,, containing a simple website with dockerfiles.
We are also provided with a server running this website.

Visiting the website redirects us to a login page, with an optional sign up link.
If we register an account and log in with it, the site gives us an upload page.
However, we can't use our newly registered account as the upload function is restricted to admin users.
That's pretty much everything we can check with the live version, so let's dig into the code!

At first, let's look at the place where we got stuck, which is the restricted upload function.
We can find the corresponding php file in webserver/src/upload.php.
And here we can see the admin check.

On line 24, the code make an SQL query to check if the current user has a staff flag set, and if not, abort the upload.
Initially, there's seemingly no obvious way around this check.
So perhaps we could head to the login function and try to find an exploit there?
Here we can see something interesting.

At a first glance, this looks like a normal sql query, simply looking for a user in the database.
But what exactly does the BINARY statement do?
In SQL, if you specify BINARY before a certain key you want to look for, that key will then be matched case sensitively.
Usually, the users admin, Admin, and even AdMiN would be treated the same in SQL, at least without the BINARY keyword.
This keyword also appears in the register function, but if we look back at the staff check, there's no BINARY to be found.
So, how exactly could we abuse this?

In this regard, login and register are case sensitive while the check isn't
If we take a look at the sql.sql file, we can see that a user called administrator is created at startup.
This means, that if we create a user called Administrator, the register and login code will then see it as a different user.
This is because the first “A” in the word is now capitalized which the BINARY statement will pick up.
However, the checking function will think that our newly created user is the same admin user created at startup
This way we can get through the check!

So, now that we're at the upload page, why not just upload a php file that would get the flag for us?
Well, if we take another look at the upload.php file, you'll notice that there are a few more checks we will have to deceive.

As the bad_extensions array here contains the extensions ["php", "phtml", "pht"], we cannot simply upload a php file.
Is there any other way to get code execution, without the php extension?
Indeed, there is with the help of htshells.
These abuse the .htaccess file found on many servers to control access to certain files on the webserver.
What these can also do is make the server see one file format as another.
For example, we can tell the server that the extension .hax is actually a PHP file, and it will happily accept it.

As such, the first step in our plan would be upload an htaccess file which makes the server think .hax is actually .php.
Then, we will have to upload a php shell, with the extension .hax.
If we then request this file, the server should execute our code.
Although there is still one last check which makes this entire process a bit harder.

Here, the server checks for the string <? in the uploaded file, and if successful the server will then overwrite the whole file which will destroy the shell.
Though there's one small oversight with this check.

The uploaded file gets put onto the server in line 63, and gets overwritten later in line 74.
This opens the door to an issue called a Race Condition.
Because, theoretically, we could access the shell after the file was uploaded, but before it gets overwritten, right inbetween that code.
For that to work we'll have to buy ourselves some time by making the search for the <? string take much longer which we can achieve by simply making the file we upload bigger.


With this idea in mind, the final plan is as follows:
1) Create an Administrator user to fool the check
2) Upload a malicious .htacces file which makes the server think .hax == .php
3) Upload a .hax file with the shell in it.
4) Finally, access the .hax file to execute our code.
The first two steps can be done manually while the last two require scripts, as race conditions can be finnicky at times and will need multiple tries to be successful.
To which, we will need to be quick or else we won't be able to make that time window.

The first step is relatively simple, register the account, and log into it, then upload an .htaccess file with the following contents:
AddType application/x-httpd-php .hax
This lets the .hax file extension be associated with the PHP file type, which in turn makes the server think that .hax files have PHP code in them.
Then, we copy the session token from our browser to put it into the upload script here:

import requests
import random

COOKIES = {"PHPSESSID": "e1e2ae163f2696d8617c7e1e4bfde4c1"}
HAX = "<?= system(\"cat /flag*\"); ?>"

BASE = ""

i = 0

while True:
        FILES = {"fileToUpload": ("cool.hax", "A\n"*random.randint(10000, 100000) + HAX)}

        y = + "/upload.php", files=FILES, cookies=COOKIES, timeout=1)

        print(y.status_code, i)

        i += 1
    except Exception:

All this script does is upload the file cool.hax over and over again, with varying amounts of A's prepended to it in order to slow down the search.
The second step is to then execute this continually uploaded shell.

import requests

BASE = ""

i = 0

while True:
        y = requests.get(BASE + "/uploads/cool.hax", timeout=1)

        print(y.status_code, i)
        if not "Nice" in y.text:

        i += 1
    except Exception:

This script requests the newly uploaded file until it finds that the file has been successfully executed.
We can check this by looking for the "Nice try hackers!" string our shell gets replaced with when the upload fails.

If we now run both scripts against the server, after some time, the flag will appear!

Yes, yes we did!


There have been multiple points of failure here which all could have been mitigated.
For one, be consistent with case sensitivity.
Either make all queries BINARY or none, so this discrepancy which lead to account takeover cannot happen.
The second mitigation: check the file *before* fully uploading it to the server.
The server should check the data of the query before moving it into a public directory.
And my last mitigation: use whitelists instead of blacklists.
If the extension blacklist was a whitelist of image formats, the .htaccess file couldn't have been uploaded.

~sw1tchbl4d3, 29/05/2022 (dd/mm/yyyy)