lib/msf/base/sessions/winrm_command_shell.rb
# -*- coding: binary -*-
require 'winrm'
require 'shellwords'
module Msf::Sessions
#
# This class provides a session for WinRM client connections, where Metasploit
# has authenticated to a remote WinRM instance.
#
class WinrmCommandShell < Msf::Sessions::CommandShell
# Abstract WinRM to look like a stream so CommandShell can be happy
class WinRMStreamAdapter
# @param shell [Net::MsfWinRM::StdinShell] Shell for talking to the WinRM service
# @param on_shell_ended [Method] Callback for when the background thread notices the shell has ended
def initialize(shell, interactive_command_id, on_shell_ended)
# To buffer input received while a session is backgrounded, we stick responses in a list
@buffer_mutex = Mutex.new
@buffer = []
@check_stdin_event = Rex::Sync::Event.new(false, true)
@received_stdout_event = Rex::Sync::Event.new(false, true)
self.interactive_command_id = interactive_command_id
self.shell = shell
self.on_shell_ended = on_shell_ended
end
def peerinfo
shell.transport.peerinfo
end
def localinfo
shell.transport.localinfo
end
# Trigger the background thread to go get more stdout
def refresh_stdout
@check_stdin_event.set
end
def write(buf)
shell.send_stdin(buf, interactive_command_id)
refresh_stdout
end
##
# :category: Msf::Session::Provider::SingleCommandShell implementors
#
# Read from the command shell.
#
def get_once(length = -1, timeout = 1)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
result = ''
loop do
result = _get_once(length)
time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
elapsed = time - start_time
time_remaining = timeout - elapsed
break if (result != '' || time_remaining <= 0)
# rubocop:disable Lint/SuppressedException
begin
# We didn't receive anything - let's wait for some more
@received_stdout_event.wait(time_remaining)
rescue ::Timeout::Error
end
# rubocop:enable Lint/SuppressedException
# If we didn't get anything, let's hurry the background thread along
refresh_stdout unless result
end
result
end
def _get_once(length)
result = ''
@buffer_mutex.synchronize do
result = @buffer.join('')
@buffer = []
if (length > -1) && (result.length > length)
# Return up to length, and keep the rest in the buffer
extra = result[length..-1]
result = result[0, length]
@buffer << extra
end
end
result
end
# Start a background thread for regularly checking for stdout
def start_keep_alive_loop(framework)
self.keep_alive_thread = framework.threads.spawn('WinRM-shell-keepalive', false, shell) do |_thr_shell|
loop_delay = 0.5
loop do
tmp_buffer = []
output_seen = false
shell.read_stdout(interactive_command_id) do |stdout, stderr|
if stdout || stderr
output_seen = true
end
tmp_buffer << stdout if stdout
tmp_buffer << stderr if stderr
end
@buffer_mutex.synchronize do
@buffer.concat(tmp_buffer)
end
# If our last request received stdout, let's be ready for some more
if output_seen
@received_stdout_event.set
loop_delay = 0.5
else
# Gradual backoff
loop_delay *= 4
loop_delay = [loop_delay, 30].min
end
# Wait loop_delay seconds, or until an interactive thread wakes us up
begin
@check_stdin_event.wait(loop_delay)
# rubocop:disable Lint/SuppressedException
rescue ::Timeout::Error
end
# rubocop:enable Lint/SuppressedException
Thread.pass
rescue WinRM::WinRMWSManFault => e
print_error(e.fault_description)
on_shell_ended.call
rescue EOFError
# Shell has been terminated
on_shell_ended.call
rescue Rex::HostUnreachable => e
on_shell_ended.call(e.message)
rescue StandardError => e
on_shell_ended.call(e.message)
end
end
end
# Stop the background thread
def stop_keep_alive_loop
keep_alive_thread.kill
end
# Close the shell; cleanly terminating it on the server if possible
#
# The shell may already be dead, or unreachable at this point, so do a best
# effort, and capture exceptions
# rubocop:disable Lint/SuppressedException
def close
stop_keep_alive_loop
shell.cleanup_command(interactive_command_id)
rescue WinRM::WinRMWSManFault
end
# rubocop:enable Lint/SuppressedException
attr_accessor :shell, :keep_alive_thread, :on_shell_ended, :interactive_command_id
end
def commands
{
'help' => 'Help menu',
'background' => 'Backgrounds the current shell session',
'sessions' => 'Quickly switch to another session',
'resource' => 'Run a meta commands script stored in a local file',
'irb' => 'Open an interactive Ruby shell on the current session',
'pry' => 'Open the Pry debugger on the current session'
}
end
#
# Create an MSF command shell from a WinRM shell object
#
# @param shell [WinRM::Shells::Base] A WinRM shell object
# @param opts [Hash] Optional parameters to pass to the session object.
def initialize(shell, interactive_command_id, opts = {})
self.shell = shell
self.interactive_command_id = interactive_command_id
self.adapter = WinRMStreamAdapter.new(self.shell, interactive_command_id, method(:shell_ended))
super(adapter, opts)
end
def abort_foreground_supported
true
end
def abort_foreground
shell.send_ctrl_c(interactive_command_id)
adapter.refresh_stdout
end
def shell_command(cmd, timeout = 5)
args = Shellwords.shellwords(cmd)
command = args.shift
shell.shell_command_synchronous(command, args, timeout)
end
# The characters used to terminate a command in this shell
# (Breaks in 2012 without this)
def command_termination
"\r\n"
end
##
# :category: Msf::Session::Interactive implementors
#
def _interact_stream
fds = [user_input.fd]
while interacting
sd = Rex::ThreadSafe.select(fds, nil, fds, 0.5)
begin
user_output.print(shell_read(-1, 0))
if sd
run_single((user_input.gets || '').chomp("\n"))
end
rescue WinRM::WinRMWSManFault => e
print_error(e.fault_description)
shell_close
end
Thread.pass
end
end
def on_registered
adapter.start_keep_alive_loop(framework)
end
# Callback used by the background thread to let us know the thread is done
def shell_ended(reason = '')
self.interacting = false
framework.events.on_session_interact_completed
framework.sessions.deregister(self, reason)
end
protected
attr_accessor :shell, :adapter, :interactive_command_id
end
end