From 554e1d4ba4d21d5596b966259c9629f5a6a533de Mon Sep 17 00:00:00 2001 From: Zach Goldman Date: Wed, 24 Jan 2024 11:12:11 -0600 Subject: [PATCH] Add MSSQL session type --- .../framework/login_scanner/mssql.rb | 9 +- lib/msf/base/config.rb | 7 + lib/msf/base/sessions/mssql.rb | 144 +++++++++++++++++ lib/msf/core/feature_manager.rb | 7 + lib/msf/core/optional_session.rb | 14 +- lib/msf/core/post/common.rb | 4 +- 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 | 148 +++++++++++++++++ .../ui/console/command_dispatcher/core.rb | 61 +++++++ .../ui/console/command_dispatcher/modules.rb | 95 +++++++++++ lib/rex/proto/mssql/client.rb | 1 + lib/rex/proto/mssql/client_mixin.rb | 1 - .../auxiliary/scanner/mssql/mssql_login.rb | 51 +++++- .../scanner/postgres/postgres_login.rb | 1 - spec/lib/msf/base/sessions/mssql_spec.rb | 152 ++++++++++++++++++ .../console/command_dispatcher/core_spec.rb | 31 ++++ 20 files changed, 982 insertions(+), 11 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 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/msf/base/config.rb b/lib/msf/base/config.rb index 12ae637292e0b..97f4b861aa721 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 def self.pry_history self.new.pry_history @@ -341,6 +344,10 @@ def postgresql_session_history config_directory + FileSep + "postgresql_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..66811885dc0df --- /dev/null +++ b/lib/msf/base/sessions/mssql.rb @@ -0,0 +1,144 @@ +# -*- coding:binary -*- + +require 'rex/post/mssql' +# TODO + +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_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 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 beb2d7441fb87..3771eaac749f2 100644 --- a/lib/msf/core/feature_manager.rb +++ b/lib/msf/core/feature_manager.rb @@ -24,6 +24,7 @@ class FeatureManager HIERARCHICAL_SEARCH_TABLE = 'hierarchical_search_table' SMB_SESSION_TYPE = 'smb_session_type' POSTGRESQL_SESSION_TYPE = 'postgresql_session_type' + MSSQL_SESSION_TYPE = 'mssql_session_type' DEFAULTS = [ { name: WRAPPED_TABLES, @@ -76,6 +77,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 69a232715b9c7..0cc0d0ccb69c3 100644 --- a/lib/msf/core/optional_session.rb +++ b/lib/msf/core/optional_session.rb @@ -25,6 +25,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) ] @@ -33,7 +43,9 @@ 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)) + return nil unless (framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE) || + framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE || + framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE)) super end diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index ccdcbbb68a1a5..f6dcd4a90d52e 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -30,7 +30,7 @@ def rhost session.sock.peerhost when 'shell', 'powershell' session.session_host - when 'postgresql' + when 'postgresql', 'mssql' session.address end rescue @@ -45,7 +45,7 @@ def rport session.sock.peerport when 'shell', 'powershell' session.session_port - when 'postgresql' + when 'postgresql', 'mssql' session.port end rescue diff --git a/lib/rex/post.rb b/lib/rex/post.rb index eb744e09ed5d5..d0177b9c713ef 100644 --- a/lib/rex/post.rb +++ b/lib/rex/post.rb @@ -4,6 +4,7 @@ require 'rex/post/meterpreter' require 'rex/post/smb' require 'rex/post/postgresql' +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..6409289fc917a --- /dev/null +++ b/lib/rex/post/mssql.rb @@ -0,0 +1,3 @@ +# -*- coding: binary -*- + +require 'rex/post/mssql/ui' \ No newline at end of file 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..9e72bd9472f8f --- /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 \ No newline at end of file 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..d9a44a8f43e3a --- /dev/null +++ b/lib/rex/post/mssql/ui/console/command_dispatcher/client.rb @@ -0,0 +1,148 @@ +# -*- 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 + until finished + begin + raw_query = ::Reline.readmultiline('SQL >> ', use_history = true) do |multiline_input| + finished = stop_words.include?(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 SHOW DATABASES;' + print_line ' query USE information_schema;' + print_line ' query SELECT * FROM SQL_FUNCTIONS;' + print_line ' query SELECT version();' + 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) + cmd_query_help && return if help_args?(args) + query = args.join(' ').to_s + print_status("Sending statement: '#{query}'...") + result = 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..9fc3eab414e4e --- /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 \ No newline at end of file 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..45520d0b7476c --- /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 \ No newline at end of file diff --git a/lib/rex/proto/mssql/client.rb b/lib/rex/proto/mssql/client.rb index 2523b31bd27e4..d09403b0b7cc7 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 diff --git a/lib/rex/proto/mssql/client_mixin.rb b/lib/rex/proto/mssql/client_mixin.rb index 23a3adb756e38..dce07f489cb7b 100644 --- a/lib/rex/proto/mssql/client_mixin.rb +++ b/lib/rex/proto/mssql/client_mixin.rb @@ -57,7 +57,6 @@ def mssql_print_reply(info) info[:rows].each do |row| tbl << row end - print_line(tbl.to_s) end end diff --git a/modules/auxiliary/scanner/mssql/mssql_login.rb b/modules/auxiliary/scanner/mssql/mssql_login.rb index ee66ea37648d8..12a3ea4fec83a 100644 --- a/modules/auxiliary/scanner/mssql/mssql_login.rb +++ b/modules/auxiliary/scanner/mssql/mssql_login.rb @@ -6,11 +6,12 @@ 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::Auxiliary::Report include Msf::Auxiliary::AuthBrute - + include Msf::Auxiliary::CommandShell include Msf::Auxiliary::Scanner def initialize @@ -40,14 +41,30 @@ def initialize OptBool.new('USE_WINDOWS_AUTHENT', [ true, 'Use windows authentication (requires DOMAIN option set)', false]), ]) - 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.") 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( @@ -73,6 +90,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'], @@ -91,12 +109,37 @@ def run_host(ip) credential_core = create_credential(credential_data) credential_data[:core] = credential_core create_credential_login(credential_data) - 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})" 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/postgres/postgres_login.rb b/modules/auxiliary/scanner/postgres/postgres_login.rb index ba503c069a8cc..d4548039d132f 100644 --- a/modules/auxiliary/scanner/postgres/postgres_login.rb +++ b/modules/auxiliary/scanner/postgres/postgres_login.rb @@ -95,7 +95,6 @@ def run_host(ip) create_credential_login(credential_data) print_good "#{ip}:#{rport} - Login Successful: #{result.credential}" - if create_session? begin postgresql_client = result.proof 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..dfa25bd0cb63f --- /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 \ No newline at end of file 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..7486f22c8d3fd --- /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 \ No newline at end of file