This was a web challenge where we had to bypass a file upload check. We were also given a Python file showing how the backend worked. It looked fairly simple at first, but the custom MIME type, skateboarding/dog, was the part that got me stuck.
The uploaded filename had to end in .dog, and mimetypes.guess_type() had to return skateboarding/dog:
if Path(filename).suffix != ".dog":
return jsonify(error="i don't like that extension!"), 400
mime_type, _ = mimetypes.guess_type(filename)
if mime_type != "skateboarding/dog":
return jsonify(error="i don't like that filetype!"), 400
My first attempt was to change the upload’s Content-Type header to skateboarding/dog. I had been researching MIME bypasses, so that was the first thing I tried.
Content-Type: skateboarding/dog
That didn’t work. Looking back at the provided code, I realised it wasn’t checking the header at all. It was using the filename:
filename = user_file.filename
mime_type, _ = mimetypes.guess_type(filename)
So I started looking into how mimetypes.guess_type() actually worked. I initially thought it only checked file extensions, but after reading the source, I found that it also supports data: URLs. For those, it takes the MIME type from the part before the comma.
That gave me a way to supply the custom MIME type through the filename while still ending it with .dog. An example of the payload is:
data:skateboarding/dog,test.dog
The two checks interpret that same filename differently. Path(filename).suffix sees .dog, while mimetypes.guess_type() recognises the data: URL and returns skateboarding/dog.
This small example shows why it passes:
import mimetypes
from pathlib import Path
filename = "data:skateboarding/dog,test.dog"
print(Path(filename).suffix)
print(mimetypes.guess_type(filename))
Output:
.dog
('skateboarding/dog', None)
For the upload, the payload goes in the multipart filename field:
Content-Disposition: form-data; name="file"; filename="data:skateboarding/dog,test.dog"
The main thing I learnt from this challenge was to look more closely at the function being used. I spent time trying to change the HTTP header, but the check was based on the filename the whole time. Which made this challenge took way longer than it should’ve. Once I understood how guess_type() handled data: URLs, the custom MIME type made a lot more sense.