Allow running a shell command separately to the currently interactive shell

This commit is contained in:
Ashley Donaldson 2021-09-21 16:20:51 +10:00
parent 91d0a6cc55
commit 9d8193150f
No known key found for this signature in database
GPG Key ID: 70277622B54D3BCE
3 changed files with 48 additions and 41 deletions

View File

@ -1,6 +1,7 @@
# -*- coding: binary -*- # -*- coding: binary -*-
require 'winrm' require 'winrm'
require 'shellwords'
module Msf::Sessions module Msf::Sessions
# #
@ -13,12 +14,13 @@ module Msf::Sessions
class WinRMStreamAdapter class WinRMStreamAdapter
# @param shell [Net::MsfWinRM::StdinShell] Shell for talking to the WinRM service # @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 # @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 # To buffer input received while a session is backgrounded, we stick responses in a list
@buffer_mutex = Mutex.new @buffer_mutex = Mutex.new
@buffer = [] @buffer = []
@check_stdin_event = Rex::Sync::Event.new(false, true) @check_stdin_event = Rex::Sync::Event.new(false, true)
@received_stdout_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.shell = shell
self.on_shell_ended = on_shell_ended self.on_shell_ended = on_shell_ended
end end
@ -37,7 +39,7 @@ module Msf::Sessions
end end
def write(buf) def write(buf)
shell.send_stdin(buf) shell.send_stdin(buf, interactive_command_id)
refresh_stdout refresh_stdout
end end
@ -91,7 +93,7 @@ module Msf::Sessions
loop do loop do
tmp_buffer = [] tmp_buffer = []
output_seen = false output_seen = false
shell.read_stdout do |stdout, stderr| shell.read_stdout(interactive_command_id) do |stdout, stderr|
if stdout || stderr if stdout || stderr
output_seen = true output_seen = true
end end
@ -146,12 +148,12 @@ module Msf::Sessions
# rubocop:disable Lint/SuppressedException # rubocop:disable Lint/SuppressedException
def close def close
stop_keep_alive_loop stop_keep_alive_loop
shell.cleanup_shell shell.cleanup_command(interactive_command_id)
rescue WinRM::WinRMWSManFault rescue WinRM::WinRMWSManFault
end end
# rubocop:enable Lint/SuppressedException # 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 end
@ -171,9 +173,10 @@ module Msf::Sessions
# #
# @param shell [WinRM::Shells::Base] A WinRM shell object # @param shell [WinRM::Shells::Base] A WinRM shell object
# @param opts [Hash] Optional parameters to pass to the session 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.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) super(adapter, opts)
end end
@ -182,10 +185,16 @@ module Msf::Sessions
end end
def abort_foreground def abort_foreground
shell.send_ctrl_c shell.send_ctrl_c(interactive_command_id)
adapter.refresh_stdout adapter.refresh_stdout
end 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 # The characters used to terminate a command in this shell
# (Breaks in 2012 without this) # (Breaks in 2012 without this)
def command_termination def command_termination
@ -225,7 +234,7 @@ module Msf::Sessions
protected protected
attr_accessor :shell, :adapter attr_accessor :shell, :adapter, :interactive_command_id
end end
end end

View File

@ -16,25 +16,29 @@ module Net
def add_finalizer; end def add_finalizer; end
def create_proc def send_command(command, arguments = [])
# We use cmd rather than powershell because powershell v3 on 2012 (and maybe earlier) open unless shell_id
# do not seem to pass us stdout/stderr. super(command, arguments)
self.command_id = send_command('cmd.exe', [])
end end
def with_command_shell(input, _arguments = []) # Runs a shell command synchronously, and returns the output
tries ||= 2 def shell_command_synchronous(command, args, timeout)
send_stdin(input) start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
yield shell_id, command_id command_id = send_command(command, args)
rescue WinRM::WinRMWSManFault => e buffer = []
raise unless FAULTS_FOR_RESET.include?(e.fault_code) && (tries -= 1) > 0 begin
while (Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - start_time) < (timeout * 1000)
reset_on_error(e) read_stdout(command_id) do |stdout, stderr|
retry buffer << stdout if stdout
end buffer << stderr if stderr
end
def cleanup_shell end
cleanup_command(command_id) rescue EOFError
# Shell terminated of its own accord
ensure
cleanup_command(command_id)
end
buffer.join('')
end end
# Runs the specified command with optional arguments # Runs the specified command with optional arguments
@ -42,9 +46,8 @@ module Net
# @yieldparam [string] standard out response text # @yieldparam [string] standard out response text
# @yieldparam [string] standard error response text # @yieldparam [string] standard error response text
# @yieldreturn [WinRM::Output] The command output # @yieldreturn [WinRM::Output] The command output
def read_stdout(&block) def read_stdout(command_id, &block)
open unless shell_id open unless shell_id
create_proc unless command_id
begin begin
response_reader.read_output(command_output_message(shell_id, command_id), &block) response_reader.read_output(command_output_message(shell_id, command_id), &block)
rescue WinRM::WinRMWSManFault => e rescue WinRM::WinRMWSManFault => e
@ -61,10 +64,7 @@ module Net
end end
end end
def send_ctrl_c def send_ctrl_c(command_id)
open unless shell_id
create_proc unless command_id
ctrl_c_msg = CtrlC.new( ctrl_c_msg = CtrlC.new(
connection_opts, connection_opts,
shell_uri: shell_uri, shell_uri: shell_uri,
@ -74,9 +74,8 @@ module Net
transport.send_request(ctrl_c_msg.build) transport.send_request(ctrl_c_msg.build)
end end
def send_stdin(input) def send_stdin(input, command_id)
open unless shell_id open unless shell_id
create_proc unless command_id
stdin_msg = WinRM::WSMV::WriteStdin.new( stdin_msg = WinRM::WSMV::WriteStdin.new(
connection_opts, connection_opts,
@ -101,15 +100,13 @@ module Net
def open_shell def open_shell
msg = WinRM::WSMV::CreateShell.new(connection_opts, shell_opts) msg = WinRM::WSMV::CreateShell.new(connection_opts, shell_opts)
resp_doc = transport.send_request(msg.build) 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 REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
end end
attr_accessor :owner attr_accessor :owner
protected
attr_accessor :command_id
end end
end end
end end

View File

@ -98,8 +98,6 @@ class MetasploitModule < Msf::Auxiliary
} }
) )
shell = conn.shell(:stdin, {}) shell = conn.shell(:stdin, {})
# Trigger the shell to open
shell.send_stdin('')
session_setup(shell, rhost, rport, endpoint) session_setup(shell, rhost, rport, endpoint)
end end
else else
@ -110,7 +108,10 @@ class MetasploitModule < Msf::Auxiliary
end end
def session_setup(shell, _rhost, _rport, _endpoint) 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' sess.platform = 'windows'
username = datastore['USERNAME'] username = datastore['USERNAME']
password = datastore['PASSWORD'] password = datastore['PASSWORD']