Skip to content

Commit c540ba4

Browse files
committed
Land rapid7#5297 : Track machine_id and dead sessions
2 parents 05e4af8 + a577bef commit c540ba4

File tree

7 files changed

+146
-46
lines changed

7 files changed

+146
-46
lines changed

lib/msf/base/serializer/readable_text.rb

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,8 @@ def self.dump_sessions(framework, opts={})
527527
indent = opts[:indent] || DefaultIndent
528528
col = opts[:col] || DefaultColumnWrap
529529

530+
return dump_sessions_verbose(framework, opts) if verbose
531+
530532
columns =
531533
[
532534
'Id',
@@ -535,9 +537,6 @@ def self.dump_sessions(framework, opts={})
535537
'Connection'
536538
]
537539

538-
columns << 'Via' if verbose
539-
columns << 'PayloadId' if verbose
540-
541540
tbl = Rex::Ui::Text::Table.new(
542541
'Indent' => indent,
543542
'Header' => "Active sessions",
@@ -554,12 +553,7 @@ def self.dump_sessions(framework, opts={})
554553

555554
row = [ session.sid.to_s, session.type.to_s, sinfo, session.tunnel_to_s + " (#{session.session_host})" ]
556555
if session.respond_to? :platform
557-
row[1] += " " + session.platform
558-
end
559-
560-
if verbose
561-
row << session.via_exploit.to_s
562-
row << session.payload_uuid.to_s
556+
row[1] << (" " + session.platform)
563557
end
564558

565559
tbl << row
@@ -568,6 +562,59 @@ def self.dump_sessions(framework, opts={})
568562
return framework.sessions.length > 0 ? tbl.to_s : "#{tbl.header_to_s}No active sessions.\n"
569563
end
570564

565+
# Dumps the list of active sessions in verbose mode
566+
#
567+
# @param framework [Msf::Framework] the framework to dump.
568+
# @param opts [Hash] the options to dump with.
569+
# @option opts :session_ids [Array] the list of sessions to dump (no
570+
# effect).
571+
# @return [String] the formatted list of sessions.
572+
def self.dump_sessions_verbose(framework, opts={})
573+
ids = (opts[:session_ids] || framework.sessions.keys).sort
574+
575+
out = "Active sessions\n" +
576+
"===============\n\n"
577+
578+
if framework.sessions.length == 0
579+
out << "No active sessions.\n"
580+
return out
581+
end
582+
583+
framework.sessions.each_sorted do |k|
584+
session = framework.sessions[k]
585+
586+
sess_info = session.info.to_s
587+
sess_id = session.sid.to_s
588+
sess_tunnel = session.tunnel_to_s + " (#{session.session_host})"
589+
sess_via = session.via_exploit.to_s
590+
sess_type = session.type.to_s
591+
sess_uuid = session.payload_uuid.to_s
592+
sess_checkin = "<none>"
593+
sess_machine_id = session.machine_id.to_s
594+
595+
if session.respond_to? :platform
596+
sess_type << (" " + session.platform)
597+
end
598+
599+
if session.respond_to?(:last_checkin) && session.last_checkin
600+
sess_checkin = "#{(Time.now.to_i - session.last_checkin.to_i)}s ago @ #{session.last_checkin.to_s}"
601+
end
602+
603+
out << " Session ID: #{sess_id}\n"
604+
out << " Type: #{sess_type}\n"
605+
out << " Info: #{sess_info}\n"
606+
out << " Tunnel: #{sess_tunnel}\n"
607+
out << " Via: #{sess_via}\n"
608+
out << " UUID: #{sess_uuid}\n"
609+
out << " MachineID: #{sess_machine_id}\n"
610+
out << " CheckIn: #{sess_checkin}\n"
611+
out << "\n"
612+
end
613+
614+
out << "\n"
615+
return out
616+
end
617+
571618
# Dumps the list of running jobs.
572619
#
573620
# @param framework [Msf::Framework] the framework.

lib/msf/base/sessions/meterpreter.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,10 @@ def reset_ui
255255
def kill
256256
begin
257257
cleanup_meterpreter
258-
self.sock.close
258+
self.sock.close if self.sock
259259
rescue ::Exception
260260
end
261+
# deregister will actually trigger another cleanup
261262
framework.sessions.deregister(self)
262263
end
263264

@@ -298,6 +299,24 @@ def load_priv()
298299
console.disable_output = original
299300
end
300301

302+
#
303+
# Validate session information by checking for a machine_id response
304+
#
305+
def is_valid_session?(timeout=10)
306+
return true if self.machine_id
307+
308+
begin
309+
self.machine_id = self.core.machine_id(timeout)
310+
return true
311+
rescue ::Rex::Post::Meterpreter::RequestError
312+
# This meterpreter doesn't support core_machine_id
313+
return true
314+
rescue ::Exception => e
315+
dlog("Session #{self.sid} did not respond to validation request #{e.class}: #{e}")
316+
end
317+
false
318+
end
319+
301320
#
302321
# Populate the session information.
303322
#
@@ -448,6 +467,7 @@ def create(param)
448467
attr_accessor :binary_suffix
449468
attr_accessor :console # :nodoc:
450469
attr_accessor :skip_ssl
470+
attr_accessor :skip_cleanup
451471
attr_accessor :target_id
452472

453473
protected

lib/msf/base/sessions/meterpreter_options.rb

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def initialize(info = {})
1212
register_advanced_options(
1313
[
1414
OptBool.new('AutoLoadStdapi', [true, "Automatically load the Stdapi extension", true]),
15+
OptBool.new('AutoVerifySession', [true, "Automatically verify and drop invalid sessions", true]),
1516
OptString.new('InitialAutoRunScript', [false, "An initial script to run on session creation (before AutoRunScript)", '']),
1617
OptString.new('AutoRunScript', [false, "A script to run automatically on session creation.", '']),
1718
OptBool.new('AutoSystemInfo', [true, "Automatically capture system information on initialization.", true]),
@@ -40,43 +41,49 @@ def on_session(session)
4041

4142
session.init_ui(self.user_input, self.user_output)
4243

43-
if (datastore['AutoLoadStdapi'] == true)
44+
valid = true
4445

45-
session.load_stdapi
46-
47-
if datastore['AutoSystemInfo']
48-
session.load_session_info
46+
if datastore['AutoVerifySession'] == true
47+
if not session.is_valid_session?
48+
print_error("Meterpreter session #{session.sid} is not valid and will be closed")
49+
valid = false
4950
end
51+
end
52+
53+
if valid
54+
55+
if datastore['AutoLoadStdapi'] == true
56+
57+
session.load_stdapi
58+
59+
if datastore['AutoSystemInfo']
60+
session.load_session_info
61+
end
5062

51-
=begin
52-
admin = false
53-
begin
54-
::Timeout.timeout(30) do
55-
if session.railgun and session.railgun.shell32.IsUserAnAdmin()["return"] == true
56-
admin = true
57-
session.info += " (ADMIN)"
58-
end
63+
if session.platform =~ /win32|win64/i
64+
session.load_priv rescue nil
5965
end
60-
rescue ::Exception
6166
end
62-
=end
63-
if session.platform =~ /win32|win64/i
64-
session.load_priv rescue nil
67+
68+
if session.platform =~ /android/i
69+
if datastore['AutoLoadAndroid']
70+
session.load_android
71+
end
6572
end
66-
end
6773

68-
if session.platform =~ /android/i
69-
if datastore['AutoLoadAndroid']
70-
session.load_android
74+
[ 'InitialAutoRunScript', 'AutoRunScript' ].each do |key|
75+
if (datastore[key].empty? == false)
76+
args = Shellwords.shellwords( datastore[key] )
77+
print_status("Session ID #{session.sid} (#{session.tunnel_to_s}) processing #{key} '#{datastore[key]}'")
78+
session.execute_script(args.shift, *args)
79+
end
7180
end
7281
end
7382

74-
[ 'InitialAutoRunScript', 'AutoRunScript' ].each do |key|
75-
if (datastore[key].empty? == false)
76-
args = Shellwords.shellwords( datastore[key] )
77-
print_status("Session ID #{session.sid} (#{session.tunnel_to_s}) processing #{key} '#{datastore[key]}'")
78-
session.execute_script(args.shift, *args)
79-
end
83+
# Terminate the session without cleanup if it did not validate
84+
if not valid
85+
session.skip_cleanup = true
86+
session.kill
8087
end
8188

8289
}

lib/msf/core/session.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ def alive?
389389
#
390390
attr_accessor :payload_uuid
391391
#
392+
# The unique machine identifier for the host that created this session
393+
#
394+
attr_accessor :machine_id
395+
#
392396
# The actual exploit module instance that created this session
393397
#
394398
attr_accessor :exploit

lib/rex/post/meterpreter/client.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,18 @@ def initialize(sock,opts={})
8585
# Cleans up the meterpreter instance, terminating the dispatcher thread.
8686
#
8787
def cleanup_meterpreter
88-
ext.aliases.each_value do | extension |
89-
extension.cleanup if extension.respond_to?( 'cleanup' )
88+
if not self.skip_cleanup
89+
ext.aliases.each_value do | extension |
90+
extension.cleanup if extension.respond_to?( 'cleanup' )
91+
end
9092
end
93+
9194
dispatcher_thread.kill if dispatcher_thread
92-
core.shutdown rescue nil
95+
96+
if not self.skip_cleanup
97+
core.shutdown rescue nil
98+
end
99+
93100
shutdown_passive_dispatcher
94101
end
95102

@@ -474,6 +481,10 @@ def unicode_filter_decode(str)
474481
# A list of the commands
475482
#
476483
attr_reader :commands
484+
#
485+
# The timestamp of the last received response
486+
#
487+
attr_accessor :last_checkin
477488

478489
protected
479490
attr_accessor :parser, :ext_aliases # :nodoc:

lib/rex/post/meterpreter/client_core.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,14 +273,16 @@ def use(mod, opts = { })
273273
return true
274274
end
275275

276-
def machine_id
276+
def machine_id(timeout=nil)
277277
request = Packet.create_request('core_machine_id')
278278

279-
response = client.send_request(request)
279+
args = [ request ]
280+
args << timeout if timeout
281+
282+
response = client.send_request(*args)
280283

281-
id = response.get_tlv_value(TLV_TYPE_MACHINE_ID)
282-
# TODO: Determine if we're going to MD5/SHA1 this
283-
return Rex::Text.md5(id)
284+
mid = response.get_tlv_value(TLV_TYPE_MACHINE_ID)
285+
return Rex::Text.md5(mid)
284286
end
285287

286288
def transport_change(opts={})

lib/rex/post/meterpreter/packet_dispatcher.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ def initialize_passive_dispatcher
7979

8080
def shutdown_passive_dispatcher
8181
return if not self.passive_service
82-
self.passive_service.remove_resource(self.conn_id + "/")
82+
83+
# Ensure that there is only one leading and trailing slash on the URI
84+
resource_uri = "/" + self.conn_id.to_s.gsub(/(^\/|\/$)/, '') + "/"
85+
86+
self.passive_service.remove_resource(resource_uri)
8387

8488
# If there are no more resources registered on the service, stop it entirely
8589
if self.passive_service.resources.empty?
@@ -102,6 +106,8 @@ def on_passive_request(cli, req)
102106
resp['Content-Type'] = 'application/octet-stream'
103107
resp['Connection'] = 'close'
104108

109+
self.last_checkin = Time.now
110+
105111
# If the first 4 bytes are "RECV", return the oldest packet from the outbound queue
106112
if req.body[0,4] == "RECV"
107113
rpkt = send_queue.shift
@@ -494,6 +500,9 @@ def dispatch_inbound_packet(packet, client = nil)
494500
client = self
495501
end
496502

503+
# Update our last reply time
504+
client.last_checkin = Time.now
505+
497506
# If the packet is a response, try to notify any potential
498507
# waiters
499508
if ((resp = packet.response?))

0 commit comments

Comments
 (0)