diff --git a/lib/msf/base/sessions/meterpreter.rb b/lib/msf/base/sessions/meterpreter.rb index 46c8724bfb..be50d62d03 100644 --- a/lib/msf/base/sessions/meterpreter.rb +++ b/lib/msf/base/sessions/meterpreter.rb @@ -151,6 +151,8 @@ class Meterpreter < Rex::Post::Meterpreter::Client # TODO: This session was either staged or previously known, and so we should do some accounting here! end + session.commands.concat(session.core.get_loaded_extension_commands('core')) + # Unhook the process prior to loading stdapi to reduce logging/inspection by any AV/PSP if datastore['AutoUnhookProcess'] == true console.run_single('load unhook') diff --git a/lib/rex/post/meterpreter/client.rb b/lib/rex/post/meterpreter/client.rb index 521992674e..866d54c847 100644 --- a/lib/rex/post/meterpreter/client.rb +++ b/lib/rex/post/meterpreter/client.rb @@ -316,7 +316,6 @@ class Client # registered extension that can be reached through client.ext.[extension]. # def add_extension(name, commands=[]) - self.commands |= [] self.commands.concat(commands) # Check to see if this extension has already been loaded. diff --git a/lib/rex/post/meterpreter/client_core.rb b/lib/rex/post/meterpreter/client_core.rb index 4ce4570955..21c7be285a 100644 --- a/lib/rex/post/meterpreter/client_core.rb +++ b/lib/rex/post/meterpreter/client_core.rb @@ -98,11 +98,16 @@ class ClientCore < Extension # # Get a list of loaded commands for the given extension. # - def get_loaded_extension_commands(extension_name) + # @param [String, Integer] extension Either the extension name or the extension ID to load the commands for. + # + # @return [Array] An array of command IDs that are supported by the specified extension. + def get_loaded_extension_commands(extension) request = Packet.create_request(COMMAND_ID_CORE_ENUMEXTCMD) - start = Rex::Post::Meterpreter::ExtensionMapper.get_extension_id(extension_name) - request.add_tlv(TLV_TYPE_UINT, start) + # handle 'core' as a special case since it's not a typical extension + extension = EXTENSION_ID_CORE if extension == 'core' + extension = Rex::Post::Meterpreter::ExtensionMapper.get_extension_id(extension) unless extension.is_a? Integer + request.add_tlv(TLV_TYPE_UINT, extension) request.add_tlv(TLV_TYPE_LENGTH, COMMAND_ID_RANGE) begin @@ -359,16 +364,20 @@ class ClientCore < Extension end if path.nil? and image.nil? - raise RuntimeError, "No module of the name #{modnameprovided} found", caller + if Rex::Post::Meterpreter::ExtensionMapper.get_extension_names.include?(mod.downcase) + raise RuntimeError, "The \"#{mod.downcase}\" extension is not supported by this Meterpreter type (#{client.session_type})", caller + else + raise RuntimeError, "No module of the name #{modnameprovided} found", caller + end end # Load the extension DLL commands = load_library( - 'LibraryFilePath' => path, + 'LibraryFilePath' => path, 'LibraryFileImage' => image, - 'UploadLibrary' => true, - 'Extension' => true, - 'SaveToDisk' => opts['LoadFromDisk']) + 'UploadLibrary' => true, + 'Extension' => true, + 'SaveToDisk' => opts['LoadFromDisk']) end # wire the commands into the client diff --git a/lib/rex/post/meterpreter/extension_mapper.rb b/lib/rex/post/meterpreter/extension_mapper.rb index d3ee0a6932..74c8f7ad95 100644 --- a/lib/rex/post/meterpreter/extension_mapper.rb +++ b/lib/rex/post/meterpreter/extension_mapper.rb @@ -16,18 +16,24 @@ class ExtensionMapper end def self.get_extension_id(name) - k = self.get_extension_klass(name) + begin + k = self.get_extension_klass(name) + rescue RuntimeError + return nil + end + k.extension_id end def self.get_extension_name(id) - self.get_extension_names.each do |name| + self.get_extension_names.find do |name| begin klass = self.get_extension_klass(name) rescue RuntimeError next end - return name if klass.extension_id == id + + klass.extension_id == id end end diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher.rb index 0c111df6ef..a1b08f7cd8 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher.rb @@ -38,6 +38,7 @@ module Console::CommandDispatcher def initialize(shell) @msf_loaded = nil + @filtered_commands = [] super end @@ -53,10 +54,22 @@ module Console::CommandDispatcher # def filter_commands(all, reqs) all.delete_if do |cmd, _desc| - reqs[cmd].any? { |req| !client.commands.include?(req) } + if reqs[cmd]&.any? { |req| !client.commands.include?(req) } + @filtered_commands << cmd + true + end end end + def unknown_command(cmd, line) + if @filtered_commands.include?(cmd) + print_error("The \"#{cmd}\" command is not supported by this Meterpreter type (#{client.session_type})") + return :handled + end + + super + end + # # Return the subdir of the `documentation/` directory that should be used # to find usage documentation diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb index abc16e33a7..00e66fcdc0 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb @@ -47,7 +47,7 @@ class Console::CommandDispatcher::Core # List of supported commands. # def commands - c = { + cmds = { '?' => 'Help menu', 'background' => 'Backgrounds the current session', 'bg' => 'Alias for background', @@ -69,55 +69,57 @@ class Console::CommandDispatcher::Core 'run' => 'Executes a meterpreter script or Post module', 'bgrun' => 'Executes a meterpreter script as a background thread', 'bgkill' => 'Kills a background meterpreter script', - 'get_timeouts' => 'Get the current session timeout values', - 'set_timeouts' => 'Set the current session timeout values', 'sessions' => 'Quickly switch to another session', 'bglist' => 'Lists running background scripts', 'write' => 'Writes data to a channel', 'enable_unicode_encoding' => 'Enables encoding of unicode strings', - 'disable_unicode_encoding' => 'Disables encoding of unicode strings' + 'disable_unicode_encoding' => 'Disables encoding of unicode strings', + 'migrate' => 'Migrate the server to another process', + 'pivot' => 'Manage pivot listeners', + # transport related commands + 'detach' => 'Detach the meterpreter session (for http/https)', + 'sleep' => 'Force Meterpreter to go quiet, then re-establish session', + 'transport' => 'Manage the transport mechanisms', + 'get_timeouts' => 'Get the current session timeout values', + 'set_timeouts' => 'Set the current session timeout values', + 'ssl_verify' => 'Modify the SSL certificate verification setting' } - if client.passive_service - c['detach'] = 'Detach the meterpreter session (for http/https)' - end - - # Currently we have some windows-specific core commands` - if client.platform == 'windows' - # only support the SSL switching for HTTPS - if client.passive_service && client.sock.type? == 'tcp-ssl' - c['ssl_verify'] = 'Modify the SSL certificate verification setting' - end - - c['pivot'] = 'Manage pivot listeners' - end - - if client.platform == 'windows' || client.platform == 'linux' - # Migration only supported on windows and linux - c['migrate'] = 'Migrate the server to another process' - end - - # TODO: This code currently checks both platform and architecture for the python - # and java types because technically the platform should be updated to indicate - # the OS platform rather than the meterpreter arch. When we've properly implemented - # the platform update feature we can remove some of these conditions - if client.platform == 'windows' || client.platform == 'linux' || - client.platform == 'python' || client.arch == ARCH_PYTHON || - client.platform == 'java' || client.arch == ARCH_JAVA || - client.platform == 'android' || client.arch == ARCH_DALVIK - # Yet to implement transport hopping for other meterpreters. - c['transport'] = 'Change the current transport mechanism' - - # sleep functionality relies on the transport features, so only - # wire that in with the transport stuff. - c['sleep'] = 'Force Meterpreter to go quiet, then re-establish session.' - end - if msf_loaded? - c['info'] = 'Displays information about a Post module' + cmds['info'] = 'Displays information about a Post module' end - c + reqs = { + 'load' => [COMMAND_ID_CORE_LOADLIB], + 'machine_id' => [COMMAND_ID_CORE_MACHINE_ID], + 'migrate' => [COMMAND_ID_CORE_MIGRATE], + 'pivot' => [COMMAND_ID_CORE_PIVOT_ADD, COMMAND_ID_CORE_PIVOT_REMOVE], + 'secure' => [COMMAND_ID_CORE_NEGOTIATE_TLV_ENCRYPTION], + # channel related commands + 'read' => [COMMAND_ID_CORE_CHANNEL_READ], + 'write' => [COMMAND_ID_CORE_CHANNEL_WRITE], + 'close' => [COMMAND_ID_CORE_CHANNEL_CLOSE], + # transport related commands + 'sleep' => [COMMAND_ID_CORE_TRANSPORT_SLEEP], + 'ssl_verify' => [COMMAND_ID_CORE_TRANSPORT_GETCERTHASH, COMMAND_ID_CORE_TRANSPORT_SETCERTHASH], + 'transport' => [ + COMMAND_ID_CORE_TRANSPORT_ADD, + COMMAND_ID_CORE_TRANSPORT_CHANGE, + COMMAND_ID_CORE_TRANSPORT_LIST, + COMMAND_ID_CORE_TRANSPORT_NEXT, + COMMAND_ID_CORE_TRANSPORT_PREV, + COMMAND_ID_CORE_TRANSPORT_REMOVE + ], + 'get_timeouts' => [COMMAND_ID_CORE_TRANSPORT_SET_TIMEOUTS], + 'set_timeouts' => [COMMAND_ID_CORE_TRANSPORT_SET_TIMEOUTS], + } + + # XXX: Remove this line once the payloads gem has had another major version bump from 2.x to 3.x and + # rapid7/metasploit-payloads#451 has been landed to correct the `enumextcmd` behavior on Windows. Until then, skip + # filtering for Windows which supports all the filtered commands anyways. + reqs.clear if client.base_platform == 'windows' + + filter_commands(cmds, reqs) end # @@ -508,6 +510,10 @@ class Console::CommandDispatcher::Core # Disconnects the session # def cmd_detach(*args) + unless client.passive_service + print_error('The detach command is not applicable with the current transport') + return + end client.shutdown_passive_dispatcher shell.stop end @@ -718,7 +724,7 @@ class Console::CommandDispatcher::Core @@ssl_verify_opts = Rex::Parser::Arguments.new( '-e' => [ false, 'Enable SSL certificate verification' ], '-d' => [ false, 'Disable SSL certificate verification' ], - '-q' => [ false, 'Query the statis of SSL certificate verification' ], + '-q' => [ false, 'Query the status of SSL certificate verification' ], '-h' => [ false, 'Help menu' ]) # @@ -741,6 +747,11 @@ class Console::CommandDispatcher::Core return end + unless client.passive_service && client.sock.type? == 'tcp-ssl' + print_error('The ssl_verify command is not applicable with the current transport') + return + end + query = false enable = false disable = false @@ -1259,23 +1270,7 @@ class Console::CommandDispatcher::Core # Use API to get list of extensions from the gem exts.merge(MetasploitPayloads::Mettle.available_extensions(client.sys.config.sysinfo['BuildTuple'])) else - msf_path = MetasploitPayloads.msf_meterpreter_dir - gem_path = MetasploitPayloads.local_meterpreter_dir - [msf_path, gem_path].each do |path| - ::Dir.entries(path).each { |f| - if (::File.file?(::File.join(path, f))) - client.binary_suffix.each { |s| - if (f =~ /ext_server_(.*)\.#{s}/ ) - if (client.binary_suffix.size > 1) - exts.add($1 + ".#{s}") - else - exts.add($1) - end - end - } - end - } - end + exts.merge(client.binary_suffix.map { |suffix| MetasploitPayloads.list_meterpreter_extensions(suffix) }.flatten) end print(exts.to_a.join("\n") + "\n") @@ -1291,7 +1286,7 @@ class Console::CommandDispatcher::Core md = m.downcase # Temporary hack to pivot mimikatz over to kiwi until - # everone remembers to do it themselves + # everyone remembers to do it themselves if md == 'mimikatz' print_warning('The "mimikatz" extension has been replaced by "kiwi". Please use this in future.') md = 'kiwi' @@ -1309,7 +1304,7 @@ class Console::CommandDispatcher::Core end if (extensions.include?(md)) - print_error("The '#{md}' extension has already been loaded.") + print_error("The \"#{md}\" extension has already been loaded.") next end @@ -1323,6 +1318,7 @@ class Console::CommandDispatcher::Core rescue print_line log_error("Failed to load extension: #{$!}") + next end @@ -1335,30 +1331,9 @@ class Console::CommandDispatcher::Core def cmd_load_tabs(str, words) tabs = SortedSet.new if extensions.include?('stdapi') && !client.sys.config.sysinfo['BuildTuple'].blank? - # Use API to get list of extensions from the gem - MetasploitPayloads::Mettle.available_extensions(client.sys.config.sysinfo['BuildTuple']).each { |f| - if !extensions.include?(f.split('.').first) - tabs.add(f) - end - } + tabs.merge(MetasploitPayloads::Mettle.available_extensions(client.sys.config.sysinfo['BuildTuple'])) else - msf_path = MetasploitPayloads.msf_meterpreter_dir - gem_path = MetasploitPayloads.local_meterpreter_dir - [msf_path, gem_path].each do |path| - ::Dir.entries(path).each { |f| - if (::File.file?(::File.join(path, f))) - client.binary_suffix.each { |s| - if (f =~ /ext_server_(.*)\.#{s}/ ) - if (client.binary_suffix.size > 1 && !extensions.include?($1 + ".#{s}")) - tabs.add($1 + ".#{s}") - elsif (!extensions.include?($1)) - tabs.add($1) - end - end - } - end - } - end + tabs.merge(client.binary_suffix.map { |suffix| MetasploitPayloads.list_meterpreter_extensions(suffix) }.flatten) end return tabs.to_a end diff --git a/lib/rex/ui/text/dispatcher_shell.rb b/lib/rex/ui/text/dispatcher_shell.rb index 5dd1f4ce55..6e90bf0c61 100644 --- a/lib/rex/ui/text/dispatcher_shell.rb +++ b/lib/rex/ui/text/dispatcher_shell.rb @@ -328,6 +328,17 @@ module DispatcherShell end addresses end + + # + # A callback that can be used to handle unknown commands. This can for example, allow a dispatcher to mark a command + # as being disabled. + # + # @return [Symbol, nil] Returns a symbol specifying the action that was taken by the handler or `nil` if no action + # was taken. The only supported action at this time is `:handled`, signifying that the unknown command was handled + # by this dispatcher and no additional dispatchers should receive it. + def unknown_command(method, line) + nil + end end # @@ -454,11 +465,16 @@ module DispatcherShell # # Run a single command line. # + # @param [String] line The command string that should be executed. + # @param [Boolean] propagate_errors Whether or not to raise exceptions that are caught while executing the command. + # + # @return [Boolean] A boolean value signifying whether or not the command was handled. Value is `true` when the + # command line was handled. def run_single(line, propagate_errors: false) - arguments = parse_line(line) - method = arguments.shift - found = false - error = false + arguments = parse_line(line) + method = arguments.shift + cmd_status = nil # currently either nil or :handled, more statuses can be added in the future + error = false # If output is disabled output will be nil output.reset_color if (output) @@ -473,10 +489,12 @@ module DispatcherShell if (dispatcher.commands.has_key?(method) or dispatcher.deprecated_commands.include?(method)) self.on_command_proc.call(line.strip) if self.on_command_proc run_command(dispatcher, method, arguments) - found = true + cmd_status = :handled + elsif cmd_status.nil? + cmd_status = dispatcher.unknown_command(method, line) end rescue ::Interrupt - found = true + cmd_status = :handled print_error("#{method}: Interrupted") raise if propagate_errors rescue OptionParser::ParseError => e @@ -504,12 +522,12 @@ module DispatcherShell break if (dispatcher_stack.length != entries) } - if (found == false and error == false) + if (cmd_status.nil? && error == false) unknown_command(method, line) end end - return found + return cmd_status == :handled end # diff --git a/test/modules/post/test/meterpreter.rb b/test/modules/post/test/meterpreter.rb index 22e33b9377..670e66d984 100644 --- a/test/modules/post/test/meterpreter.rb +++ b/test/modules/post/test/meterpreter.rb @@ -47,6 +47,19 @@ class MetasploitModule < Msf::Post super end + def test_core_command_id_enumeration + commands = [] + + it "should enumerate supported core commands" do + commands.concat(session.core.get_loaded_extension_commands('core')) + !commands.empty? + end + + # 3 is arbitrary, but it's probably a good bare minimum to include enumextcmd, machine_id, and loadlib + it "should support 3 or more core commands" do + commands.length >= 3 + end + end def test_sys_process vprint_status("Starting process tests")