# # $Id$ # # This plugin provides management and interaction with an external session aggregator. # # $Revision$ # require "metasploit/aggregator" module Msf Aggregator_yaml = "#{Msf::Config.get_config_root}/aggregator.yaml" # location of the aggregator.yml containing saved aggregator creds class Plugin::Aggregator < Msf::Plugin class AggregatorCommandDispatcher include Msf::Ui::Console::CommandDispatcher @response_queue = [] def name "Aggregator" end def commands { 'aggregator_connect' => "Connect to a running Aggregator instance ( host[:port] )", 'aggregator_save' => "Save connection details to an Aggregator instance", 'aggregator_disconnect' => "Disconnect from an active Aggregator instance", 'aggregator_addresses' => "List all remote ip addresses available for ingress", 'aggregator_cables' => "List all remote listeners for sessions", 'aggregator_cable_add' => "Setup remote https listener for sessions", 'aggregator_cable_remove' => "Stop remote listener for sessions", 'aggregator_default_forward' => "forward a unlisted/unhandled sessions to a specified listener", 'aggregator_sessions' => "List all remote sessions currently available from the Aggregator instance", 'aggregator_session_forward' => "forward a session to a specified listener", 'aggregator_session_park' => "Park an existing session on the Aggregator instance" } end def aggregator_verify if !@aggregator print_error("No active Aggregator instance has been configured, please use 'aggregator_connect'") return false end true end def usage(*lines) print_status("Usage: ") lines.each do |line| print_status(" #{line}") end end def usage_save usage("aggregator_save") end def usage_connect usage("aggregator_connect host[:port]", " -OR- ", "aggregator_connect host port") end def usage_cable_add usage('aggregator_cable_add host:port [certificate]', ' -OR- ', 'aggregator_cable_add host port [certificate]') end def usage_cable_remove usage('aggregator_cable_remove host:port', ' -OR- ', 'aggregator_cable_remove host port') end def usage_session_forward usage("aggregator_session_forward remote_id") end def usage_default_forward usage("aggregator_session_forward") end def show_session(details, target, local_id) status = pad_space(" #{local_id}", 4) status += " #{details['ID']}" status = pad_space(status, 15) status += " meterpreter " status += "#{guess_target_platform(details['OS'])} " status = pad_space(status, 43) status += "#{details['USER']} @ #{details['HOSTNAME']} " status = pad_space(status, 64) status += "#{details['LOCAL_SOCKET']} -> #{details['REMOTE_SOCKET']}" print_status status end def show_session_detailed(details, target, local_id) print_status "\t Remote ID: #{details['ID']}" print_status "\t Type: meterpreter #{guess_target_platform(details['OS'])}" print_status "\t Info: #{details['USER']} @ #{details['HOSTNAME']}" print_status "\t Tunnel: #{details['LOCAL_SOCKET']} -> #{details['REMOTE_SOCKET']}" print_status "\t Via: exploit/multi/handler" print_status "\t UUID: #{details['UUID']}" print_status "\t MachineID: #{details['MachineID']}" print_status "\t CheckIn: #{details['LAST_SEEN'].to_i}s ago" unless details['LAST_SEEN'].nil? print_status "\tRegistered: Not Yet Implemented" print_status "\t Forward: #{target}" print_status "\tSession ID: #{local_id}" unless local_id.nil? print_status "" end def cmd_aggregator_save(*args) # if we are logged in, save session details to aggregator.yaml if args.length > 0 || args[0] == "-h" usage_save return end if args[0] usage_save return end group = "default" if (@host && @host.length.positive?) && (@port && @port.length.positive? && @port.to_i > 0) config = { "#{group}" => { 'server' => @host, 'port' => @port } } ::File.open("#{Aggregator_yaml}", "wb") { |f| f.puts YAML.dump(config) } print_good("#{Aggregator_yaml} created.") else print_error("Missing server/port - reconnect and then try again.") return end end def cmd_aggregator_connect(*args) if !args[0] if ::File.readable?("#{Aggregator_yaml}") lconfig = YAML.load_file("#{Aggregator_yaml}") @host = lconfig['default']['server'] @port = lconfig['default']['port'] aggregator_login return end end if args.length == 0 || args[0].empty? || args[0] == "-h" usage_connect return end @host = @port = @sslv = nil case args.length when 1 @host, @port = args[0].split(':', 2) @port ||= '2447' when 2 @host, @port = args else usage_connect return end aggregator_login end def cmd_aggregator_sessions(*args) case args.length when 0 isDetailed = false when 1 unless args[0] == "-v" usage_sessions return end isDetailed = true else usage_sessions return end return unless aggregator_verify sessions_list = @aggregator.sessions return if sessions_list.nil? session_map = {} # get details for each session and print in format of sessions -v sessions_list.each do |session| session_id, target = session details = @aggregator.session_details(session_id) local_id = nil framework.sessions.each_pair do |key, value| next unless value.conn_id == session_id local_id = key end # filter session that do not have details as forwarding options (this may change later) next unless details && details['ID'] session_map[details['ID']] = [details, target, local_id] end print_status("Remote sessions") print_status("===============") print_status("") if session_map.length == 0 print_status("No remote sessions.") else unless isDetailed print_status(" Id Remote Id Type Information Connection") print_status(" -- --------- ---- ----------- ----------") end session_map.keys.sort.each do |key| details, target, local_id = session_map[key] unless isDetailed show_session(details, target, local_id) else show_session_detailed(details, target, local_id) end end end end def cmd_aggregator_addresses(*_args) return if !aggregator_verify address_list = @aggregator.available_addresses return if address_list.nil? print_status("Remote addresses found:") address_list.each do |addr| print_status(" #{addr}") end end def cmd_aggregator_cable_add(*args) host, port, certificate = nil case args.length when 1 host, port = args[0].split(':', 2) when 2 host, port = args[0].split(':', 2) if port.nil? port = args[1] else certificate = args[1] end when 3 host, port, certificate = args else usage_cable_add return end if !aggregator_verify || args.length == 0 || args[0] == '-h' || \ port.nil? || port.to_i <= 0 usage_cable_add return end certificate = File.new(certificate).read if certificate && File.exists?(certificate) @aggregator.add_cable(Metasploit::Aggregator::Cable::HTTPS, host, port, certificate) end def cmd_aggregator_cables(*_args) return if !aggregator_verify res = @aggregator.cables print_status("Remote Cables:") res.each do |k| print_status(" #{k}") end end def cmd_aggregator_cable_remove(*args) case args.length when 1 host, port = args[0].split(':', 2) when 2 host, port = args end if !aggregator_verify || args.length == 0 || args[0] == '-h' || host.nil? usage_cable_remove return end @aggregator.remove_cable(host, port) end def cmd_aggregator_session_park(*args) return if !aggregator_verify case args.length when 1 session_id = args[0] s = framework.sessions.get(session_id) unless s.nil? if @aggregator.sessions.keys.include? s.conn_id @aggregator.release_session(s.conn_id) framework.sessions.deregister(s) else # TODO: determine if we can add a transport and route with the # aggregator. For now, just report action not taken. print_status("#{session_id} does not originate from the aggregator connection.") end else print_status("#{session_id} is not a valid session.") end else usage('aggregator_session_park session_id') return end end def cmd_aggregator_default_forward(*_args) return if !aggregator_verify @aggregator.register_default(@aggregator.uuid, nil) end def cmd_aggregator_session_forward(*args) return if !aggregator_verify remote_id = nil case args.length when 1 remote_id = args[0] else usage_session_forward return end # find session with ID matching request @aggregator.sessions.each do |session| session_uri, _target = session details = @aggregator.session_details(session_uri) next unless details['ID'] == remote_id return @aggregator.obtain_session(session_uri, @aggregator.uuid) end print_error("#{remote_id} was not found.") end def cmd_aggregator_disconnect(*_args) if @aggregator && @aggregator.available? # check if this connection is the default forward @aggregator.register_default(nil, nil) if @aggregator.default == @aggregator.uuid # now check for any specifically forwarded sessions local_sessions_by_id = {} framework.sessions.each_pair do |_id, s| local_sessions_by_id[s.conn_id] = s end sessions = @aggregator.sessions unless sessions.nil? sessions.each_pair do |session, console| next unless local_sessions_by_id.keys.include?(session) if console == @aggregator.uuid # park each session locally addressed cmd_aggregator_session_park(framework.sessions.key(local_sessions_by_id[session])) else # simple disconnect session that were from the default forward framework.sessions.deregister(local_sessions_by_id[session]) end end end end @aggregator.stop if @aggregator if @payload_job_ids @payload_job_ids.each do |id| framework.jobs.stop_job(id) end @payload_job_ids = nil end @aggregator = nil end def aggregator_login if !((@host && @host.length.positive?) && (@port && @port.length.positive? && @port.to_i > 0)) usage_connect return end if @host != "localhost" and @host != "127.0.0.1" print_error("Warning: SSL connections are not verified in this release, it is possible for an attacker") print_error(" with the ability to man-in-the-middle the Aggregator traffic to capture the Aggregator") print_error(" traffic, if you are running this on an untrusted network.") return end # Wrap this so a duplicate session does not prevent access begin cmd_aggregator_disconnect rescue ::Interrupt => i raise i rescue ::Exception end begin print_status("Connecting to Aggregator instance at #{@host}:#{@port}...") @aggregator = Metasploit::Aggregator::ServerProxy.new(@host, @port) end aggregator_compatibility_check unless @payload_job_ids @payload_job_ids = [] @my_io = local_handler end @aggregator.register_response_channel(@my_io) @aggregator end def aggregator_compatibility_check false if @aggregator.nil? unless @aggregator.available? print_error("Connection to aggregator @ #{@host}:#{@port} is unavailable.") cmd_aggregator_disconnect end end def local_handler # get a random ephemeral port server = TCPServer.new('127.0.0.1', 0) port = server.addr[1] server.close multi_handler = framework.exploits.create('multi/handler') multi_handler.datastore['LHOST'] = "127.0.0.1" # multi_handler.datastore['PAYLOAD'] = "multi/meterpreter/reverse_https" multi_handler.datastore['PAYLOAD'] = "multi/meterpreter/reverse_http" multi_handler.datastore['LPORT'] = "#{port}" # %w(DebugOptions PrependMigrate PrependMigrateProc # InitialAutoRunScript AutoRunScript CAMPAIGN_ID HandlerSSLCert # StagerVerifySSLCert PayloadUUIDTracking PayloadUUIDName # IgnoreUnknownPayloads SessionRetryTotal SessionRetryWait # SessionExpirationTimeout SessionCommunicationTimeout).each do |opt| # multi_handler.datastore[opt] = datastore[opt] if datastore[opt] # end multi_handler.datastore['ExitOnSession'] = false multi_handler.datastore['EXITFUNC'] = 'thread' multi_handler.exploit_simple( 'LocalInput' => nil, 'LocalOutput' => nil, 'Payload' => multi_handler.datastore['PAYLOAD'], 'RunAsJob' => true ) @payload_job_ids << multi_handler.job_id # requester = Metasploit::Aggregator::Http::SslRequester.new(multi_handler.datastore['LHOST'], multi_handler.datastore['LPORT']) requester = Metasploit::Aggregator::Http::Requester.new(multi_handler.datastore['LHOST'], multi_handler.datastore['LPORT']) requester end # borrowed from Msf::Sessions::Meterpreter for now def guess_target_platform(os) case os when /windows/i Msf::Module::Platform::Windows.realname.downcase when /darwin/i Msf::Module::Platform::OSX.realname.downcase when /mac os ?x/i # this happens with java on OSX (for real!) Msf::Module::Platform::OSX.realname.downcase when /freebsd/i Msf::Module::Platform::FreeBSD.realname.downcase when /openbsd/i, /netbsd/i Msf::Module::Platform::BSD.realname.downcase else Msf::Module::Platform::Linux.realname.downcase end end def pad_space(status, length) while status.length < length status << " " end status end private :guess_target_platform private :aggregator_login private :aggregator_compatibility_check private :aggregator_verify private :local_handler private :pad_space private :show_session private :show_session_detailed end # # Plugin initialization # def initialize(framework, opts) super add_console_dispatcher(AggregatorCommandDispatcher) print_status("Aggregator interaction has been enabled") end def cleanup remove_console_dispatcher('Aggregator') end def name "aggregator" end def desc "Interacts with the external Session Aggregator" end end end