333 lines
11 KiB
Ruby
333 lines
11 KiB
Ruby
# 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
|