lib/msf/core/session_compatibility.rb
# frozen_string_literal: true
module Msf
module SessionCompatibility
include Msf::Auxiliary::Report
include Msf::Module::HasActions
include Msf::Post::Common
def initialize(info = {})
super
# Default stance is active
self.passive = info['Passive'] || false
self.session_types = info['SessionTypes'] || []
end
#
# Grabs a session object from the framework or raises {OptionValidateError}
# if one doesn't exist and is required. Initializes user input and output on the session.
#
# @raise [OptionValidateError] if {#session} returns nil
def setup
alert_user
if options['SESSION']&.required && session.blank?
raise Msf::OptionValidateError, ['SESSION']
end
if datastore['SESSION'] && session.nil?
raise Msf::OptionValidateError, ['SESSION']
end
# Msf::Exploit#setup for exploits, NoMethodError for post modules
super rescue NoMethodError
return unless session
# Check session readiness before compatibility so the session can be queried
# for its platform, capabilities, etc.
check_for_session_readiness if session.type == "meterpreter"
incompatibility_reasons = session_incompatibility_reasons(session)
if incompatibility_reasons.any?
print_warning('SESSION may not be compatible with this module:')
incompatibility_reasons.each do |reason|
print_warning(" * #{reason}")
end
end
@session.init_ui(user_input, user_output) if @session
@sysinfo = nil
end
# Meterpreter sometimes needs a little bit of extra time to
# actually be responsive for post modules. Default tries
# and retries for 5 seconds.
def check_for_session_readiness(tries=6)
session_ready_count = 0
session_ready = false
until session.sys or session_ready_count > tries
session_ready_count += 1
back_off_period = (session_ready_count**2)/10.0
select(nil,nil,nil,back_off_period)
end
session_ready = !!session.sys
unless session_ready
raise "The stdapi extension has not been loaded yet." unless session.tlv_enc_key.nil?
raise "Could not get a hold of the session."
end
return session_ready
end
#
# Default cleanup handler does nothing
#
def cleanup
super if defined?(super)
end
#
# Return the associated session or nil if there isn't one
#
# @return [Msf::Session]
# @return [nil] if the id provided in the datastore does not
# correspond to a session
def session
# Try the cached one
return @session if @session && !session_changed?
if datastore['SESSION']
@session = framework.sessions.get(datastore['SESSION'].to_i)
else
@session = nil
end
@session
end
def session_display_info
"Session: #{session.sid} (#{session.session_host})"
end
alias :client :session
#
# Cached sysinfo, returns nil for non-meterpreter sessions
#
# @return [Hash,nil]
def sysinfo
begin
@sysinfo ||= session.sys.config.sysinfo
rescue NoMethodError
@sysinfo = nil
end
@sysinfo
end
#
# Can be overridden by individual modules to add new commands
#
def post_commands
{}
end
# Whether this module's {Msf::Exploit::Stance} is {Msf::Exploit::Stance::Passive passive}
def passive?
passive
end
#
# Return a (possibly empty) list of all compatible sessions
#
# @return [Array]
def compatible_sessions
sessions = []
framework.sessions.each do |sid, s|
sessions << sid if session_compatible?(s)
end
sessions
end
#
# Return false if the given session is not compatible with this module
#
# Checks the session's type against this module's
# <tt>module_info["SessionTypes"]</tt> as well as examining platform
# and arch compatibility.
#
# +sess_or_sid+ can be a Session object, Integer, or
# String. In the latter cases it should be a key in
# +framework.sessions+.
#
# @note Because it errs on the side of compatibility, a true return
# value from this method does not guarantee the module will work
# with the session. For example, ARCH_CMD modules can work on a
# variety of platforms and archs and thus return true in this check.
#
# @param sess_or_sid [Msf::Session,Integer,String]
# A session or session ID to compare against this module for
# compatibility.
#
def session_compatible?(sess_or_sid)
session_incompatibility_reasons(sess_or_sid).empty?
end
#
# Return the reasons why a session is incompatible.
#
# @return Array<String>
def session_incompatibility_reasons(sess_or_sid)
# Normalize the argument to an actual Session
case sess_or_sid
when ::Integer, ::String
s = framework.sessions[sess_or_sid.to_i]
when ::Msf::Session
s = sess_or_sid
when nil
# No session provided
return []
end
issues = []
# Can't do anything without a session
unless s
issues << ['invalid session']
return issues
end
# Can't be compatible if it's the wrong type
if session_types && !session_types.include?(s.type)
issues << "incompatible session type: #{s.type}. This module works with: #{session_types.join(', ')}."
end
# Check to make sure architectures match
mod_arch = module_info['Arch']
if mod_arch
if s.arch.blank?
issues << 'Unknown session arch'
else
mod_arch = Array.wrap(mod_arch)
# Assume ARCH_CMD modules can work on supported SessionTypes since both shell and meterpreter types can execute commands
issues << "incompatible session architecture: #{s.arch}" unless mod_arch.include?(s.arch) || mod_arch.include?(ARCH_CMD)
end
end
# Arch is okay, now check the platform.
if platform && platform.is_a?(Msf::Module::PlatformList) && !platform.empty?
if s.platform.blank?
issues << "Unknown session platform. This module works with: #{platform.names.join(', ')}."
elsif !platform.supports?(Msf::Module::PlatformList.transform(s.platform))
issues << "incompatible session platform: #{s.platform}. This module works with: #{platform ? platform.names.join(', ') : platform.inspect}."
end
end
# Check all specified meterpreter commands are provided by the remote session
if s.type == 'meterpreter'
issues += meterpreter_session_incompatibility_reasons(s)
end
issues
end
#
# True when this module is passive, false when active
#
# @return [Boolean]
# @see passive?
attr_reader :passive
#
# A list of compatible session types
#
# @return [Array]
attr_reader :session_types
protected
attr_writer :passive, :session_types
def session_changed?
@ds_session ||= datastore['SESSION']
if (@ds_session != datastore['SESSION'])
@ds_session = nil
return true
else
return false
end
end
#
# Return the reasons why a meterpreter session is incompatible. Checks all specified meterpreter commands
# are provided by the remote session
#
# @return Array<String>
def meterpreter_session_incompatibility_reasons(session)
cmd_name_wildcards = module_info.dig('Compat', 'Meterpreter', 'Commands') || []
cmd_names = Rex::Post::Meterpreter::CommandMapper.get_command_names.select do |cmd_name|
cmd_name_wildcards.any? { |cmd_name_wildcard| ::File.fnmatch(cmd_name_wildcard, cmd_name) }
end
unmatched_wildcards = cmd_name_wildcards.select { |cmd_name_wildcard| cmd_names.none? { |cmd_name| ::File.fnmatch(cmd_name_wildcard, cmd_name) } }
unless unmatched_wildcards.empty?
# This implies that there was a typo in one of the wildcards because it didn't match anything. This is a developer mistake.
wlog("The #{fullname} module specified the following Meterpreter command wildcards that did not match anything: #{ unmatched_wildcards.join(', ') }")
end
cmd_ids = cmd_names.map { |name| Rex::Post::Meterpreter::CommandMapper.get_command_id(name) }
# XXX: Remove this condition 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
# proactive validation of Windows core commands. This is not the only instance of this workaround.
if session.base_platform == 'windows'
cmd_ids = cmd_ids.select do |cmd_id|
!cmd_id.between?(
Rex::Post::Meterpreter::ClientCore.extension_id,
Rex::Post::Meterpreter::ClientCore.extension_id + Rex::Post::Meterpreter::COMMAND_ID_RANGE - 1
)
end
end
# Windows does not support chmod, but will be defined by default in the file mixin
if session.base_platform == 'windows'
cmd_ids -= [Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_FS_CHMOD]
end
missing_cmd_ids = (cmd_ids - session.commands)
unless missing_cmd_ids.empty?
# If there are missing commands, try to load the necessary extension.
# If core_loadlib isn't supported, then extensions can't be loaded
return ['missing Meterpreter features: core can not be extended'] unless session.commands.include?(Rex::Post::Meterpreter::COMMAND_ID_CORE_LOADLIB)
# Since core is already loaded, if the missing command is a core command then it's truly missing
missing_core_cmd_ids = missing_cmd_ids.select do |cmd_id|
cmd_id.between?(
Rex::Post::Meterpreter::ClientCore.extension_id,
Rex::Post::Meterpreter::ClientCore.extension_id + Rex::Post::Meterpreter::COMMAND_ID_RANGE - 1
)
end
if missing_core_cmd_ids.any?
return ["missing Meterpreter features: #{command_names_for(missing_core_cmd_ids)}"]
end
missing_extensions = missing_cmd_ids.map { |cmd_id| Rex::Post::Meterpreter::ExtensionMapper.get_extension_name(cmd_id) }.uniq
missing_extensions.each do |ext_name|
# If the extension is already loaded, the command is truly missing
return ["missing Meterpreter features: #{command_names_for(missing_cmd_ids)}"] if session.ext.aliases.include?(ext_name)
begin
session.core.use(ext_name)
rescue RuntimeError
return ["unloadable Meterpreter extension: #{ext_name}"]
end
end
end
missing_cmd_ids -= session.commands
return ["missing Meterpreter features: #{command_names_for(missing_cmd_ids)}"] unless missing_cmd_ids.empty?
[]
end
def command_names_for(command_ids)
command_ids.map { |id| Rex::Post::Meterpreter::CommandMapper.get_command_name(id) }.join(', ')
end
end
end