389 lines
9.1 KiB
Ruby
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
|