Exploiting File Uploads Pt. 1 – MIME Sniffing to Stored XSS #bugbounty

#TL;DR;

While bug hunting on a private program I was able to find a Stored XSS vulnerability through a file upload functionality. I was able to bypass file type checks and create a malicious HTML file as a GIF by abusing how IE / Edge handle files. I also break down file upload filters, and a bit into my mindset when exploiting them.

#The bad

One of the things I always like to fuzz when I start looking at a new program is file uploads. Vulnerabilities in file uploads will generally give you high severity bugs, and it also seems like developers have a hard time securing them.

Looking at this private program, I noticed that it had a functionality to contact support. In this contact form you are able to upload an attachment. The first thing I noticed is that when I uploaded an image, it was uploading it to the same domain.

Example File Upload Request

Request To upload File

Example Response

{"result":true,"message":"/UploadFiles/redacted/redacted/3021d74f18ddasdasd50abe934f.png,"code":0}

Immediately this caught my attention. Usually it is not a very good practice to store files users can upload in the same location/domain, as it can lead to very nasty vulnerabilities, including Remote Code Execution.

#The ugly…

Filter 1 Bypass

Next thing we need to figure out in order to exploit this, is how to upload malicious files. The first thing I tried, expecting it not to work, was to change the file extension to something like .html . Of course that did not work and we get:

{"result":false,"message":"That file type is not supported.","code":0}

We can conclude there is a filter in place for the file extensions. A quick way we can find out which files are allowed is to bruteforce the extension via Burp Intruder. SecLists has a nice wordlist of file extensions we can use. Unfortunately, the endpoint had rate limiting, and after a few dozen requests our IP address gets a time out. ๐Ÿ™

Switching VPN servers, I got back and started manually testing some extensions. I noticed the web app only accepted : jpg, jpeg, png, and gif. I also tried all the extensions bypass I could think of such as nullbytes, unicode encoding, and others. Anything before the first instance of “.” is ignored because the application creates its own unique file name. However, I noticed that special characters in the extensions were not removed after the extension check, but were ignored during the check itself. For example using the filename badfile.”gif was accepted, however, badfile.foo”gif was not.

Sending the following request:

-----------------------------6683303835495
Content-Disposition: form-data; name="upload"; filename="badfile.''gif"
Content-Type: image/png

GIF89a
@HackerOn2Wheels
-----------------------------6683303835495--

Returned:

{"result":true,"message":"/UploadFiles/redacted/redacted/3021d74f18f649f5ac943ff50abe934f.''gif","code":0}

So it seems the file extension filter works the following way:

  1. Get extension from the [filename] after the last instance of “.” .
  2. Remove all non-alphanumeric characters (not a-z A-Z 0-9) .
  3. Compare against a whitelist of extensions (gif, png, jpg, jpeg) .
  4. If file extension is in the list get the extension from the first step and create the file.

We will see why this is dangerous in a little bit.

Filter 2 bypass Ignore?

Another thing that the web application was also looking at was the file signature, or “magic bytes” as some people call it. Therefore, if I simply tried to upload a file with random data like for example:

-----------------------------6683303835495
Content-Disposition: form-data; name="upload"; filename="badfile.''gif"
Content-Type: image/png

foobar
@HackerOn2Wheels

-----------------------------6683303835495--

It returned:

{"result":false,"message":"That file type is not supported.","code":0}

However, one thing VERY common about file upload filters is that they only look at the first 4 bytes of the file signature. These are the bytes the file upload functionality usually look for in each image type:

JPEG  - FF D8 FF DB - รฟร˜รฟร› 
GIF   - 47 49 46 38 - GIF8
PNG   - 89 50 4E 47 - โ€ฐPNG

So as long as I have one of these in the file contents, our file will be uploaded successful. However, this is unfortunately not enough to protect against malicious files. Most browsers “look for” the actual full file signature headers and others (IE/Edge) don’t care at all. For example for GIF and PNG files signatures are not only 4 bytes. The full signatures are:

GIF - 47 49 46 38 39 61 - GIF89a ( or GIF87a )
PNG - 89 50 4E 47 0D 0A 1A 0A  - .PNG.... 

https://en.wikipedia.org/wiki/List_of_file_signatures

I will dive into this in more detail in the #TakeAways Section of the post.

#alert(‘Combining all the pieces ‘);

Alright so how do we actually exploit this? All we need to do is to upload a file with a bad extension to “confuse” the browser, and add the magic GIF8 bytes to the beginning of the file.

Final payload example:

-----------------------------6683303835495
Content-Disposition: form-data; name="upload"; filename="badfile.''gif"
Content-Type: image/png

GIF8
<html><script>alert('XSS is easy');</script></html>
-----------------------------6683303835495--

Returns:

{"result":true,"message":"/UploadFiles/redacted/redacted/5060bddf6e024def9a8f5f8b9c42ba1f.''gif","code":0}

Now if we visit https://redacted.com/UploadFiles/redacted/redacted/5060bddf6e024def9a8f5f8b9c42ba1f.”gif using Microsoft Edge or Internet Explorer(more on this below) we get:


#TakeAways – Why does this work?

First thing we need to ask ourselves is: how do browsers actually work? **liveoverflow voice**

All jokes aside, for us to understand how browsers will render files let’s create some example files to test. I’ve created three GIF files. The first file contains only the four byte GIF image signature (“GIF8”) , the second file has the full GIF image signature (“GIF89a”) , and the third file does not have any GIF file signature, but it does have the ” .gif “ extention.

GIF with only 4 byte signature
GIF with Full Signature
GIF with no signature but with correct extension

If we use the file tool in Linux we can see how these files are identified.

As we can see here, using file the first 2 files are identified as GIFs based on the file signature, including the one with just 4-bytes signature, and the last file with only the extension is identified as HTML. However, if we open these 3 files in a browser we can see that it handles these files differently.

For example, Edge and IE seem to not care at all about GIF file signature headers. It will render the HTML without skipping a beat.

However it does care about the file extension. THANKS MICROSOFT!

The fact is, IE and Edge are by default “vulnerable” to something called MIME sniffing/ Content Sniffing. In short, Edge and IE will “inspect” the file contents it is trying to access and set it’s content type based on it. So when we create a “badfile.”gif” , it will first read it’s content and set the content type to text/html because we have <html> tags in the content. You can read more about this here. https://en.wikipedia.org/wiki/Content_sniffing

Here is where it get’s interesting. Firefox and Chrome do care about extension and signature. However, it actually only takes into account the full signature. For example opening the four byte signature vs the full signature in Firefox will behave very differently.

File with Full Signature and no Extension.
File with only the 4 byte signature and no extension.

As we can see from the image above, just simply having the first four bytes of the GIF file signature does not make Firefox (nor Chrome) render it as a GIF file. However, the newer versions of Firefox and Chrome do add a pre-wrap to the file contents as seen above, which breaks the html execution. Now is it possible to break this? Or change this behavior? I don’t know yet. Let me know if you do! However since our text is displayed we could potentially use it to social engineer our victim to disable the pre-wrap feature in about:config. It does however makes the exploitability much harder as it requires heavy user interaction, and at that point one could say it would be a case of self-XSS.

In conclusion if you ever face a Image File Upload that lets you “corrupt” the GIF / PNG file extension with special characters or create the file without an extension, you may be able execute JS and html in Edge and IE through MIME/Content sniffing.