# -*- 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