Learning to test file uploads
Almost every website offers the ability for a user to upload a file, usually an an image for their profile photo or when sharing content. There is a 99% chance the developer has created a filter as to what files to allow and what to block, for example they won't want you uploading
.html files for your profile photo!
In this article we will explore how we can determine what filters may be in place, and get an idea into how the developers are determining what file type we are uploading.
When testing file uploads you may find your file is uploaded to a third party domain, such as
.amazonaws.com or even
.cloudfront.net. This means the file has been stored on an external cloud service and thus the malicious file would not affect your target domain. However sometimes companies will add DNS records to subdomains pointing to their cloud service (usually S3 bucket). For example,
media.example.com may be used to render images from their S3 bucket.
How do they determine what the file is?
Imagine when editing your profile they may say only photos are allowed, but how are they actually validating it's a photo? It could be via
.extension or even
mime type. As well as this, are all photos saved in the same format regardless of the photo type we uploaded? For example if you uploaded a .png file, do they simply not care and will save as .jpg regardless? Let's explore some common things we can test in order to test their file uploads.
For the text below, imagine the feature you are testing only allows for .png and .jpg
Testing file extension- Simple, I first test for .txt to check how strict the filter actually is. A .txt file is harmless and may not be filtered and may help me determine how they handle file uploads and they may be filtering based on malicious extensions, rather than valid extensions. If the .txt file is successful, then I know the file extension is trusted server side. So let's try cause some impact.
If then trying to upload .html, .php, .xml in order to increase impact you notice the extension is filtered due to being malicious, then we can try URL encoding characters such as Null bytes, %00, New lines, %0d%0a and tabs, %09 %07 in order to bypass filters. For example:
------WebKitFormBoundaryAxbOlwnrQnLjU1j9 Content-Disposition: form-data; name="imageupload"; filename="malicious.jpg%0d%0a.html" Content-Type: image/png <html>HTML code!</html>
With the above request, using Burp suite you could then RIGHT CLICK on
%0d%0ain the request, Click Convert Selection and choose URL Decode. You will see your payload "disappear", and now when sending the request your payload will be sent url encoded and may bypass filters. The same can be achieved when using PASTE FROM FILE if you have saved your specific payload via command line.
If changing it to .txt or other extensions does not work, sometimes actually a lot of developers will sometimes create filters for image/*, meaning an .svg upload will work as it's content type is image/svg+xml. .svg files are classed as photos and some developers do not realise they can be used to achieve XSS. Not only this, but sometimes popular Frameworks can be the issue, as for example in Laravel you could have:
The code above says to only allows for image, however as described above an .svg file is considered an image! This would result in XSS. This is defined here in Laravel in validation rules for images.
$request->validate([ 'post_body' => 'required|string|max:150', 'post_image' => 'nullable|image|max:1024', ]);
Testing content type and file headers- If the file extension approach doesn't work, then next we can try playing with the content-type. Leaving the file extension as .jpg along with the correct image headers (ÿøÿà for example), we could simply try changing the Content-Type to text/html. When saved server side, the code may allow the file upload as .jpg is found for the extension, but when viewed the content type will be set to text/html. This is especially common if the URL to browse your file doesn't have an extension by default and typically looks like,
https://www.example.com/media/QmVQcbUNxs3qV991rwAcpJp6mTFFvRV9JcJnEvZNy5Vzvh. This is because the server is happy to save your upload due to it being .jpg with the correct headers, ÿøÿà, but then uses the Content-Type when viewed.
HOWEVER! Changing the Content-Type isn't the only thing we can try here. Sometimes just changing this isn't enough, and it is recommend to add the corrosponding file headers required for the file upload. For example for text/html you'd add
<html>code here, and for application/xml you'd add
Testing filenames- Using certain characters in the actual filename can help bypass filters. For example what happens if you provide a file name of
zseano.php/.jpg- the code may see .jpg and allow, but the server actually writes it to the server as zseano.php and misses everything after the forward slash. Other common characters to try are
`. Don't forget to try something as simple as case sensitive filters,
.PhP, although uncommon in 2022 in my opinion.
If the filename is saved based on your input, then you could check if the filename is on the page anywhere and you can smuggle XSS characters in the filename. Some developers may think users can’t save files with < and > in them, but we are simply modifying the request via our proxy tool (burp suite).
------WebKitFormBoundarySrtFN30pCNmqmNz2 Content-Disposition: form-data; name="file"; filename="<svg onload=confirm()>58832_300x300.jpg" Content-Type: image/jpeg ÿØÿà ....
Example file upload requests
------WebKitFormBoundaryAxbOlwnrQnLjU1j9 Content-Disposition: form-data; name="imageupload"; filename="zseano.jpg" Content-Type: text/html <html><h2>codehere</h2>
Sometimes providing NO file extension (or even filename) will cause it to default to the content-type or file extension.
------WebKitFormBoundaryAxbOlwnrQnLjU1j9 Content-Disposition: form-data; name="imageupload"; filename="zseano." Content-Type: text/html <html><h2>codehere</h2>
------WebKitFormBoundaryAxbOlwnrQnLjU1j9 Content-Disposition: form-data; name="imageupload"; filename=".html" Content-Type: image/png <html>HTML code!</html>
------WebKitFormBoundaryAxbOlwnrQnLjU1j9 Content-Disposition: form-data; name="imageupload"; filename="zseano.jpg#/?&=+\.html" Content-Type: image/jpeg <html><h2>codehere</h2>
------WebKitFormBoundaryAxbOlwnrQnLjU1j9 Content-Disposition: form-data; name="imageupload"; filename="zseano.jpg%0d%0a.php" Content-Type: application/php <?php echo "oops!"; ?>
------WebKitFormBoundaryAxbOlwnrQnLjU1j9 Content-Disposition: form-data; name="imageupload"; filename="zseano.jpg%u0025%u0030%u0039.php" Content-Type: application/php <?php echo "oops!"; ?>
Sometimes if you leave the image header this is enough to bypass the checks. Below is a real payload used on a bug bounty program.
------WebKitFormBoundaryoMZOWnpiPkiDc0yV Content-Disposition: form-data; name="oauth_application[logo_image_file]"; filename="testing1.jpg" Content-Type: text/html ÿØÿà <script>alert(0)</script>