How I Find an Arbitrary File Upload Vulnerability with a Unique Bypass

7 months ago 51
BOOK THIS SPACE FOR AD
ARTICLE AD

Peng Zhou

WordPress Bug Bounty Write-up for Patchstack Competition

I am writing this article to elaborate on a very interesting Arbitrary File Upload (AFU) vulnerability I found during my WordPress Bug Bounty journey on Patchstack’s monthly competition. I caught this AFU from the WordPress plugin cubewp-framework <=1.1.12 and reported it to Patchstack in February 2024. Now, it has been patched (not completely) and published publicly with CVE-2024–30500.

OK, no more words wasted, let’s look at the vulnerable code first.

Vulnerable code in WordPress plugin cubewp-framework <=1.1.12 (modified for clarification)

According to the code, this AFU appears due to an unsafe Zip file upload function. The attackers can embed arbitrary types of files (including the *.php files) into the Zip Archive to upload. The code did nothing to check or sanitize the types or extension names for the files extracted from the uploaded Zip Archive.

At first glance, it looks like a stupid AFU that is simple to exploit. But when going further, we find an unlovely code in line 181 that calls the function $this->rmdir_recursive(). This function is defined to remove the uploaded Zip Archive directory recursively with all its files and subdirectories, making our AFU unable to exploit.

To bypass the directory removal, my first idea is to make some race conditions to exploit. That means I can insert a huge file (hundreds of GigaBits) into the Zip Archive and put our malicious PHP files behind this large file. In this way, we can utilize a subtle timing delay when unliking the huge file to access the malicious stuff. However, when I try this method with a 10 GB file, I find the timing is too short, and thus my exploit cannot work robustly.

To have a reliable exploit, I went back to review the implementation of the function rmdir_recursive(), and quickly found a new insight. To explain this, I paste the source code of rmdir_recursive() below:

class CubeWp_Import {
... ...
public function rmdir_recursive($dir) {
foreach(scandir($dir) as $file) {
if ('.' === $file || '..' === $file) continue;
if (is_dir("$dir/$file")) rmdir_recursive("$dir/$file");
else unlink("$dir/$file");
}
rmdir($dir);
}
... ...
}

As can be seen, the rmdir_recursive() function is defined as a recursive function that tries to call itself if the “$dir/$file” is still a directory (i.e., sub-directory).

if (is_dir("$dir/$file")) rmdir_recursive("$dir/$file");

Looks good to this code? The magic is that, the rmdir_recursive() function is a member function of the class CubeWp_Import. In PHP, the class’s member function should be called inside the class body by the keyword $this->. As a result, if there is a sub-directory inside the Zip Archive, the rmdir_recursive() function will call rmdir_recursive() without $this-> beforehand, necessarily unable to search the function in the class scope, hence being crashed by an undefined function error:

Uncaught Error: Call to undefined function rmdir_recursive() in /srv/www/wordpress/wp-content/plugins/cubewp-framework/cube/classes/class-cubewp-import.php:105

Consequently, the execution can be stopped, and the following unlink() function cannot be worked to remove the malicious files.

With this finding, I thus can make a Zip Archive containing a sub-directory to avoid file removal. I show an example I used in my working exploits below:

backdoor.zip:
==>backdoor.php
==>backdoor==>backdoor.php

1, To prevent AFU, the checking and sanitizing of files’ (extension) names and types are necessary. The post-removal is not the best practice.

2, The class’s member function should be called using the keyword $this-> beforehand in PHP.

Read Entire Article