Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling for incompatible Meterpreter extensions and commands (Round 2) #15109

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 11 additions & 8 deletions lib/rex/post/meterpreter/client_core.rb
Expand Up @@ -309,15 +309,17 @@ def load_library(opts)

#
# Loads a meterpreter extension on the remote server instance and
# initializes the client-side extension handlers
# initializes the client-side extension handlers.
#
# Module
# The module that should be loaded
# @param [String] mod The extension that should be loaded.
# @param [Hash] opts The options with which to load the extension.
# @option opts [String] LoadFromDisk Indicates that the library should be
# loaded from disk, not from memory on the remote machine.
#
# LoadFromDisk
# Indicates that the library should be loaded from disk, not from
# memory on the remote machine
# @raise [RuntimeError] An exception is raised if the extension could not be
# loaded.
#
# @return [true] This always returns true or raises an exception.
def use(mod, opts = { })
if mod.nil?
raise RuntimeError, "No modules were specified", caller
Expand Down Expand Up @@ -364,10 +366,11 @@ def use(mod, opts = { })
end

if path.nil? and image.nil?
error = Rex::Post::Meterpreter::ExtensionLoadError.new(name: mod.downcase)
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
raise error, "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
raise error, "No module of the name #{modnameprovided} found", caller
end
end

Expand Down
15 changes: 15 additions & 0 deletions lib/rex/post/meterpreter/extension.rb
Expand Up @@ -4,6 +4,21 @@ module Rex
module Post
module Meterpreter

#
# An error that is raised when a particular Meterpreter extension can not be
# loaded for any reason.
#
# @attr_reader [String] name The name of the extension that could not be loaded.
class ExtensionLoadError < RuntimeError
attr_reader :name

# @param [String] name The name of the extension that could not be loaded.
def initialize(name:)
@name = name
super
end
end

###
#
# Base class for all extensions that holds a reference to the
Expand Down
18 changes: 0 additions & 18 deletions lib/rex/post/meterpreter/extensions/mimikatz/command_ids.rb

This file was deleted.

3 changes: 0 additions & 3 deletions lib/rex/post/meterpreter/packet.rb
Expand Up @@ -150,7 +150,6 @@ def self.generate_command_id_map_c
powershell
lanattacks
peinjector
mimikatz
})

command_ids = id_map.map {|k, v| "#define COMMAND_ID_#{k.upcase} #{v}"}
Expand Down Expand Up @@ -234,7 +233,6 @@ def self.generate_command_id_map_python_extension
powershell
lanattacks
peinjector
mimikatz
})
command_ids = id_map.map {|k, v| "COMMAND_ID_#{k.upcase} = #{v}"}
%Q^
Expand Down Expand Up @@ -263,7 +261,6 @@ def self.generate_command_id_map_csharp
powershell
lanattacks
peinjector
mimikatz
})
command_ids = id_map.map {|k, v| "#{k.split('_').map(&:capitalize).join} = #{v},"}
%Q^
Expand Down
118 changes: 84 additions & 34 deletions lib/rex/post/meterpreter/ui/console/command_dispatcher/core.rb
Expand Up @@ -1320,9 +1320,45 @@ def cmd_load(*args)
if (client.core.use(modulenameprovided) == true)
add_extension_client(md)
end
rescue
rescue => ex
print_line
log_error("Failed to load extension: #{$!}")
log_error("Failed to load extension: #{ex.message}")
if ex.kind_of?(ExtensionLoadError) && ex.name
# MetasploitPayloads and MetasploitPayloads::Mettle do things completely differently, build an array of
# suggestion keys (binary_suffixes and Mettle build-tuples)
suggestion_keys = MetasploitPayloads.list_meterpreter_extension_suffixes(ex.name) + MetasploitPayloads::Mettle.available_platforms(ex.name)
suggestion_map = {
# Extension Suffixes
'jar' => 'java',
'php' => 'php',
'py' => 'python',
'x64.dll' => 'windows/x64',
'x86.dll' => 'windows',
# Mettle Platforms
'aarch64-iphone-darwin' => 'apple_ios/aarch64',
'aarch64-linux-musl' => 'linux/aarch64',
'arm-iphone-darwin' => 'apple_ios/armle',
'armv5b-linux-musleabi' => 'linux/armbe',
'armv5l-linux-musleabi' => 'linux/armle',
'i486-linux-musl' => 'linux/x86',
'mips64-linux-muslsf' => 'linux/mips64',
'mipsel-linux-muslsf' => 'linux/mipsle',
'mips-linux-muslsf' => 'linux/mipsbe',
'powerpc64le-linux-musl' => 'linux/ppc64le',
'powerpc-e500v2-linux-musl' => 'linux/ppce500v2',
'powerpc-linux-muslsf' => 'linux/ppc',
's390x-linux-musl' => 'linux/zarch',
'x86_64-apple-darwin' => 'osx/x64',
'x86_64-linux-musl' => 'linux/x64',
}
suggestions = suggestion_map.select { |k,_v| suggestion_keys.include?(k) }.values
unless suggestions.empty?
log_error("The \"#{ex.name}\" extension is supported by the following Meterpreter payloads:")
suggestions.each do |suggestion|
log_error(" - #{suggestion}/meterpreter*")
end
end
end

next
end
Expand Down Expand Up @@ -1762,6 +1798,27 @@ 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:
Expand All @@ -1773,39 +1830,9 @@ def self.client_extension_search_paths
# Loads the client extension specified in mod
#
def add_extension_client(mod)
loaded = false
klass = nil
self.class.client_extension_search_paths.each do |path|
path = ::File.join(path, "#{mod}.rb")
klass = CommDispatcher.check_hash(path)
if (klass == nil)
old = CommDispatcher.constants
next unless ::File.exist? path

if (require(path))
new = CommDispatcher.constants
diff = new - old

next if (diff.empty?)

klass = CommDispatcher.const_get(diff[0])

CommDispatcher.set_hash(path, klass)
loaded = true
break
else
print_error("Failed to load client script file: #{path}")
return false
end
klass = get_extension_client_class(mod)

else
# the klass is already loaded, from a previous invocation
loaded = true
break
end
end

unless loaded
if klass.nil?
print_error("Failed to load client portion of #{mod}.")
return false
end
Expand All @@ -1817,6 +1844,29 @@ def add_extension_client(mod)
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|
Expand Down
Expand Up @@ -32,6 +32,10 @@ class Console::CommandDispatcher::Extapi

include Console::CommandDispatcher

def self.has_command?(name)
Dispatchers.any? { |klass| klass.has_command?(name) }
end

#
# Initializes an instance of the extended API command interaction.
#
Expand Down
Expand Up @@ -26,6 +26,10 @@ class Console::CommandDispatcher::Lanattacks

include Console::CommandDispatcher

def self.has_command?(name)
Dispatchers.any? { |klass| klass.has_command?(name) }
end

#
# Initializes an instance of the lanattacks command interaction.
#
Expand Down
Expand Up @@ -28,6 +28,10 @@ class Console::CommandDispatcher::Priv

include Console::CommandDispatcher

def self.has_command?(name)
Dispatchers.any? { |klass| klass.has_command?(name) }
end

#
# Initializes an instance of the priv command interaction.
#
Expand Down
Expand Up @@ -37,6 +37,10 @@ class Console::CommandDispatcher::Stdapi

include Console::CommandDispatcher

def self.has_command?(name)
Dispatchers.any? { |klass| klass.has_command?(name) }
end

#
# Initializes an instance of the stdapi command interaction.
#
Expand Down
25 changes: 24 additions & 1 deletion lib/rex/ui/text/dispatcher_shell.rb
Expand Up @@ -28,6 +28,29 @@ module DispatcherShell
###
module CommandDispatcher

module ClassMethods
#
# Check whether or not the command dispatcher is capable of handling the
# specified command. The command may still be disabled through some means
# at runtime.
#
# @param [String] name The name of the command to check.
# @return [Boolean] true if the dispatcher can handle the command.
def has_command?(name)
self.method_defined?("cmd_#{name}")
end

def included(base)
# Propagate the included hook
CommandDispatcher.included(base)
end
end

def self.included(base)
# Install class methods so they are inheritable
base.extend(ClassMethods)
end
Comment on lines +31 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another comment to help future travelers:

This sets up a system so that anything that includes either CommandDispatcher or ClassMethods now has a hook so that when that class is included elsewhere (please note its only include that is hooked, see https://apidock.com/ruby/Module/included for more information on how the included() method operates), the hook (aka the included() method defined in ClassMethods) propagates itself and that class is extended to include the propagation hook function via included() and also add the has_command?() function to whatever class it is imported into.

This has the effect of essentially propagating the has_command?() function infinitely through every class or module that imports , either directly or indirectly, from CommandDispatcher or ClassMethods, thereby ensuring has_command()? is populated as a class method through all these indirect imports.


#
# Initializes the command dispatcher mixin.
#
Expand Down Expand Up @@ -539,7 +562,7 @@ def run_command(dispatcher, method, arguments)
# If the command is unknown...
#
def unknown_command(method, line)
print_error("Unknown command: #{method}.")
print_error("Unknown command: #{method}")
end

#
Expand Down