From 2c60780dc06ad48b086523cc4261f7314317e0ea Mon Sep 17 00:00:00 2001 From: Zach Goldman Date: Tue, 12 Dec 2023 09:53:37 -0600 Subject: [PATCH] Add MSSQL session Type --- .../framework/login_scanner/mssql.rb | 16 +- lib/msf/base/config.rb | 11 ++ lib/msf/base/sessions/mssql.rb | 147 +++++++++++++++++ lib/msf/core/feature_manager.rb | 7 + lib/msf/core/optional_session.rb | 14 +- lib/rex/post.rb | 1 + lib/rex/post/mssql.rb | 3 + lib/rex/post/mssql/ui.rb | 3 + lib/rex/post/mssql/ui/console.rb | 147 +++++++++++++++++ .../mssql/ui/console/command_dispatcher.rb | 113 +++++++++++++ .../ui/console/command_dispatcher/client.rb | 147 +++++++++++++++++ .../ui/console/command_dispatcher/core.rb | 61 +++++++ .../ui/console/command_dispatcher/modules.rb | 95 +++++++++++ lib/rex/proto/mssql/client_mixin.rb | 2 +- .../admin/mssql/mssql_findandsampledata.rb | 2 +- .../auxiliary/scanner/mssql/mssql_login.rb | 57 ++++++- spec/lib/msf/base/sessions/mssql_spec.rb | 156 ++++++++++++++++++ .../console/command_dispatcher/core_spec.rb | 31 ++++ 18 files changed, 1003 insertions(+), 10 deletions(-) create mode 100644 lib/msf/base/sessions/mssql.rb create mode 100644 lib/rex/post/mssql.rb create mode 100644 lib/rex/post/mssql/ui.rb create mode 100644 lib/rex/post/mssql/ui/console.rb create mode 100644 lib/rex/post/mssql/ui/console/command_dispatcher.rb create mode 100644 lib/rex/post/mssql/ui/console/command_dispatcher/client.rb create mode 100644 lib/rex/post/mssql/ui/console/command_dispatcher/core.rb create mode 100644 lib/rex/post/mssql/ui/console/command_dispatcher/modules.rb create mode 100644 spec/lib/msf/base/sessions/mssql_spec.rb create mode 100644 spec/lib/rex/post/mssql/ui/console/command_dispatcher/core_spec.rb diff --git a/lib/metasploit/framework/login_scanner/mssql.rb b/lib/metasploit/framework/login_scanner/mssql.rb index 658420801f..116edd4703 100644 --- a/lib/metasploit/framework/login_scanner/mssql.rb +++ b/lib/metasploit/framework/login_scanner/mssql.rb @@ -47,7 +47,16 @@ module Metasploit # @return [Boolean] Whether to use Windows Authentication instead of SQL Server Auth. attr_accessor :windows_authentication + # @!attribute use_client_as_proof + # @return [Boolean] If a login is successful and this attribute is true - an MSSQL::Client instance is used as proof + attr_accessor :use_client_as_proof + + # @!attribute max_send_size + # @return [Integer] The max size of the data to encapsulate in a single packet attr_accessor :max_send_size + + # @!attribute send_delay + # @return [Integer] The delay between sending packets attr_accessor :send_delay validates :windows_authentication, @@ -71,6 +80,11 @@ module Metasploit 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 + end else result_options[:status] = Metasploit::Model::Login::Status::INCORRECT end @@ -81,8 +95,6 @@ module Metasploit 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/msf/base/config.rb b/lib/msf/base/config.rb index 7f2bca1d7d..01ff7d68f1 100644 --- a/lib/msf/base/config.rb +++ b/lib/msf/base/config.rb @@ -228,6 +228,13 @@ class Config < Hash self.new.postgresql_session_history end + # Returns the full path to the MSSQL session history file. + # + # @return [String] path to the history file. + def self.mssql_session_history + self.new.mssql_session_history + end + # Returns the full path to the MySQL session history file. # # @return [String] path to the history file. @@ -352,6 +359,10 @@ class Config < Hash 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 0000000000..62ea6e38a4 --- /dev/null +++ b/lib/msf/base/sessions/mssql.rb @@ -0,0 +1,147 @@ +# -*- 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 + # @return [String] The address MSSQL is running on + attr_accessor :address + # @return [Integer] The port MSSQL is running on + attr_accessor :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 #{self.sid} (#{self.tunnel_to_s}) processing #{key} '#{datastore[key]}'") + self.execute_script(args.shift, *args) + end + 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/feature_manager.rb b/lib/msf/core/feature_manager.rb index 2ed4aa021d..40baa7f62b 100644 --- a/lib/msf/core/feature_manager.rb +++ b/lib/msf/core/feature_manager.rb @@ -25,6 +25,7 @@ module Msf 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 @@ module Msf 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 0152a6faea..e64cc82586 100644 --- a/lib/msf/core/optional_session.rb +++ b/lib/msf/core/optional_session.rb @@ -37,8 +37,18 @@ module Msf::OptionalSession 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) + Msf::Opt::RPORT(1433, false) ] ) add_info('New in Metasploit 6.4 - This module can target a %grnSESSION%clr or an %grnRHOST%clr') @@ -46,7 +56,7 @@ module Msf::OptionalSession 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 259b732919..ee6659beb1 100644 --- a/lib/rex/post.rb +++ b/lib/rex/post.rb @@ -5,6 +5,7 @@ require 'rex/post/meterpreter' 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 0000000000..7ae9afd210 --- /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 0000000000..f33c460e85 --- /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 0000000000..a040b1974a --- /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 = session.client.mssql_query('SELECT DB_NAME();')[:rows][0][0] + 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 0000000000..8b317d02b1 --- /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 0000000000..9cba240fed --- /dev/null +++ b/lib/rex/post/mssql/ui/console/command_dispatcher/client.rb @@ -0,0 +1,147 @@ +# -*- 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 && !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 + client.mssql_query(query, true) || [] + end + 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 0000000000..bd6bc102e1 --- /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 0000000000..259d0b47af --- /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_mixin.rb b/lib/rex/proto/mssql/client_mixin.rb index 23a3adb756..cc0aa7635e 100644 --- a/lib/rex/proto/mssql/client_mixin.rb +++ b/lib/rex/proto/mssql/client_mixin.rb @@ -49,7 +49,7 @@ module ClientMixin tbl = Rex::Text::Table.new( 'Indent' => 1, - 'Header' => "", + 'Header' => "Response", 'Columns' => info[:colnames], 'SortIndex' => -1 ) diff --git a/modules/auxiliary/admin/mssql/mssql_findandsampledata.rb b/modules/auxiliary/admin/mssql/mssql_findandsampledata.rb index db99af638c..c81b40a4cc 100644 --- a/modules/auxiliary/admin/mssql/mssql_findandsampledata.rb +++ b/modules/auxiliary/admin/mssql/mssql_findandsampledata.rb @@ -346,7 +346,7 @@ class MetasploitModule < Msf::Auxiliary column_data = result[:rows] print_good("Successfully connected to #{rhost}:#{rport}") rescue - print_error("Failed to connect to #{rhost}:#{rport}.") + print_error("Failed to connect to #{rhost}:#{rport}") return end diff --git a/modules/auxiliary/scanner/mssql/mssql_login.rb b/modules/auxiliary/scanner/mssql/mssql_login.rb index 4ba590d2ea..23862b7fda 100644 --- a/modules/auxiliary/scanner/mssql/mssql_login.rb +++ b/modules/auxiliary/scanner/mssql/mssql_login.rb @@ -6,12 +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 @@ -35,14 +36,36 @@ class MetasploitModule < Msf::Auxiliary OptBool.new('TDSENCRYPTION', [ true, 'Use TLS/SSL for TDS data "Force Encryption"', true]), ]) - deregister_options('PASSWORD_SPRAY') + options_to_deregister = %w[PASSWORD_SPRAY] + if framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE) + add_info('New in Metasploit 6.4 - The %grnCreateSession%clr option within this module can open an interactive session') + else + 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("#{rhost}:#{rport} - MSSQL - Starting authentication scanner.") if datastore['TDSENCRYPTION'] - print_status("Manually enabled TLS/SSL to encrypt TDS payloads.") + if create_session? + raise Msf::OptionValidateError.new( + { + 'TDSENCRYPTION' => "Cannot create sessions when encryption is enabled. See https://github.com/rapid7/metasploit-framework/issues/18745 to vote for this feature" + } + ) + else + print_status("TDS Encryption enabled") + end end cred_collection = build_credential_collection( @@ -68,6 +91,7 @@ class MetasploitModule < Msf::Auxiliary 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'], @@ -86,12 +110,37 @@ class MetasploitModule < Msf::Auxiliary credential_core = create_credential(credential_data) credential_data[:core] = credential_core create_credential_login(credential_data) - print_good "#{ip}:#{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}:#{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 }) # 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/spec/lib/msf/base/sessions/mssql_spec.rb b/spec/lib/msf/base/sessions/mssql_spec.rb new file mode 100644 index 0000000000..3797813c35 --- /dev/null +++ b/spec/lib/msf/base/sessions/mssql_spec.rb @@ -0,0 +1,156 @@ +# 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(:query_result) do + { rows: [['mssql']]} + end + 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(client).to receive(:mssql_query).and_return(query_result) + 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 0000000000..33e32143fe --- /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