From c365dc0ab788786377c967968d0589d49cf66ef8 Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Wed, 3 Jan 2024 17:52:27 +0000 Subject: [PATCH] Pull in common session functionality changes & fix PostgreSQL session exit & code cleanup --- lib/msf/base/sessions/postgresql.rb | 31 +- lib/rex/post/postgresql/ui.rb | 2 +- lib/rex/post/postgresql/ui/console.rb | 4 +- .../ui/console/command_dispatcher.rb | 45 +-- .../command_dispatcher/{db.rb => client.rb} | 33 +- .../ui/console/command_dispatcher/core.rb | 361 +----------------- .../scanner/postgres/postgres_login.rb | 25 +- 7 files changed, 69 insertions(+), 432 deletions(-) rename lib/rex/post/postgresql/ui/console/command_dispatcher/{db.rb => client.rb} (90%) diff --git a/lib/msf/base/sessions/postgresql.rb b/lib/msf/base/sessions/postgresql.rb index fd8aa1e97e8b4..2058c41dbad1f 100644 --- a/lib/msf/base/sessions/postgresql.rb +++ b/lib/msf/base/sessions/postgresql.rb @@ -15,8 +15,9 @@ class Msf::Sessions::PostgreSQL # < Msf::Sessions::CommandShell attr_accessor :platform attr_accessor :arch - # ##@param [PostgreSQL::Client] client - # @param [PostgreSQL::Client] rstream + # @param[Rex::IO::Stream] rstream + # @param [Hash] opts Options + # @param opts [PostgreSQL::Client] :client def initialize(rstream, opts={}) @client = opts.fetch(:client) self.console = ::Rex::Post::PostgreSQL::Ui::Console.new(self) @@ -35,7 +36,13 @@ def bootstrap(datastore = {}, handler = nil) end def process_autoruns(datastore) - # TODO - Implemented for now to keep things happy + ['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 @@ -62,6 +69,20 @@ def desc 'PostgreSQL' end + def address + return @address if @address + + @address, @port = self.client.conn.peerinfo.split(':') + @address + end + + def port + return @port if @port + + @address, @port = self.client.conn.peerinfo.split(':') + @port + end + ## # :category: Msf::Session::Interactive implementors # @@ -84,6 +105,10 @@ def reset_ui self.console.reset_ui end + def exit + self.console.stop + end + protected ## diff --git a/lib/rex/post/postgresql/ui.rb b/lib/rex/post/postgresql/ui.rb index 0ab77956df3db..f22d97bfd9aa4 100644 --- a/lib/rex/post/postgresql/ui.rb +++ b/lib/rex/post/postgresql/ui.rb @@ -1,3 +1,3 @@ # -*- coding: binary -*- -require 'rex/post/postgresql/ui/console' \ No newline at end of file +require 'rex/post/postgresql/ui/console' diff --git a/lib/rex/post/postgresql/ui/console.rb b/lib/rex/post/postgresql/ui/console.rb index 99ef908dbb7fa..fe9e21e35d44d 100644 --- a/lib/rex/post/postgresql/ui/console.rb +++ b/lib/rex/post/postgresql/ui/console.rb @@ -15,7 +15,7 @@ class Console # Dispatchers require 'rex/post/postgresql/ui/console/command_dispatcher' require 'rex/post/postgresql/ui/console/command_dispatcher/core' - require 'rex/post/postgresql/ui/console/command_dispatcher/db' + require 'rex/post/postgresql/ui/console/command_dispatcher/client' # # Initialize the PostgreSQL console. @@ -36,7 +36,7 @@ def initialize(session) reset_ui enstack_dispatcher(::Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher::Core) - enstack_dispatcher(::Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher::DB) + enstack_dispatcher(::Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher::Client) # Set up logging to whatever logsink 'core' is using if ! $dispatcher['postgresql'] diff --git a/lib/rex/post/postgresql/ui/console/command_dispatcher.rb b/lib/rex/post/postgresql/ui/console/command_dispatcher.rb index 17f71abcf7755..95a8aede686e4 100644 --- a/lib/rex/post/postgresql/ui/console/command_dispatcher.rb +++ b/lib/rex/post/postgresql/ui/console/command_dispatcher.rb @@ -12,28 +12,7 @@ module Ui # ### module Console::CommandDispatcher - include Rex::Ui::Text::DispatcherShell::CommandDispatcher - - # - # The hash of file names to class names after a module has already been - # loaded once on the client side. - # - # @@file_hash = {} - # - # # - # # Checks the file name to hash association to see if the module being - # # requested has already been loaded once. - # # - # def self.check_hash(name) - # @@file_hash[name] - # end - # - # # - # # Sets the file path to class name association for future reference. - # # - # def self.set_hash(name, klass) - # @@file_hash[name] = klass - # end + include Msf::Ui::Console::CommandDispatcher::Session # # Initializes an instance of the core command set using the supplied session and client @@ -64,14 +43,6 @@ def session console.session end - def address - self.client.conn.remote_address.ip_address - end - - def port - self.client.conn.remote_address.ip_port - end - # # Returns the commands that meet the requirements # @@ -86,7 +57,7 @@ def filter_commands(all, reqs) def unknown_command(cmd, line) if @filtered_commands.include?(cmd) - print_error("The \"#{cmd}\" command is not supported by this session type (#{client.session_type})") + print_error("The \"#{cmd}\" command is not supported by this session type (#{session.session_type})") return :handled end @@ -98,7 +69,7 @@ def unknown_command(cmd, line) # to find usage documentation # def docs_dir - ::File.join(super, 'meterpreter') + ::File.join(super, 'postgresql_session') end # @@ -110,7 +81,9 @@ def msf_loaded? return @msf_loaded unless @msf_loaded.nil? # if we get here we must not have initialized yet - @msf_loaded = !!(self.client.framework) + + @msf_loaded = !session.framework.nil? + @msf_loaded end # @@ -121,12 +94,10 @@ def log_error(msg) elog(msg, 'postgresql') - dlog("Call stack:\n#{$@.join("\n")}", 'postgresql') + dlog("Call stack:\n#{$ERROR_POSITION.join("\n")}", 'postgresql') end - end - end end end -end \ No newline at end of file +end diff --git a/lib/rex/post/postgresql/ui/console/command_dispatcher/db.rb b/lib/rex/post/postgresql/ui/console/command_dispatcher/client.rb similarity index 90% rename from lib/rex/post/postgresql/ui/console/command_dispatcher/db.rb rename to lib/rex/post/postgresql/ui/console/command_dispatcher/client.rb index 1892d33ebb713..bb7a647af5164 100644 --- a/lib/rex/post/postgresql/ui/console/command_dispatcher/db.rb +++ b/lib/rex/post/postgresql/ui/console/command_dispatcher/client.rb @@ -10,10 +10,10 @@ module Ui ### # - # Core SMB client commands + # Core PostgreSQL client commands # ### - class Console::CommandDispatcher::DB + class Console::CommandDispatcher::Client include Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher @@ -37,13 +37,13 @@ class Console::CommandDispatcher::DB 'daticurules' => 'ICU Rules', 'datcollversion' => 'Collation Version', 'datacl' => 'Access Privileges' - } + }.freeze # # Initializes an instance of the core command set using the supplied console # for interactivity. # - # @param [Rex::Post::SMB::Ui::Console] console + # @param [Rex::Post::PostgreSQL::Ui::Console] console def initialize(console) super @@ -75,7 +75,7 @@ def commands end def name - 'DB' + 'PostgreSQL Client' end def help_args?(args) @@ -87,7 +87,8 @@ def help_args?(args) 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 '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 @@ -106,14 +107,22 @@ def cmd_shell(*args) finished = false until finished - raw_query = ::Reline.readmultiline('SQL >> ', use_history) do |multiline_input| - if stop_words.include?(multiline_input.split.last) - finished = true - true + begin + raw_query = ::Reline.readmultiline('SQL >> ', use_history) do |multiline_input| + if stop_words.include?(multiline_input.split.last) + finished = true + true + end + !multiline_input.split.last.end_with?('\\') end - !multiline_input.split.last.end_with?('\\') + rescue ::Interrupt + finished = true + end + + if finished + print_status "Exiting Shell mode." + return end - return if finished formatted_query = raw_query.split.map { |word| word.chomp('\\') }.reject(&:empty?).compact.join(' ') diff --git a/lib/rex/post/postgresql/ui/console/command_dispatcher/core.rb b/lib/rex/post/postgresql/ui/console/command_dispatcher/core.rb index 50579c7824c31..c5c2930fba680 100644 --- a/lib/rex/post/postgresql/ui/console/command_dispatcher/core.rb +++ b/lib/rex/post/postgresql/ui/console/command_dispatcher/core.rb @@ -14,20 +14,6 @@ class Console::CommandDispatcher::Core include Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher - # - # Initializes an instance of the core command set using the supplied session and client - # for interactivity. - # - # @param [Rex::Post::PostgreSQL::Ui::Console] console - def initialize(console) - super - end - - @@irb_opts = Rex::Parser::Arguments.new( - '-h' => [false, 'Help menu.' ], - '-e' => [true, 'Expression to evaluate.'] - ) - # # List of supported commands. # @@ -36,370 +22,29 @@ def commands '?' => 'Help menu', 'background' => 'Backgrounds the current session', 'bg' => 'Alias for background', - 'exit' => 'Terminate the SMB session', + 'exit' => 'Terminate the PostgreSQL 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 = { - } + reqs = {} filter_commands(cmds, reqs) end - # - # Core - # def name 'Core' end - def cmd_sessions_help - print_line('Usage: sessions ') - print_line - print_line('Interact with a different session Id.') - print_line('This works the same as calling this from the MSF shell: sessions -i ') - print_line - end - - def cmd_sessions(*args) - if args.empty? || args[0].to_i == 0 - cmd_sessions_help - elsif args[0].to_s == client.name.to_s - print_status("Session #{client.name} is already interactive.") - else - print_status("Backgrounding session #{client.name}...") - # store the next session id so that it can be referenced as soon - # as this session is no longer interacting - client.next_session = args[0] - client.interacting = false - end - end - - def cmd_background_help - print_line('Usage: background') - print_line - print_line('Stop interacting with this session and return to the parent prompt') - print_line - end - - def cmd_background - print_status("Backgrounding session #{self.session.name}...") - self.session.interacting = false - end - - alias cmd_bg cmd_background - alias cmd_bg_help cmd_background_help - - # - # Terminates the PostgreSQL session. - # - def cmd_exit(*args) - print_status('Shutting down PostgreSQL...') - - begin - self.client.close - rescue ::RuntimeError => e # Connection already closed - print_warning e.message - end - - shell.stop - end - - def cmd_irb_help - print_line('Usage: irb') - print_line - print_line('Open an interactive Ruby shell on the current session.') - print @@irb_opts.usage - end - - def cmd_irb_tabs(str, words) - return [] if words.length > 1 - - @@irb_opts.option_keys - end - - # - # Open an interactive Ruby shell on the current session - # - def cmd_irb(*args) - expressions = [] - - # Parse the command options - @@irb_opts.parse(args) do |opt, idx, val| - case opt - when '-e' - expressions << val - when '-h' - return cmd_irb_help - end - end - - session = self.session - framework = session.framework - - if expressions.empty? - print_status('Starting IRB shell...') - print_status("You are in the PostgreSQL command dispatcher object\n") - framework.history_manager.with_context(name: :irb) do - Rex::Ui::Text::IrbShell.new(session).run - end - else - # XXX: No vprint_status here - if framework.datastore['VERBOSE'].to_s == 'true' - print_status("You are executing expressions in #{binding.receiver}") - end - - expressions.each { |expression| eval(expression, binding) } - end - end - - def cmd_pry_help - print_line 'Usage: pry' - print_line - print_line 'Open the Pry debugger on the current session.' - print_line - end - - # - # Open the Pry debugger on the current session - # - def cmd_pry(*args) - if args.include?('-h') - cmd_pry_help - return - end - - begin - require 'pry-byebug' - rescue ::LoadError - print_error('Failed to load Pry, try "gem install pry-byebug"') - return - end - - print_status('Starting Pry shell...') - print_status("You are in the \"client\" (session) object\n") - - ::Pry.config.history_load = false - ::Rex::Ui::Text::Shell::HistoryManager.with_context(history_file: ::Msf::Config.pry_history, name: :pry) do - client.pry - end - end - - # def cmd_info_help - # print_line('Usage: info ') - # print_line - # print_line('Prints information about a post-exploitation module') - # print_line - # end - # - # # - # # Show info for a given Post module. - # # - # # See also +cmd_info+ in lib/msf/ui/console/command_dispatcher/core.rb - # # - # def cmd_info(*args) - # return unless msf_loaded? - # - # if args.length != 1 or args.include?('-h') - # cmd_info_help - # return - # end - # - # module_name = args.shift - # mod = client.framework.modules.create(module_name); - # - # if mod.nil? - # print_error("Invalid module: #{module_name}") - # end - # - # if (mod) - # print_line(::Msf::Serializer::ReadableText.dump_module(mod)) - # mod_opt = ::Msf::Serializer::ReadableText.dump_options(mod, ' ') - # print_line("\nModule options (#{mod.fullname}):\n\n#{mod_opt}") if (mod_opt and mod_opt.length > 0) - # end - # end - # - # def cmd_info_tabs(str, words) - # tab_complete_modules(str, words) if msf_loaded? - # end - - # def cmd_resource_help - # print_line "Usage: resource path1 [path2 ...]" - # print_line - # print_line "Run the commands stored in the supplied files. (- for stdin, press CTRL+D to end input from stdin)" - # print_line "Resource files may also contain ERB or Ruby code between tags." - # print_line - # end - # - # def cmd_resource(*args) - # if args.empty? - # cmd_resource_help - # return false - # end - # - # args.each do |res| - # good_res = nil - # if res == '-' - # good_res = res - # elsif ::File.exist?(res) - # good_res = res - # elsif - # # let's check to see if it's in the scripts/resource dir (like when tab completed) - # [ - # ::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', - # ::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter' - # ].each do |dir| - # res_path = dir + ::File::SEPARATOR + res - # if ::File.exist?(res_path) - # good_res = res_path - # break - # end - # end - # end - # if good_res - # client.console.load_resource(good_res) - # else - # print_error("#{res} is not a valid resource file") - # next - # end - # end - # end - # - # def cmd_resource_tabs(str, words) - # tabs = [] - # #return tabs if words.length > 1 - # if ( str and str =~ /^#{Regexp.escape(::File::SEPARATOR)}/ ) - # # then you are probably specifying a full path so let's just use normal file completion - # return tab_complete_filenames(str,words) - # elsif (not words[1] or not words[1].match(/^\//)) - # # then let's start tab completion in the scripts/resource directories - # begin - # [ - # ::Msf::Config.script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', - # ::Msf::Config.user_script_directory + ::File::SEPARATOR + 'resource' + ::File::SEPARATOR + 'meterpreter', - # '.' - # ].each do |dir| - # next if not ::File.exist? dir - # tabs += ::Dir.new(dir).find_all { |e| - # path = dir + ::File::SEPARATOR + e - # ::File.file?(path) and ::File.readable?(path) - # } - # end - # rescue Exception - # end - # else - # tabs += tab_complete_filenames(str,words) - # end - # return tabs - # end - - # def cmd_enable_unicode_encoding - # client.encode_unicode = true - # print_status('Unicode encoding is enabled') - # end - # - # def cmd_disable_unicode_encoding - # client.encode_unicode = false - # print_status('Unicode encoding is disabled') - # end - - # @@client_extension_search_paths = [::File.join(Rex::Root, 'post', 'meterpreter', 'ui', 'console', 'command_dispatcher')] - # - # def self.add_client_extension_search_path(path) - # @@client_extension_search_paths << path unless @@client_extension_search_paths.include?(path) - # end - # - # def self.client_extension_search_paths - # @@client_extension_search_paths - # end - def unknown_command(cmd, line) status = super - # if status.nil? - # # Check to see if we can find this command in another extension. This relies on the core extension being the last - # # in the dispatcher stack which it should be since it's the first loaded. - # Rex::Post::Meterpreter::ExtensionMapper.get_extension_names.each do |ext_name| - # next if extensions.include?(ext_name) - # ext_klass = get_extension_client_class(ext_name) - # next if ext_klass.nil? - # - # if ext_klass.has_command?(cmd) - # print_error("The \"#{cmd}\" command requires the \"#{ext_name}\" extension to be loaded (run: `load #{ext_name}`)") - # return :handled - # end - # end - # end - status end - - protected - - # attr_accessor :extensions # :nodoc: - # attr_accessor :bgjobs, :bgjob_id # :nodoc: - - # CommDispatcher = Console::CommandDispatcher - - # - # Loads the client extension specified in mod - # - # def add_extension_client(mod) - # klass = get_extension_client_class(mod) - # - # if klass.nil? - # print_error("Failed to load client portion of #{mod}.") - # return false - # end - # - # # Enstack the dispatcher - # self.shell.enstack_dispatcher(klass) - # - # # Insert the module into the list of extensions - # self.extensions << mod - # end - - # def get_extension_client_class(mod) - # self.class.client_extension_search_paths.each do |path| - # path = ::File.join(path, "#{mod}.rb") - # klass = CommDispatcher.check_hash(path) - # return klass unless klass.nil? - # - # old = CommDispatcher.constants - # next unless ::File.exist? path - # - # return nil unless require(path) - # - # new = CommDispatcher.constants - # diff = new - old - # - # next if (diff.empty?) - # - # klass = CommDispatcher.const_get(diff[0]) - # - # CommDispatcher.set_hash(path, klass) - # return klass - # end - # end - - # def tab_complete_modules(str, words) - # tabs = [] - # client.framework.modules.post.map do |name,klass| - # tabs << 'post/' + name - # end - # client.framework.modules.module_names('exploit'). - # grep(/(multi|#{Regexp.escape(client.platform)})\/local\//).each do |name| - # tabs << 'exploit/' + name - # end - # return tabs.sort - # end - end - end end end -end \ No newline at end of file +end diff --git a/modules/auxiliary/scanner/postgres/postgres_login.rb b/modules/auxiliary/scanner/postgres/postgres_login.rb index 9fd3a56bed456..e33390e3b8823 100644 --- a/modules/auxiliary/scanner/postgres/postgres_login.rb +++ b/modules/auxiliary/scanner/postgres/postgres_login.rb @@ -84,7 +84,6 @@ def run_host(ip) print_good "#{ip}:#{rport} - Login Successful: #{result.credential}" - #require 'pry-byebug'; binding.pry; if datastore['CreateSession'] == true begin postgresql_client = result.proof @@ -115,23 +114,13 @@ def rport def session_setup(result, client) return unless (result && client) - #require 'pry-byebug'; binding.pry; - - platform = 'MacOS' # scanner.get_platform(client) - - # TODO: Add 'dispatcher' to PostgreSQL client - #rstream = client.dispatcher.tcp_socket + # TODO: In the future, this can be extracted from a PostgreSQL query `select version()` + # E.g. in a Docker container running on MacOS: + # PostgreSQL 16.0 (Debian 16.0-1.pgdg120+1) on x86_64-pc-linux-gnu, \ + # compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit + platform = 'PostgreSQL' rstream = client.conn - - - begin - ::Msf::Sessions::Postgresql - rescue ::StandardError => _e - - end - - my_session = ::Msf::Sessions::PostgreSQL.new(rstream, { client: client } ) - + my_session = ::Msf::Sessions::PostgreSQL.new(rstream, { client: client }) merging = { 'USERPASS_FILE' => nil, 'USER_FILE' => nil, @@ -141,9 +130,7 @@ def session_setup(result, client) } s = start_session(self, nil, merging, false, my_session.rstream, my_session) - s.platform = platform - s end end