diff --git a/lib/msf/core/exploit/powershell.rb b/lib/msf/core/exploit/powershell.rb index be16970ec5..0dc1fa02fb 100644 --- a/lib/msf/core/exploit/powershell.rb +++ b/lib/msf/core/exploit/powershell.rb @@ -1,10 +1,8 @@ # -*- coding: binary -*- -require 'rex/exploitation/powershell' +require 'rex/powershell' module Msf module Exploit::Powershell - PowershellScript = Rex::Exploitation::Powershell::Script - def initialize(info = {}) super register_advanced_options( @@ -27,15 +25,13 @@ module Exploit::Powershell # # @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| + opts = {} + datastore.select { |k, v| k =~ /^Powershell::(strip|sub)/ && v }.keys.map do |k| mod_method = k.split('::').last.intern - psh.send(mod_method) + opts[mod_method.to_sym] = true end - psh.encode_code + Rex::Powershell::Command.encode_script(script_in, opts) end # @@ -46,16 +42,14 @@ module Exploit::Powershell # @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| + def compress_script(script_in, eof=nil) + opts = {} + datastore.select { |k, v| k =~ /^Powershell::(strip|sub)/ && v }.keys.map do |k| mod_method = k.split('::').last.intern - psh.send(mod_method) + opts[mod_method.to_sym] = true end - psh.compress_code(eof) + Rex::Powershell::Command.compress_script(script_in, eof, opts) end # @@ -69,19 +63,7 @@ module Exploit::Powershell # # @return [String] Powershell command line with arguments def generate_psh_command_line(opts) - if opts[:path] and (opts[:path][-1, 1] != '\\') - opts[:path] << '\\' - end - - if opts[:no_full_stop] - binary = 'powershell' - else - binary = 'powershell.exe' - end - - args = generate_psh_args(opts) - - "#{opts[:path]}#{binary} #{args}" + Rex::Powershell::Command.generate_psh_command_line(opts) end # @@ -122,66 +104,7 @@ module Exploit::Powershell 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 - - # 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 + Rex::Powershell::Command.generate_psh_args(opts) end # @@ -196,41 +119,15 @@ module Exploit::Powershell # @return [String] Wrapped powershell code def run_hidden_psh(ps_code, payload_arch, encoded) arg_opts = { - noprofile: true, - windowstyle: 'hidden', + 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' + arg_opts[:noexit] = (datastore['Powershell::method'] == 'old') + arg_opts[:shorten] = (datastore['Powershell::method'] != 'old') - ps_args = generate_psh_args(arg_opts) - - process_start_info = < 0 - psh_payload = "Start-Sleep -s #{opts[:prepend_sleep]};" << psh_payload - elsif opts[:prepend_sleep].to_i < 0 - vprint_error('Sleep time must be greater than or equal to 0 seconds') - end - end - - compressed_payload = compress_script(psh_payload) - encoded_payload = encode_script(psh_payload) - - # This branch is probably never taken... - if encoded_payload.length <= compressed_payload.length - smallest_payload = encoded_payload - encoded = true - else - if opts[:encode_inner_payload] - encoded = true - compressed_encoded_payload = encode_script(compressed_payload) - - if encoded_payload.length <= compressed_encoded_payload.length - smallest_payload = encoded_payload - else - smallest_payload = compressed_encoded_payload - end - else - smallest_payload = compressed_payload - encoded = false - end - end - - # Wrap in hidden runtime / architecture detection - final_payload = run_hidden_psh(smallest_payload, payload_arch, encoded) - - command_args = { - noprofile: true, - windowstyle: 'hidden' - }.merge(opts) - - if opts[:encode_final_payload] - command_args[:encodedcommand] = encode_script(final_payload) - - # If '=' is a bad character pad the payload until Base64 encoded - # payload contains none. - if opts[:no_equals] - while command_args[:encodedcommand].include? '=' - final_payload << ' ' - command_args[:encodedcommand] = encode_script(final_payload) - end - end - else - if opts[:use_single_quotes] - # Escape Single Quotes - final_payload.gsub!("'", "''") - # Wrap command in quotes - final_payload = "'#{final_payload}'" - end - - command_args[:command] = final_payload - end - - psh_command = generate_psh_command_line(command_args) - - if opts[:remove_comspec] - command = psh_command - else - command = "%COMSPEC% /b /c start /b /min #{psh_command}" + unless opts.key? :shorten + opts[:shorten] = (datastore['Powershell::method'] != 'old') end + template_path = File.join(Msf::Config.data_directory, + "templates", + "scripts") + command = Rex::Powershell::Command.cmd_psh_payload(pay, + payload_arch, + template_path, + opts) vprint_status("Powershell command length: #{command.length}") - if command.length > 8191 - fail RuntimeError, 'Powershell command length is greater than the command line maximum (8192 characters)' - end command end + # # Useful method cache # module PshMethods - include Rex::Exploitation::Powershell::PshMethods + include Rex::Powershell::PshMethods end end end diff --git a/lib/msf/util/exe.rb b/lib/msf/util/exe.rb index ba7bdf2b08..3e1f19860e 100644 --- a/lib/msf/util/exe.rb +++ b/lib/msf/util/exe.rb @@ -15,6 +15,7 @@ require 'rex/peparsey' require 'rex/pescan' require 'rex/random_identifier_generator' require 'rex/zip' +require 'rex/powershell' require 'metasm' require 'digest/sha1' require 'msf/core/exe/segment_injector' @@ -1080,36 +1081,17 @@ require 'msf/core/exe/segment_appender' end def self.to_win32pe_psh_net(framework, code, opts={}) - rig = Rex::RandomIdentifierGenerator.new() - rig.init_var(:var_code) - rig.init_var(:var_kernel32) - rig.init_var(:var_baseaddr) - rig.init_var(:var_threadHandle) - rig.init_var(:var_output) - rig.init_var(:var_codeProvider) - rig.init_var(:var_compileParams) - rig.init_var(:var_syscode) - rig.init_var(:var_temp) - - hash_sub = rig.to_h - hash_sub[:b64shellcode] = Rex::Text.encode_base64(code) - - read_replace_script_template("to_mem_dotnet.ps1.template", hash_sub).gsub(/(?' - else - fail ArgumentError, 'Unknown starting bracket' - end - end - - # - # Extract block of code inside brackets/parenthesis - # - # Attempts to match the bracket at idx, handling nesting manually - # Once the balanced matching bracket is found, all script content - # between idx and the index of the matching bracket is returned - # - # @param idx [Integer] index of opening bracket - # - # @return [String] content between matching brackets - def block_extract(idx) - fail ArgumentError unless idx - - if idx < 0 || idx >= code.length - fail ArgumentError, 'Invalid index' - end - - start = code[idx] - stop = match_start(start) - delims = scan_with_index(/#{Regexp.escape(start)}|#{Regexp.escape(stop)}/, code[idx + 1..-1]) - delims.map { |x| x[1] = x[1] + idx + 1 } - c = 1 - sidx = nil - # Go through delims till we balance, get idx - while (c != 0) && (x = delims.shift) - sidx = x[1] - x[0] == stop ? c -= 1 : c += 1 - end - - code[idx..sidx] - end - - # - # Extract a block of function code - # - # @param func_name [String] function name - # @param delete [Boolean] delete the function from the code - # - # @return [String] function block - def get_func(func_name, delete = false) - start = code.index(func_name) - - return nil unless start - - idx = code[start..-1].index('{') + start - func_txt = block_extract(idx) - - if delete - delete_code = code[0..idx] - delete_code << code[(idx + func_txt.length)..-1] - @code = delete_code - end - - Function.new(func_name, func_txt) - end - end # Parser - end - end -end diff --git a/lib/rex/powershell.rb b/lib/rex/powershell.rb new file mode 100644 index 0000000000..a8df5a5c3c --- /dev/null +++ b/lib/rex/powershell.rb @@ -0,0 +1,62 @@ +# -*- coding: binary -*- +require 'rex/powershell/payload' +require 'rex/powershell/output' +require 'rex/powershell/parser' +require 'rex/powershell/obfu' +require 'rex/powershell/param' +require 'rex/powershell/function' +require 'rex/powershell/script' +require 'rex/powershell/psh_methods' +require 'rex/powershell/command' + + +module Rex + module Powershell + # + # Reads script into a PowershellScript + # + # @param script_path [String] Path to the Script File + # + # @return [Script] Powershell Script object + def self.read_script(script_path) + Rex::Powershell::Script.new(script_path) + end + + # + # Insert substitutions into the powershell script + # If script is a path to a file then read the file + # otherwise treat it as the contents of a file + # + # @param script [String] Script file or path to script + # @param subs [Array] Substitutions to insert + # + # @return [String] Modified script file + def self.make_subs(script, subs) + if ::File.file?(script) + script = ::File.read(script) + end + + subs.each do |set| + script.gsub!(set[0], set[1]) + end + + script + end + + # + # Return an array of substitutions for use in make_subs + # + # @param subs [String] A ; seperated list of substitutions + # + # @return [Array] An array of substitutions + def self.process_subs(subs) + return [] if subs.nil? or subs.empty? + new_subs = [] + subs.split(';').each do |set| + new_subs << set.split(',', 2) + end + + new_subs + end + end +end diff --git a/lib/rex/powershell/command.rb b/lib/rex/powershell/command.rb new file mode 100644 index 0000000000..f241387482 --- /dev/null +++ b/lib/rex/powershell/command.rb @@ -0,0 +1,359 @@ +# -*- coding: binary -*- + +module Rex +module Powershell +module Command + # + # Return an encoded powershell script + # Will invoke PSH modifiers as enabled + # + # @param script_in [String] Script contents + # @param opts [Hash] The options for encoding + # @option opts [Bool] :strip_comments Strip comments + # @option opts [Bool] :strip_whitespace Strip whitespace + # @option opts [Bool] :sub_vars Substitute variable names + # @option opts [Bool] :sub_funcs Substitute function names + # + # @return [String] Encoded script + def self.encode_script(script_in, opts={}) + # Build script object + psh = Rex::Powershell::Script.new(script_in) + psh.strip_comments if opts[:strip_comments] + psh.strip_whitespace if opts[:strip_whitespace] + psh.sub_vars if opts[:sub_vars] + psh.sub_funcs if opts[:sub_funcs] + psh.encode_code + end + + # + # Return a gzip compressed powershell script + # Will invoke PSH modifiers as enabled + # + # @param script_in [String] Script contents + # @param eof [String] Marker to indicate the end of file appended to script + # @param opts [Hash] The options for encoding + # @option opts [Bool] :strip_comments Strip comments + # @option opts [Bool] :strip_whitespace Strip whitespace + # @option opts [Bool] :sub_vars Substitute variable names + # @option opts [Bool] :sub_funcs Substitute function names + # + # @return [String] Compressed script with decompression stub + def self.compress_script(script_in, eof=nil, opts={}) + # Build script object + psh = Rex::Powershell::Script.new(script_in) + psh.strip_comments if opts[:strip_comments] + psh.strip_whitespace if opts[:strip_whitespace] + psh.sub_vars if opts[:sub_vars] + psh.sub_funcs if opts[:sub_funcs] + psh.compress_code(eof) + end + + # + # Generate a powershell command line, options are passed on to + # generate_psh_args + # + # @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 self.generate_psh_command_line(opts) + if opts[:path] and (opts[:path][-1, 1] != '\\') + opts[:path] << '\\' + end + + if opts[:no_full_stop] + binary = 'powershell' + else + binary = 'powershell.exe' + end + + args = generate_psh_args(opts) + + "#{opts[:path]}#{binary} #{args}" + end + + # + # 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 " + # + # @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 self.generate_psh_args(opts) + return '' unless opts + + unless opts.key? :shorten + opts[:shorten] = (opts[: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 + + # 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 + + # + # Wraps the powershell code to launch a hidden window and + # detect the execution environment and spawn the appropriate + # powershell executable for the payload architecture. + # + # @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 + # @param opts [Hash] The options for generate_psh_args + # + # @return [String] Wrapped powershell code + def self.run_hidden_psh(ps_code, payload_arch, encoded, opts={}) + opts[:noprofile] ||= 'true' + opts[:windowstyle] ||= 'hidden' + + # Old method needs host process to stay open + opts[:noexit] = true if (opts[:method] == 'old') + + if encoded + opts[:encodedcommand] = ps_code + else + opts[:command] = ps_code.gsub("'", "''") + end + + ps_args = generate_psh_args(opts) + + process_start_info = <