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

View File

@ -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

View File

@ -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']