Files
metasploit-gs/lib/msf/core/payload_generator.rb
T
Adam Cammack 5f8181efa7 Avoid auto-encoding payloads free of badchars
Payloads without any of the specified badchars will no longer be encoded
by default. This should hopefully lead to less surprising results when
using simple payloads (especially commands. Things that had incomplete
badchar analysis may break as a result, since not everything will be
encoded by default anymore. Sorry in advance if they do.
2020-05-07 10:58:03 -05:00

585 lines
23 KiB
Ruby

# -*- coding: binary -*-
require 'msf/core/payload/apk'
require 'active_support/core_ext/numeric/bytes'
require 'msf/core/payload/windows/payload_db_conf'
module Msf
class PayloadGeneratorError < StandardError
end
class EncoderSpaceViolation < PayloadGeneratorError
end
class PayloadSpaceViolation < PayloadGeneratorError
end
class IncompatibleArch < PayloadGeneratorError
end
class IncompatibleEndianess < PayloadGeneratorError
end
class IncompatiblePlatform < PayloadGeneratorError
end
class InvalidFormat < PayloadGeneratorError
end
class PayloadGenerator
# @!attribute add_code
# @return [String] The path to a shellcode file to execute in a separate thread
attr_accessor :add_code
# @!attribute arch
# @return [String] The CPU architecture to build the payload for
attr_accessor :arch
# @!attribute badchars
# @return [String] The bad characters that can't be in the payload
attr_accessor :badchars
# @!attribute cli
# @return [Boolean] Whether this is being run by a CLI script
attr_accessor :cli
# @!attribute datastore
# @return [Hash] The datastore to apply to the payload module
attr_accessor :datastore
# @!attribute encoder
# @return [String] The encoder(s) you want applied to the payload
attr_accessor :encoder
# @!attribute secname
# @return [String] The name of the new section within the generated Windows binary
attr_accessor :secname
# @!attribute format
# @return [String] The format you want the payload returned in
attr_accessor :format
# @!attribute framework
# @return [Msf::Framework] The framework instance to use for generation
attr_accessor :framework
# @!attribute iterations
# @return [Integer] The number of iterations to run the encoder
attr_accessor :iterations
# @!attribute keep
# @return [Boolean] Whether or not to preserve the original functionality of the template
attr_accessor :keep
# @!attribute nops
# @return [Integer] The size in bytes of NOP sled to prepend the payload with
attr_accessor :nops
# @!attribute padnops
# @return [Boolean] Whether to use @!attribute nops as the total payload size
attr_accessor :padnops
# @!attribute payload
# @return [String] The refname of the payload to generate
attr_accessor :payload
# @!attribute payload_module
# @return [Module] The payload module object if applicable
attr_accessor :payload_module
# @!attribute platform
# @return [String] The platform to build the payload for
attr_accessor :platform
# @!attribute smallest
# @return [Boolean] Whether or not to find the smallest possible output
attr_accessor :smallest
# @!attribute space
# @return [Integer] The maximum size in bytes of the payload
attr_accessor :space
# @!attribute encoder_space
# @return [Integer] The maximum size in bytes of the encoded payload
attr_accessor :encoder_space
# @!attribute stdin
# @return [String] The raw bytes of a payload taken from STDIN
attr_accessor :stdin
# @!attribute template
# @return [String] The path to an executable template to use
attr_accessor :template
# @!attribute var_name
# @return [String] The custom variable string for certain output formats
attr_accessor :var_name
# @!attribute encryption_format
# @return [String] The encryption format to use for the shellcode.
attr_accessor :encryption_format
# @!attribute encryption_key
# @return [String] The key to use for the encryption
attr_accessor :encryption_key
# @!attribute encryption_iv
# @return [String] The initialization vector for the encryption (not all apply)
attr_accessor :encryption_iv
# @param opts [Hash] The options hash
# @option opts [String] :payload (see #payload)
# @option opts [String] :format (see #format)
# @option opts [String] :encoder (see #encoder)
# @option opts [String] :secname (see #secname)
# @option opts [Integer] :iterations (see #iterations)
# @option opts [String] :arch (see #arch)
# @option opts [String] :platform (see #platform)
# @option opts [String] :badchars (see #badchars)
# @option opts [String] :template (see #template)
# @option opts [Integer] :space (see #space)
# @option opts [Integer] :encoder_space (see #encoder_space)
# @option opts [Integer] :nops (see #nops)
# @option opts [Boolean] :padnops (see #padnops)
# @option opts [String] :add_code (see #add_code)
# @option opts [Boolean] :keep (see #keep)
# @option opts [Hash] :datastore (see #datastore)
# @option opts [Msf::Framework] :framework (see #framework)
# @option opts [Boolean] :cli (see #cli)
# @option opts [Boolean] :smallest (see #smallest)
# @raise [KeyError] if framework is not provided in the options hash
def initialize(opts={})
@add_code = opts.fetch(:add_code, '')
@arch = opts.fetch(:arch, '')
@badchars = opts.fetch(:badchars, '')
@cli = opts.fetch(:cli, false)
@datastore = opts.fetch(:datastore, {})
@encoder = opts.fetch(:encoder, '')
@secname = opts.fetch(:secname, '')
@format = opts.fetch(:format, 'raw')
@iterations = opts.fetch(:iterations, 1)
@keep = opts.fetch(:keep, false)
@nops = opts.fetch(:nops, 0)
@padnops = opts.fetch(:padnops, false)
@payload = opts.fetch(:payload, '')
@platform = opts.fetch(:platform, '')
@space = opts.fetch(:space, 1.gigabyte)
@stdin = opts.fetch(:stdin, nil)
@template = opts.fetch(:template, '')
@var_name = opts.fetch(:var_name, 'buf')
@smallest = opts.fetch(:smallest, false)
@encoder_space = opts.fetch(:encoder_space, @space)
@encryption_format = opts.fetch(:encryption_format, nil)
@encryption_key = opts.fetch(:encryption_key, nil)
@encryption_iv = opts.fetch(:encryption_iv, nil)
@framework = opts.fetch(:framework)
raise InvalidFormat, "invalid format: #{format}" unless format_is_valid?
raise ArgumentError, "invalid payload: #{payload}" unless payload_is_valid?
# A side-effecto of running framework.payloads.create is that
# framework.payloads.keys gets pruned of unloadable payloads. So, we do it
# after checking payload_is_valid?, which refers to the cached keys.
@payload_module = framework.payloads.create(@payload)
raise ArgumentError, "unloadable payload: #{payload}" unless payload_module || @payload == 'stdin'
# In smallest mode, override the payload @space & @encoder_space settings
if @smallest
@space = 0
@encoder_space = 1.gigabyte
end
end
# This method takes the shellcode generated so far and adds shellcode from
# a supplied file. The added shellcode is executed in a separate thread
# from the main payload.
# @param shellcode [String] The shellcode to add to
# @return [String] the combined shellcode which executes the added code in a separate thread
def add_shellcode(shellcode)
if add_code.present? and platform_list.platforms.include? Msf::Module::Platform::Windows and arch == ARCH_X86
cli_print "Adding shellcode from #{add_code} to the payload"
shellcode_file = File.open(add_code)
shellcode_file.binmode
added_code = shellcode_file.read
shellcode_file.close
shellcode = ::Msf::Util::EXE.win32_rwx_exec_thread(shellcode,0,'end')
shellcode << added_code
else
shellcode.dup
end
end
# This method takes a payload module and tries to reconcile a chosen
# arch with the arches supported by the module.
# @param mod [Msf::Payload] The module class to choose an arch for
# @return [String] String form of the arch if a valid arch found
# @return [Nil] if no valid arch found
def choose_arch(mod)
if arch.blank?
@arch = mod.arch.first
cli_print "[-] No arch selected, selecting arch: #{arch} from the payload"
datastore['ARCH'] = arch if mod.kind_of?(Msf::Payload::Generic)
return mod.arch.first
elsif mod.arch.include? arch
datastore['ARCH'] = arch if mod.kind_of?(Msf::Payload::Generic)
return arch
else
return nil
end
end
# This method takes a payload module and tries to reconcile a chosen
# platform with the platforms supported by the module.
# @param mod [Msf::Payload] The module class to choose a platform for
# @return [Msf::Module::PlatformList] The selected platform list
def choose_platform(mod)
# By default, platform_list will at least return Msf::Module::Platform
# if there is absolutely no pre-configured platform info at all
chosen_platform = platform_list
if chosen_platform.platforms.empty?
chosen_platform = mod.platform
cli_print "[-] No platform was selected, choosing #{chosen_platform.platforms.first} from the payload"
@platform = mod.platform.platforms.first.to_s.split("::").last
elsif (chosen_platform & mod.platform).empty?
chosen_platform = Msf::Module::PlatformList.new
end
begin
platform_object = Msf::Module::Platform.find_platform(platform)
rescue ArgumentError
platform_object = nil
end
if mod.kind_of?(Msf::Payload::Generic) && mod.send(:module_info)['Platform'].empty? && platform_object
datastore['PLATFORM'] = platform
end
chosen_platform
end
def multiple_encode_payload(shellcode)
encoder_str = encoder[1..-1]
encoder_str.scan(/([^:, ]+):?([^,]+)?/).map do |encoder_opt|
@iterations = (encoder_opt[1] || 1).to_i
@iterations = 1 if iterations < 1
encoder_mod = framework.encoders.create(encoder_opt[0])
encoder_mod.datastore.import_options_from_hash(datastore)
shellcode = run_encoder(encoder_mod, shellcode)
end
shellcode
end
# This method takes the shellcode generated so far and iterates through
# the chosen or compatible encoders. It attempts to encode the payload
# with each encoder until it finds one that works.
# @param shellcode [String] The shellcode to encode
# @return [String] The encoded shellcode
def encode_payload(shellcode)
shellcode = shellcode.dup
encoder_list = get_encoders(shellcode)
if encoder_list.empty?
cli_print "No encoder specified, outputting raw payload"
return shellcode
end
results = {}
cli_print "Found #{encoder_list.count} compatible encoders"
encoder_list.each do |encoder_mod|
cli_print "Attempting to encode payload with #{iterations} iterations of #{encoder_mod.refname}"
begin
encoder_mod.available_space = @encoder_space unless @smallest
results[encoder_mod.refname] = run_encoder(encoder_mod, shellcode.dup)
break unless @smallest
rescue ::Msf::EncoderSpaceViolation => e
cli_print "#{encoder_mod.refname} failed with #{e.message}"
next
rescue ::Msf::EncodingError => e
cli_print "#{encoder_mod.refname} failed with #{e.message}"
next
end
end
if results.keys.length == 0
raise ::Msf::EncodingError, "No Encoder Succeeded"
end
# Return the shortest encoding of the payload
chosen_encoder = results.keys.sort{|a,b| results[a].length <=> results[b].length}.first
cli_print "#{chosen_encoder} chosen with final size #{results[chosen_encoder].length}"
results[chosen_encoder]
end
# This returns a hash for the exe format generation of payloads
# @return [Hash] The hash needed for generating an executable format
def exe_options
opts = { inject: keep }
unless template.blank?
opts[:template_path] = File.dirname(template)
opts[:template] = File.basename(template)
end
unless secname.blank?
opts[:secname] = secname
end
opts
end
# This method takes the payload shellcode and formats it appropriately based
# on the selected output format.
# @param shellcode [String] the processed shellcode to be formatted
# @return [String] The final formatted form of the payload
def format_payload(shellcode)
encryption_opts = {}
encryption_opts[:format] = encryption_format if encryption_format
encryption_opts[:iv] = encryption_iv if encryption_iv
encryption_opts[:key] = encryption_key if encryption_key
if Msf::Util::EXE.elf?(shellcode) && format.downcase != 'elf'
# TODO: force generation from stager/stage if available
raise InvalidFormat, 'selected payload can only generate ELF files'
end
if Msf::Util::EXE.macho?(shellcode) && format.downcase != 'macho'
# TODO: force generation from stager/stage if available
raise InvalidFormat, 'selected payload can only generate MACHO files'
end
case format.downcase
when "js_be"
if Rex::Arch.endian(arch) != ENDIAN_BIG
raise IncompatibleEndianess, "Big endian format selected for a non big endian payload"
else
::Msf::Simple::Buffer.transform(shellcode, format, @var_name, encryption_opts)
end
when *::Msf::Simple::Buffer.transform_formats
::Msf::Simple::Buffer.transform(shellcode, format, @var_name, encryption_opts)
when *::Msf::Util::EXE.to_executable_fmt_formats
::Msf::Util::EXE.to_executable_fmt(framework, arch, platform_list, shellcode, format, exe_options)
else
raise InvalidFormat, "you have selected an invalid payload format"
end
end
# This method generates Java payloads which are a special case.
# They can be generated in raw or war formats, which respectively
# produce a JAR or WAR file for the java payload.
# @return [String] Java payload as a JAR or WAR file
def generate_java_payload
payload_module.datastore.import_options_from_hash(datastore)
case format
when "raw", "jar"
if payload_module.respond_to? :generate_jar
payload_module.generate_jar.pack
else
payload_module.generate
end
when "war"
if payload_module.respond_to? :generate_war
payload_module.generate_war.pack
else
raise InvalidFormat, "#{payload} is not a Java payload"
end
when "axis2"
if payload_module.respond_to? :generate_axis2
payload_module.generate_axis2.pack
else
raise InvalidFormat, "#{payload} is not a Java payload"
end
else
raise InvalidFormat, "#{format} is not a valid format for Java payloads"
end
end
# This method is a wrapper around all of the other methods. It calls the correct
# methods in order based on the supplied options and returns the finished payload.
# @return [String] A string containing the bytes of the payload in the format selected
def generate_payload
if payload.include?("pingback") and framework.db.active == false
cli_print "[-] WARNING: UUID cannot be saved because database is inactive."
end
if platform == "java" or arch == "java" or payload.start_with? "java/"
raw_payload = generate_java_payload
encoded_payload = raw_payload
gen_payload = raw_payload
elsif payload.start_with? "android/" and not template.blank?
if payload.start_with? "android/meterpreter_"
raise PayloadGeneratorError, "Stageless Android payloads (e.g #{payload}) are not compatible with injection (-x)"
end
cli_print "Using APK template: #{template}"
apk_backdoor = ::Msf::Payload::Apk.new
raw_payload = apk_backdoor.backdoor_apk(template, generate_raw_payload)
gen_payload = raw_payload
else
if payload_module.is_a?(Msf::Payload::Windows::PayloadDBConf)
payload_module.datastore.import_options_from_hash(datastore)
ds_opt = payload_module.datastore
cli_print("[!] Database is not active! Payload key and nonce must be manually set when creating handler") unless framework.db.active
cli_print("[-] Please ensure payload key and nonce match when setting up handler: #{ds_opt['ChachaKey']} - #{ds_opt['ChachaNonce']}")
end
raw_payload = generate_raw_payload
raw_payload = add_shellcode(raw_payload)
if encoder != nil and encoder.start_with?("@")
raw_payload = multiple_encode_payload(raw_payload)
else
raw_payload = encode_payload(raw_payload)
end
if padnops
@nops = nops - raw_payload.length
end
raw_payload = prepend_nops(raw_payload)
gen_payload = format_payload(raw_payload)
end
cli_print "Payload size: #{raw_payload.length} bytes"
if gen_payload.nil?
raise PayloadGeneratorError, 'The payload could not be generated, check options'
elsif raw_payload.length > @space and not @smallest
raise PayloadSpaceViolation, 'The payload exceeds the specified space'
else
if format.to_s != 'raw'
cli_print "Final size of #{format} file: #{gen_payload.length} bytes"
end
gen_payload
end
end
# This method generates the raw form of the payload as generated by the payload module itself.
# @raise [Msf::IncompatiblePlatform] if no platform was selected for a stdin payload
# @raise [Msf::IncompatibleArch] if no arch was selected for a stdin payload
# @raise [Msf::IncompatiblePlatform] if the platform is incompatible with the payload
# @raise [Msf::IncompatibleArch] if the arch is incompatible with the payload
# @return [String] the raw bytes of the payload to be generated
def generate_raw_payload
if payload == 'stdin'
if arch.blank?
raise IncompatibleArch, "You must select an arch for a custom payload"
elsif platform.blank?
raise IncompatiblePlatform, "You must select a platform for a custom payload"
end
stdin
else
chosen_platform = choose_platform(payload_module)
if chosen_platform.platforms.empty?
raise IncompatiblePlatform, "The selected platform is incompatible with the payload"
end
chosen_arch = choose_arch(payload_module)
unless chosen_arch
raise IncompatibleArch, "The selected arch is incompatible with the payload"
end
payload_module.generate_simple(
'Format' => 'raw',
'Options' => datastore,
'Encoder' => nil,
'MaxSize' => @space,
'DisableNops' => true
)
end
end
# This method returns an array of encoders that either match the
# encoders selected by the user, or match the arch selected.
# @return [Array<Msf::Encoder>] An array of potential encoders to use
def get_encoders(buf)
encoders = []
if encoder.present?
# Allow comma separated list of encoders so users can choose several
encoder.split(',').each do |chosen_encoder|
e = framework.encoders.create(chosen_encoder)
if e.nil?
cli_print "[-] Skipping invalid encoder #{chosen_encoder}"
next
end
e.datastore.import_options_from_hash(datastore)
encoders << e if e
end
if encoders.empty?
cli_print "[!] Couldn't find encoder to use"
return encoders
end
encoders.sort_by { |my_encoder| my_encoder.rank }.reverse
elsif !badchars.empty? && !badchars.nil?
badchars_present = false
badchars.each_byte do |bad|
badchars_present = true if buf.index(bad.chr(Encoding::ASCII_8BIT))
end
unless badchars_present
cli_print "No badchars present in payload, skipping automatic encoding"
return []
end
framework.encoders.each_module_ranked('Arch' => [arch], 'Platform' => platform_list) do |name, mod|
e = framework.encoders.create(name)
e.datastore.import_options_from_hash(datastore)
encoders << e if e
end
encoders.select{ |my_encoder| my_encoder.rank != ManualRanking }.sort_by { |my_encoder| my_encoder.rank }.reverse
else
encoders
end
end
# Returns a PlatformList object based on the platform string given at creation.
# @return [Msf::Module::PlatformList] It will be empty if no valid platforms found
def platform_list
if platform.blank?
list = Msf::Module::PlatformList.new
else
begin
list = ::Msf::Module::PlatformList.transform(platform)
rescue
list = Msf::Module::PlatformList.new
end
end
list
end
# This method takes an encoded payload and prepends a NOP Sled to it
# with a size based on the nops value given to the generator.
# @param shellcode [String] The shellcode to prepend the NOPs to
# @return [String] the shellcode with the appropriate nopsled affixed
def prepend_nops(shellcode)
if nops > 0
framework.nops.each_module_ranked('Arch' => [arch]) do |name, mod|
nop = framework.nops.create(name)
raw = nop.generate_sled(nops, {'BadChars' => badchars, 'SaveRegisters' => [ 'esp', 'ebp', 'esi', 'edi' ] })
if raw
cli_print "Successfully added NOP sled of size #{raw.length} from #{name}"
return raw + shellcode
end
end
else
shellcode
end
end
# This method runs a specified encoder, for a number of defined iterations against the shellcode.
# @param encoder_module [Msf::Encoder] The Encoder to run against the shellcode
# @param shellcode [String] The shellcode to be encoded
# @return [String] The encoded shellcode
# @raise [Msf::EncoderSpaceViolation] If the Encoder makes the shellcode larger than the supplied space limit
def run_encoder(encoder_module, shellcode)
iterations.times do |x|
shellcode = encoder_module.encode(shellcode.dup, badchars, nil, platform_list)
cli_print "#{encoder_module.refname} succeeded with size #{shellcode.length} (iteration=#{x})"
if shellcode.length > encoder_space
raise EncoderSpaceViolation, "encoder has made a buffer that is too big"
end
end
shellcode
end
private
# This method prints output to the console if running in CLI mode
# @param [String] message The message to print to the console.
def cli_print(message= '')
$stderr.puts message if cli
end
# This method checks if the Generator's selected format is valid
# @return [True] if the format is valid
# @return [False] if the format is not valid
def format_is_valid?
formats = (::Msf::Util::EXE.to_executable_fmt_formats + ::Msf::Simple::Buffer.transform_formats).uniq
formats.include? format.downcase
end
# This method checks if the Generator's selected payload is valid
# @return [True] if the payload is a valid Metasploit Payload
# @return [False] if the payload is not a valid Metasploit Payload
def payload_is_valid?
(framework.payloads.keys + ['stdin']).include? payload
end
end
end