Files
metasploit-gs/lib/msf/base/sessions/command_shell.rb
T

389 lines
9.1 KiB
Ruby

# -*- coding: binary -*-
require 'msf/base'
require 'msf/base/sessions/scriptable'
require 'shellwords'
require 'rex/text/table'
module Msf
module Sessions
###
#
# This class provides basic interaction with a command shell on the remote
# endpoint. This session is initialized with a stream that will be used
# as the pipe for reading and writing the command shell.
#
###
class CommandShell
#
# This interface supports basic interaction.
#
include Msf::Session::Basic
#
# This interface supports interacting with a single command shell.
#
include Msf::Session::Provider::SingleCommandShell
include Msf::Session::Scriptable
##
# :category: Msf::Session::Scriptable implementors
#
# Executes the supplied script, must be specified as full path.
#
# Msf::Session::Scriptable implementor
#
def execute_file(full_path, args)
o = Rex::Script::Shell.new(self, full_path)
o.run(args)
end
#
# Returns the type of session.
#
def self.type
"shell"
end
def initialize(conn, opts = {})
self.platform ||= ""
self.arch ||= ""
self.max_threads = 1
@cleanup = false
datastore = opts[:datastore]
if datastore && !datastore["CommandShellCleanupCommand"].blank?
@cleanup_command = datastore["CommandShellCleanupCommand"]
end
super
end
#
# Returns the session description.
#
def desc
"Command shell"
end
#
# List of supported commands.
#
def commands
{
'help' => 'Help menu',
'background' => 'Backgrounds the current shell session',
'sessions' => 'Quickly switch to another session',
}
end
#
# Parse a line into an array of arguments.
#
def parse_line(line)
line.split(' ')
end
#
# Explicitly runs a command.
#
def run_cmd(cmd)
# Do nil check for cmd (CTRL+D will cause nil error)
return unless cmd
arguments = parse_line(cmd)
method = arguments.shift
# Built-in command
if commands.key?(method)
return run_command(method, arguments)
end
# User input is not a built-in command, write to socket directly
shell_write(cmd)
end
def cmd_help(*args)
columns = ['Command', 'Description']
tbl = Rex::Text::Table.new(
'Header' => 'Meta shell commands',
'Prefix' => "\n",
'Postfix' => "\n",
'Indent' => 4,
'Columns' => columns,
'SortIndex' => -1
)
commands.each do |key, value|
tbl << [key, value]
end
print(tbl.to_s)
end
def cmd_background_help()
print_line "Usage: background"
print_line
print_line "Stop interacting with this session and return to the parent prompt"
print_line
end
def cmd_background(*args)
if !args.empty?
# We assume that background does not need arguments
# If user input does not follow this specification
# Then show help (Including '-h' '--help'...)
return cmd_background_help
end
if prompt_yesno("Background session #{name}?")
self.interacting = false
end
end
def cmd_sessions_help()
print_line('Usage: sessions <id>')
print_line
print_line('Interact with a different session Id.')
print_line('This command only accepts one positive numeric argument.')
print_line('This works the same as calling this from the MSF shell: sessions -i <session id>')
print_line
end
def cmd_sessions(*args)
if args.length.zero? || args[0].to_i <= 0
# No args
return cmd_sessions_help
end
if args.length == 1 && (args[1] == '-h' || args[1] == 'help')
# One arg, and args[1] => '-h' '-H' 'help'
return cmd_sessions_help
end
if args.length != 1
# More than one argument
return cmd_sessions_help
end
if args[0].to_s == self.name.to_s
# Src == Dst
print_status("Session #{self.name} is already interactive.")
else
print_status("Backgrounding session #{self.name}...")
# store the next session id so that it can be referenced as soon
# as this session is no longer interacting
self.next_session = args[0]
self.interacting = false
end
end
#
# Run built-in command
#
def run_command(method, arguments)
# Dynamic function call
self.send('cmd_' + method, *arguments)
end
#
# Calls the class method.
#
def type
self.class.type
end
##
# :category: Msf::Session::Provider::SingleCommandShell implementors
#
# The shell will have been initialized by default.
#
def shell_init
return true
end
##
# :category: Msf::Session::Provider::SingleCommandShell implementors
#
# Explicitly run a single command, return the output.
#
def shell_command(cmd)
# Send the command to the session's stdin.
shell_write(cmd + "\n")
timeo = 5
etime = ::Time.now.to_f + timeo
buff = ""
# Keep reading data until no more data is available or the timeout is
# reached.
while (::Time.now.to_f < etime and (self.respond_to?(:ring) or ::IO.select([rstream], nil, nil, timeo)))
res = shell_read(-1, 0.01)
buff << res if res
timeo = etime - ::Time.now.to_f
end
buff
end
##
# :category: Msf::Session::Provider::SingleCommandShell implementors
#
# Read from the command shell.
#
def shell_read(length=-1, timeout=1)
begin
rv = rstream.get_once(length, timeout)
framework.events.on_session_output(self, rv) if rv
return rv
rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE => e
#print_error("Socket error: #{e.class}: #{e}")
shell_close
raise e
end
end
##
# :category: Msf::Session::Provider::SingleCommandShell implementors
#
# Writes to the command shell.
#
def shell_write(buf)
return if not buf
begin
framework.events.on_session_command(self, buf.strip)
rstream.write(buf)
rescue ::Rex::SocketError, ::EOFError, ::IOError, ::Errno::EPIPE => e
#print_error("Socket error: #{e.class}: #{e}")
shell_close
raise e
end
end
##
# :category: Msf::Session::Provider::SingleCommandShell implementors
#
# Closes the shell.
# Note: parent's 'self.kill' method calls cleanup below.
#
def shell_close()
self.kill
end
##
# :category: Msf::Session implementors
#
# Closes the shell.
#
def cleanup
return if @cleanup
@cleanup = true
if rstream
if !@cleanup_command.blank?
# this is a best effort, since the session is possibly already dead
shell_command_token(@cleanup_command) rescue nil
# we should only ever cleanup once
@cleanup_command = nil
end
# this is also a best-effort
rstream.close rescue nil
rstream = nil
end
super
end
#
# Execute any specified auto-run scripts for this session
#
def process_autoruns(datastore)
# Read the initial output and mash it into a single line
if (not self.info or self.info.empty?)
initial_output = shell_read(-1, 0.01)
if (initial_output)
initial_output.force_encoding("ASCII-8BIT") if initial_output.respond_to?(:force_encoding)
initial_output.gsub!(/[\x00-\x08\x0b\x0c\x0e-\x19\x7f-\xff]+/n,"_")
initial_output.gsub!(/[\r\n\t]+/, ' ')
initial_output.strip!
# Set the inital output to .info
self.info = initial_output
end
end
if datastore['InitialAutoRunScript'] && !datastore['InitialAutoRunScript'].empty?
args = Shellwords.shellwords( datastore['InitialAutoRunScript'] )
print_status("Session ID #{sid} (#{tunnel_to_s}) processing InitialAutoRunScript '#{datastore['InitialAutoRunScript']}'")
execute_script(args.shift, *args)
end
if (datastore['AutoRunScript'] && datastore['AutoRunScript'].empty? == false)
args = Shellwords.shellwords( datastore['AutoRunScript'] )
print_status("Session ID #{sid} (#{tunnel_to_s}) processing AutoRunScript '#{datastore['AutoRunScript']}'")
execute_script(args.shift, *args)
end
end
attr_accessor :arch
attr_accessor :platform
attr_accessor :max_threads
protected
##
# :category: Msf::Session::Interactive implementors
#
# Override the basic session interaction to use shell_read and
# shell_write instead of operating on rstream directly.
def _interact
framework.events.on_session_interact(self)
_interact_stream
end
##
# :category: Msf::Session::Interactive implementors
#
def _interact_stream
fds = [rstream.fd, user_input.fd]
while self.interacting
sd = Rex::ThreadSafe.select(fds, nil, fds, 0.5)
next if not sd
if sd[0].include? rstream.fd
user_output.print(shell_read)
end
if sd[0].include? user_input.fd
shell_write(user_input.gets)
end
Thread.pass
end
end
end
class CommandShellWindows < CommandShell
def initialize(*args)
self.platform = "windows"
super
end
def shell_command_token(cmd,timeout = 10)
shell_command_token_win32(cmd,timeout)
end
end
class CommandShellUnix < CommandShell
def initialize(*args)
self.platform = "unix"
super
end
def shell_command_token(cmd,timeout = 10)
shell_command_token_unix(cmd,timeout)
end
end
end
end