Land #14617, Better Handling for Incompatible Meterpreter Extensions and Commands
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
#
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user