From 9d8193150fd0cf2ede16f62860c285ce68e6e9a8 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 21 Sep 2021 16:20:51 +1000 Subject: [PATCH] Allow running a shell command separately to the currently interactive shell --- lib/msf/base/sessions/winrm_command_shell.rb | 27 ++++++--- lib/net/winrm/stdin_shell.rb | 55 +++++++++---------- .../auxiliary/scanner/winrm/winrm_login.rb | 7 ++- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/lib/msf/base/sessions/winrm_command_shell.rb b/lib/msf/base/sessions/winrm_command_shell.rb index cdd5aad075..898235860a 100644 --- a/lib/msf/base/sessions/winrm_command_shell.rb +++ b/lib/msf/base/sessions/winrm_command_shell.rb @@ -1,6 +1,7 @@ # -*- coding: binary -*- require 'winrm' +require 'shellwords' module Msf::Sessions # @@ -13,12 +14,13 @@ module Msf::Sessions 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, on_shell_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 @@ -37,7 +39,7 @@ module Msf::Sessions end def write(buf) - shell.send_stdin(buf) + shell.send_stdin(buf, interactive_command_id) refresh_stdout end @@ -91,7 +93,7 @@ module Msf::Sessions loop do tmp_buffer = [] output_seen = false - shell.read_stdout do |stdout, stderr| + shell.read_stdout(interactive_command_id) do |stdout, stderr| if stdout || stderr output_seen = true end @@ -146,12 +148,12 @@ module Msf::Sessions # rubocop:disable Lint/SuppressedException def close stop_keep_alive_loop - shell.cleanup_shell + shell.cleanup_command(interactive_command_id) rescue WinRM::WinRMWSManFault end # rubocop:enable Lint/SuppressedException - attr_accessor :shell, :keep_alive_thread, :on_shell_ended + attr_accessor :shell, :keep_alive_thread, :on_shell_ended, :interactive_command_id end @@ -171,9 +173,10 @@ module Msf::Sessions # # @param shell [WinRM::Shells::Base] A WinRM shell object # @param opts [Hash] Optional parameters to pass to the session object. - def initialize(shell, opts = {}) + def initialize(shell, interactive_command_id, opts = {}) self.shell = shell - self.adapter = WinRMStreamAdapter.new(self.shell, method(:shell_ended)) + self.interactive_command_id = interactive_command_id + self.adapter = WinRMStreamAdapter.new(self.shell, interactive_command_id, method(:shell_ended)) super(adapter, opts) end @@ -182,10 +185,16 @@ module Msf::Sessions end def abort_foreground - shell.send_ctrl_c + 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 @@ -225,7 +234,7 @@ module Msf::Sessions protected - attr_accessor :shell, :adapter + attr_accessor :shell, :adapter, :interactive_command_id end end diff --git a/lib/net/winrm/stdin_shell.rb b/lib/net/winrm/stdin_shell.rb index 148cbcce2e..e9d21e04a2 100644 --- a/lib/net/winrm/stdin_shell.rb +++ b/lib/net/winrm/stdin_shell.rb @@ -16,25 +16,29 @@ module Net def add_finalizer; end - def create_proc - # We use cmd rather than powershell because powershell v3 on 2012 (and maybe earlier) - # do not seem to pass us stdout/stderr. - self.command_id = send_command('cmd.exe', []) + def send_command(command, arguments = []) + open unless shell_id + super(command, arguments) end - def with_command_shell(input, _arguments = []) - tries ||= 2 - send_stdin(input) - yield shell_id, command_id - rescue WinRM::WinRMWSManFault => e - raise unless FAULTS_FOR_RESET.include?(e.fault_code) && (tries -= 1) > 0 - - reset_on_error(e) - retry - end - - def cleanup_shell - cleanup_command(command_id) + # Runs a shell command synchronously, and returns the output + def shell_command_synchronous(command, args, timeout) + start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + command_id = send_command(command, args) + buffer = [] + begin + while (Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - start_time) < (timeout * 1000) + read_stdout(command_id) do |stdout, stderr| + buffer << stdout if stdout + buffer << stderr if stderr + end + end + rescue EOFError + # Shell terminated of its own accord + ensure + cleanup_command(command_id) + end + buffer.join('') end # Runs the specified command with optional arguments @@ -42,9 +46,8 @@ module Net # @yieldparam [string] standard out response text # @yieldparam [string] standard error response text # @yieldreturn [WinRM::Output] The command output - def read_stdout(&block) + def read_stdout(command_id, &block) open unless shell_id - create_proc unless command_id begin response_reader.read_output(command_output_message(shell_id, command_id), &block) rescue WinRM::WinRMWSManFault => e @@ -61,10 +64,7 @@ module Net end end - def send_ctrl_c - open unless shell_id - create_proc unless command_id - + def send_ctrl_c(command_id) ctrl_c_msg = CtrlC.new( connection_opts, shell_uri: shell_uri, @@ -74,9 +74,8 @@ module Net transport.send_request(ctrl_c_msg.build) end - def send_stdin(input) + def send_stdin(input, command_id) open unless shell_id - create_proc unless command_id stdin_msg = WinRM::WSMV::WriteStdin.new( connection_opts, @@ -101,15 +100,13 @@ module Net def open_shell msg = WinRM::WSMV::CreateShell.new(connection_opts, shell_opts) resp_doc = transport.send_request(msg.build) - self.owner = REXML::XPath.first(resp_doc, '//rsp:Owner').text + match = REXML::XPath.first(resp_doc, '//rsp:Owner') + self.owner = match.text if match REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text end attr_accessor :owner - protected - - attr_accessor :command_id end end end diff --git a/modules/auxiliary/scanner/winrm/winrm_login.rb b/modules/auxiliary/scanner/winrm/winrm_login.rb index e4dacab838..81fc2781dd 100644 --- a/modules/auxiliary/scanner/winrm/winrm_login.rb +++ b/modules/auxiliary/scanner/winrm/winrm_login.rb @@ -98,8 +98,6 @@ class MetasploitModule < Msf::Auxiliary } ) shell = conn.shell(:stdin, {}) - # Trigger the shell to open - shell.send_stdin('') session_setup(shell, rhost, rport, endpoint) end else @@ -110,7 +108,10 @@ class MetasploitModule < Msf::Auxiliary end def session_setup(shell, _rhost, _rport, _endpoint) - sess = Msf::Sessions::WinrmCommandShell.new(shell) + # We use cmd rather than powershell because powershell v3 on 2012 (and maybe earlier) + # do not seem to pass us stdout/stderr. + interactive_process_id = shell.send_command('cmd.exe') + sess = Msf::Sessions::WinrmCommandShell.new(shell, interactive_process_id) sess.platform = 'windows' username = datastore['USERNAME'] password = datastore['PASSWORD']