Land #14617, Better Handling for Incompatible Meterpreter Extensions and Commands

This commit is contained in:
Grant Willcox
2021-02-19 18:19:09 -06:00
8 changed files with 140 additions and 105 deletions
+2
View File
@@ -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')
-1
View File
@@ -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.
+17 -8
View File
@@ -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<Integer>] 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
+9 -3
View File
@@ -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
@@ -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
@@ -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
+26 -8
View File
@@ -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
#
+13
View File
@@ -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")