aggregator.rb 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. module Msf
  2. Aggregator_yaml = "#{Msf::Config.config_directory}/aggregator.yaml".freeze # location of the aggregator.yml containing saved aggregator creds
  3. # This plugin provides management and interaction with an external session aggregator.
  4. class Plugin::Aggregator < Msf::Plugin
  5. class AggregatorCommandDispatcher
  6. include Msf::Ui::Console::CommandDispatcher
  7. @response_queue = []
  8. def name
  9. 'Aggregator'
  10. end
  11. def commands
  12. {
  13. 'aggregator_connect' => 'Connect to a running Aggregator instance ( host[:port] )',
  14. 'aggregator_save' => 'Save connection details to an Aggregator instance',
  15. 'aggregator_disconnect' => 'Disconnect from an active Aggregator instance',
  16. 'aggregator_addresses' => 'List all remote ip addresses available for ingress',
  17. 'aggregator_cables' => 'List all remote listeners for sessions',
  18. 'aggregator_cable_add' => 'Setup remote https listener for sessions',
  19. 'aggregator_cable_remove' => 'Stop remote listener for sessions',
  20. 'aggregator_default_forward' => 'forward a unlisted/unhandled sessions to a specified listener',
  21. 'aggregator_sessions' => 'List all remote sessions currently available from the Aggregator instance',
  22. 'aggregator_session_forward' => 'forward a session to a specified listener',
  23. 'aggregator_session_park' => 'Park an existing session on the Aggregator instance'
  24. }
  25. end
  26. def aggregator_verify
  27. if !@aggregator
  28. print_error("No active Aggregator instance has been configured, please use 'aggregator_connect'")
  29. return false
  30. end
  31. true
  32. end
  33. def usage(*lines)
  34. print_status('Usage: ')
  35. lines.each do |line|
  36. print_status(" #{line}")
  37. end
  38. end
  39. def usage_save
  40. usage('aggregator_save')
  41. end
  42. def usage_connect
  43. usage('aggregator_connect host[:port]',
  44. ' -OR- ',
  45. 'aggregator_connect host port')
  46. end
  47. def usage_cable_add
  48. usage('aggregator_cable_add host:port [certificate]',
  49. ' -OR- ',
  50. 'aggregator_cable_add host port [certificate]')
  51. end
  52. def usage_cable_remove
  53. usage('aggregator_cable_remove host:port',
  54. ' -OR- ',
  55. 'aggregator_cable_remove host port')
  56. end
  57. def usage_session_forward
  58. usage('aggregator_session_forward remote_id')
  59. end
  60. def usage_default_forward
  61. usage('aggregator_session_forward')
  62. end
  63. def show_session(details, _target, local_id)
  64. status = pad_space(" #{local_id}", 4)
  65. status += " #{details['ID']}"
  66. status = pad_space(status, 15)
  67. status += ' meterpreter '
  68. status += "#{guess_target_platform(details['OS'])} "
  69. status = pad_space(status, 43)
  70. status += "#{details['USER']} @ #{details['HOSTNAME']} "
  71. status = pad_space(status, 64)
  72. status += "#{details['LOCAL_SOCKET']} -> #{details['REMOTE_SOCKET']}"
  73. print_status status
  74. end
  75. def show_session_detailed(details, target, local_id)
  76. print_status "\t Remote ID: #{details['ID']}"
  77. print_status "\t Type: meterpreter #{guess_target_platform(details['OS'])}"
  78. print_status "\t Info: #{details['USER']} @ #{details['HOSTNAME']}"
  79. print_status "\t Tunnel: #{details['LOCAL_SOCKET']} -> #{details['REMOTE_SOCKET']}"
  80. print_status "\t Via: exploit/multi/handler"
  81. print_status "\t UUID: #{details['UUID']}"
  82. print_status "\t MachineID: #{details['MachineID']}"
  83. print_status "\t CheckIn: #{details['LAST_SEEN'].to_i}s ago" unless details['LAST_SEEN'].nil?
  84. print_status "\tRegistered: Not Yet Implemented"
  85. print_status "\t Forward: #{target}"
  86. print_status "\tSession ID: #{local_id}" unless local_id.nil?
  87. print_status ''
  88. end
  89. def cmd_aggregator_save(*args)
  90. # if we are logged in, save session details to aggregator.yaml
  91. if !args.empty? || args[0] == '-h'
  92. usage_save
  93. return
  94. end
  95. if args[0]
  96. usage_save
  97. return
  98. end
  99. group = 'default'
  100. if (@host && !@host.empty?) && (@port && !@port.empty? && @port.to_i > 0)
  101. config = { group.to_s => { 'server' => @host, 'port' => @port } }
  102. ::File.open(Aggregator_yaml.to_s, 'wb') { |f| f.puts YAML.dump(config) }
  103. print_good("#{Aggregator_yaml} created.")
  104. else
  105. print_error('Missing server/port - reconnect and then try again.')
  106. return
  107. end
  108. end
  109. def cmd_aggregator_connect(*args)
  110. if !args[0] && ::File.readable?(Aggregator_yaml.to_s)
  111. lconfig = YAML.load_file(Aggregator_yaml.to_s)
  112. @host = lconfig['default']['server']
  113. @port = lconfig['default']['port']
  114. aggregator_login
  115. return
  116. end
  117. if args.empty? || args[0].empty? || args[0] == '-h'
  118. usage_connect
  119. return
  120. end
  121. @host = @port = @sslv = nil
  122. case args.length
  123. when 1
  124. @host, @port = args[0].split(':', 2)
  125. @port ||= '2447'
  126. when 2
  127. @host, @port = args
  128. else
  129. usage_connect
  130. return
  131. end
  132. aggregator_login
  133. end
  134. def cmd_aggregator_sessions(*args)
  135. case args.length
  136. when 0
  137. is_detailed = false
  138. when 1
  139. unless args[0] == '-v'
  140. usage_sessions
  141. return
  142. end
  143. is_detailed = true
  144. else
  145. usage_sessions
  146. return
  147. end
  148. return unless aggregator_verify
  149. sessions_list = @aggregator.sessions
  150. return if sessions_list.nil?
  151. session_map = {}
  152. # get details for each session and print in format of sessions -v
  153. sessions_list.each do |session|
  154. session_id, target = session
  155. details = @aggregator.session_details(session_id)
  156. local_id = nil
  157. framework.sessions.each_pair do |key, value|
  158. next unless value.conn_id == session_id
  159. local_id = key
  160. end
  161. # filter session that do not have details as forwarding options (this may change later)
  162. next unless details && details['ID']
  163. session_map[details['ID']] = [details, target, local_id]
  164. end
  165. print_status('Remote sessions')
  166. print_status('===============')
  167. print_status('')
  168. if session_map.empty?
  169. print_status('No remote sessions.')
  170. else
  171. unless is_detailed
  172. print_status(' Id Remote Id Type Information Connection')
  173. print_status(' -- --------- ---- ----------- ----------')
  174. end
  175. session_map.keys.sort.each do |key|
  176. details, target, local_id = session_map[key]
  177. if is_detailed
  178. show_session_detailed(details, target, local_id)
  179. else
  180. show_session(details, target, local_id)
  181. end
  182. end
  183. end
  184. end
  185. def cmd_aggregator_addresses(*_args)
  186. return if !aggregator_verify
  187. address_list = @aggregator.available_addresses
  188. return if address_list.nil?
  189. print_status('Remote addresses found:')
  190. address_list.each do |addr|
  191. print_status(" #{addr}")
  192. end
  193. end
  194. def cmd_aggregator_cable_add(*args)
  195. host, port, certificate = nil
  196. case args.length
  197. when 1
  198. host, port = args[0].split(':', 2)
  199. when 2
  200. host, port = args[0].split(':', 2)
  201. if port.nil?
  202. port = args[1]
  203. else
  204. certificate = args[1]
  205. end
  206. when 3
  207. host, port, certificate = args
  208. else
  209. usage_cable_add
  210. return
  211. end
  212. if !aggregator_verify || args.empty? || args[0] == '-h' || \
  213. port.nil? || port.to_i <= 0
  214. usage_cable_add
  215. return
  216. end
  217. certificate = File.new(certificate).read if certificate && File.exist?(certificate)
  218. @aggregator.add_cable(Metasploit::Aggregator::Cable::HTTPS, host, port, certificate)
  219. end
  220. def cmd_aggregator_cables(*_args)
  221. return if !aggregator_verify
  222. res = @aggregator.cables
  223. print_status('Remote Cables:')
  224. res.each do |k|
  225. print_status(" #{k}")
  226. end
  227. end
  228. def cmd_aggregator_cable_remove(*args)
  229. case args.length
  230. when 1
  231. host, port = args[0].split(':', 2)
  232. when 2
  233. host, port = args
  234. end
  235. if !aggregator_verify || args.empty? || args[0] == '-h' || host.nil?
  236. usage_cable_remove
  237. return
  238. end
  239. @aggregator.remove_cable(host, port)
  240. end
  241. def cmd_aggregator_session_park(*args)
  242. return if !aggregator_verify
  243. case args.length
  244. when 1
  245. session_id = args[0]
  246. s = framework.sessions.get(session_id)
  247. if s.nil?
  248. print_status("#{session_id} is not a valid session.")
  249. elsif @aggregator.sessions.keys.include? s.conn_id
  250. @aggregator.release_session(s.conn_id)
  251. framework.sessions.deregister(s)
  252. else
  253. # TODO: determine if we can add a transport and route with the
  254. # aggregator. For now, just report action not taken.
  255. print_status("#{session_id} does not originate from the aggregator connection.")
  256. end
  257. else
  258. usage('aggregator_session_park session_id')
  259. return
  260. end
  261. end
  262. def cmd_aggregator_default_forward(*_args)
  263. return if !aggregator_verify
  264. @aggregator.register_default(@aggregator.uuid, nil)
  265. end
  266. def cmd_aggregator_session_forward(*args)
  267. return if !aggregator_verify
  268. remote_id = nil
  269. case args.length
  270. when 1
  271. remote_id = args[0]
  272. else
  273. usage_session_forward
  274. return
  275. end
  276. # find session with ID matching request
  277. @aggregator.sessions.each do |session|
  278. session_uri, _target = session
  279. details = @aggregator.session_details(session_uri)
  280. next unless details['ID'] == remote_id
  281. return @aggregator.obtain_session(session_uri, @aggregator.uuid)
  282. end
  283. print_error("#{remote_id} was not found.")
  284. end
  285. def cmd_aggregator_disconnect(*_args)
  286. if @aggregator && @aggregator.available?
  287. # check if this connection is the default forward
  288. @aggregator.register_default(nil, nil) if @aggregator.default == @aggregator.uuid
  289. # now check for any specifically forwarded sessions
  290. local_sessions_by_id = {}
  291. framework.sessions.each_pair do |_id, s|
  292. local_sessions_by_id[s.conn_id] = s
  293. end
  294. sessions = @aggregator.sessions
  295. unless sessions.nil?
  296. sessions.each_pair do |session, console|
  297. next unless local_sessions_by_id.keys.include?(session)
  298. if console == @aggregator.uuid
  299. # park each session locally addressed
  300. cmd_aggregator_session_park(framework.sessions.key(local_sessions_by_id[session]))
  301. else
  302. # simple disconnect session that were from the default forward
  303. framework.sessions.deregister(local_sessions_by_id[session])
  304. end
  305. end
  306. end
  307. end
  308. @aggregator.stop if @aggregator
  309. if @payload_job_ids
  310. @payload_job_ids.each do |id|
  311. framework.jobs.stop_job(id)
  312. end
  313. @payload_job_ids = nil
  314. end
  315. @aggregator = nil
  316. end
  317. def aggregator_login
  318. if !((@host && !@host.empty?) && (@port && !@port.empty? && @port.to_i > 0))
  319. usage_connect
  320. return
  321. end
  322. if (@host != 'localhost') && (@host != '127.0.0.1')
  323. print_error('Warning: SSL connections are not verified in this release, it is possible for an attacker')
  324. print_error(' with the ability to man-in-the-middle the Aggregator traffic to capture the Aggregator')
  325. print_error(' traffic, if you are running this on an untrusted network.')
  326. return
  327. end
  328. # Wrap this so a duplicate session does not prevent access
  329. begin
  330. cmd_aggregator_disconnect
  331. rescue ::Interrupt => e
  332. raise e
  333. rescue ::Exception
  334. end
  335. begin
  336. print_status("Connecting to Aggregator instance at #{@host}:#{@port}...")
  337. @aggregator = Metasploit::Aggregator::ServerProxy.new(@host, @port)
  338. end
  339. aggregator_compatibility_check
  340. unless @payload_job_ids
  341. @payload_job_ids = []
  342. @my_io = local_handler
  343. end
  344. @aggregator.register_response_channel(@my_io)
  345. @aggregator
  346. end
  347. def aggregator_compatibility_check
  348. false if @aggregator.nil?
  349. unless @aggregator.available?
  350. print_error("Connection to aggregator @ #{@host}:#{@port} is unavailable.")
  351. cmd_aggregator_disconnect
  352. end
  353. end
  354. def local_handler
  355. # get a random ephemeral port
  356. server = TCPServer.new('127.0.0.1', 0)
  357. port = server.addr[1]
  358. server.close
  359. multi_handler = framework.exploits.create('multi/handler')
  360. multi_handler.datastore['LHOST'] = '127.0.0.1'
  361. # multi_handler.datastore['PAYLOAD'] = "multi/meterpreter/reverse_https"
  362. multi_handler.datastore['PAYLOAD'] = 'multi/meterpreter/reverse_http'
  363. multi_handler.datastore['LPORT'] = port.to_s
  364. # %w(DebugOptions PrependMigrate PrependMigrateProc
  365. # InitialAutoRunScript AutoRunScript CAMPAIGN_ID HandlerSSLCert
  366. # StagerVerifySSLCert PayloadUUIDTracking PayloadUUIDName
  367. # IgnoreUnknownPayloads SessionRetryTotal SessionRetryWait
  368. # SessionExpirationTimeout SessionCommunicationTimeout).each do |opt|
  369. # multi_handler.datastore[opt] = datastore[opt] if datastore[opt]
  370. # end
  371. multi_handler.datastore['ExitOnSession'] = false
  372. multi_handler.datastore['EXITFUNC'] = 'thread'
  373. multi_handler.exploit_simple(
  374. 'LocalInput' => nil,
  375. 'LocalOutput' => nil,
  376. 'Payload' => multi_handler.datastore['PAYLOAD'],
  377. 'RunAsJob' => true
  378. )
  379. @payload_job_ids << multi_handler.job_id
  380. # requester = Metasploit::Aggregator::Http::SslRequester.new(multi_handler.datastore['LHOST'], multi_handler.datastore['LPORT'])
  381. requester = Metasploit::Aggregator::Http::Requester.new(multi_handler.datastore['LHOST'], multi_handler.datastore['LPORT'])
  382. requester
  383. end
  384. # borrowed from Msf::Sessions::Meterpreter for now
  385. def guess_target_platform(os)
  386. case os
  387. when /windows/i
  388. Msf::Module::Platform::Windows.realname.downcase
  389. when /darwin/i
  390. Msf::Module::Platform::OSX.realname.downcase
  391. when /mac os ?x/i
  392. # this happens with java on OSX (for real!)
  393. Msf::Module::Platform::OSX.realname.downcase
  394. when /freebsd/i
  395. Msf::Module::Platform::FreeBSD.realname.downcase
  396. when /openbsd/i, /netbsd/i
  397. Msf::Module::Platform::BSD.realname.downcase
  398. else
  399. Msf::Module::Platform::Linux.realname.downcase
  400. end
  401. end
  402. def pad_space(status, length)
  403. status << ' ' while status.length < length
  404. status
  405. end
  406. private :guess_target_platform
  407. private :aggregator_login
  408. private :aggregator_compatibility_check
  409. private :aggregator_verify
  410. private :local_handler
  411. private :pad_space
  412. private :show_session
  413. private :show_session_detailed
  414. end
  415. #
  416. # Plugin initialization
  417. #
  418. def initialize(framework, opts)
  419. super
  420. #
  421. # Require the metasploit/aggregator gem, but fail nicely if it's not there.
  422. #
  423. begin
  424. require 'metasploit/aggregator'
  425. rescue LoadError
  426. raise 'WARNING: metasploit/aggregator is not available for now.'
  427. end
  428. add_console_dispatcher(AggregatorCommandDispatcher)
  429. print_status('Aggregator interaction has been enabled')
  430. end
  431. def cleanup
  432. remove_console_dispatcher('Aggregator')
  433. end
  434. def name
  435. 'aggregator'
  436. end
  437. def desc
  438. 'Interacts with the external Session Aggregator'
  439. end
  440. end
  441. end