diff --git a/Gemfile.lock b/Gemfile.lock index d91b47f92c..fdfc0f4102 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,9 +41,9 @@ PATH metasploit-concern metasploit-credential metasploit-model - metasploit-payloads (= 2.0.175) + metasploit-payloads (= 2.0.183) metasploit_data_models - metasploit_payloads-mettle (= 1.0.31) + metasploit_payloads-mettle (= 1.0.32) mqtt msgpack (~> 1.6.0) mutex_m @@ -295,7 +295,7 @@ GEM activemodel (~> 7.0) activesupport (~> 7.0) railties (~> 7.0) - metasploit-payloads (2.0.175) + metasploit-payloads (2.0.183) metasploit_data_models (6.0.3) activerecord (~> 7.0) activesupport (~> 7.0) @@ -306,7 +306,7 @@ GEM railties (~> 7.0) recog webrick - metasploit_payloads-mettle (1.0.31) + metasploit_payloads-mettle (1.0.32) method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) diff --git a/LICENSE_GEMS b/LICENSE_GEMS index 63ab30283a..703c6c2ed6 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -88,9 +88,9 @@ metasploit-concern, 5.0.2, "New BSD" metasploit-credential, 6.0.9, "New BSD" metasploit-framework, 6.4.31, "New BSD" metasploit-model, 5.0.2, "New BSD" -metasploit-payloads, 2.0.175, "3-clause (or ""modified"") BSD" +metasploit-payloads, 2.0.183, "3-clause (or ""modified"") BSD" metasploit_data_models, 6.0.3, "New BSD" -metasploit_payloads-mettle, 1.0.31, "3-clause (or ""modified"") BSD" +metasploit_payloads-mettle, 1.0.32, "3-clause (or ""modified"") BSD" method_source, 1.1.0, MIT mime-types, 3.5.2, MIT mime-types-data, 3.2024.0604, MIT diff --git a/lib/msf/base/sessions/command_shell.rb b/lib/msf/base/sessions/command_shell.rb index c606e85ace..bb9b04472a 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -735,6 +735,49 @@ Shell Banner: end end + # Perform command line escaping wherein most chars are able to be escaped by quoting them, + # but others don't have a valid way of existing inside quotes, so we need to "glue" together + # a series of sections of the original command line; some sections inside quotes, and some outside + # @param arg [String] The command line arg to escape + # @param quote_requiring [Array] The chars that can successfully be escaped inside quotes + # @param unquotable_char [String] The character that can't exist inside quotes + # @param escaped_unquotable_char [String] The escaped form of unquotable_char + # @param quote_char [String] The char used for quoting + def self._glue_cmdline_escape(arg, quote_requiring, unquotable_char, escaped_unquotable_char, quote_char) + current_token = "" + result = "" + in_quotes = false + + arg.each_char do |char| + if char == unquotable_char + if in_quotes + # This token has been in an inside-quote context, so let's properly wrap that before continuing + current_token = "#{quote_char}#{current_token}#{quote_char}" + end + result += current_token + result += escaped_unquotable_char # Escape the offending percent + + # Start a new token - we'll assume we're remaining outside quotes + current_token = '' + in_quotes = false + next + elsif quote_requiring.include?(char) + # Oh, it turns out we should have been inside quotes for this token. + # Let's note that, for when we actually append the token + in_quotes = true + end + current_token += char + end + + if in_quotes + # The final token has been in an inside-quote context, so let's properly wrap that before continuing + current_token = "#{quote_char}#{current_token}#{quote_char}" + end + result += current_token + + result + end + attr_accessor :arch attr_accessor :platform attr_accessor :max_threads diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index c3bd2dd135..16a999357f 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -5,9 +5,44 @@ module Msf::Sessions self.platform = "unix" super end + def shell_command_token(cmd,timeout = 10) shell_command_token_unix(cmd,timeout) end + + # Convert the executable and argument array to a command that can be run in this command shell + # @param cmd_and_args [Array] The process path and the arguments to the process + def to_cmd(cmd_and_args) + self.class.to_cmd(cmd_and_args) + end + + # Escape an individual argument per Unix shell rules + # @param arg [String] Shell argument + def escape_arg(arg) + self.class.escape_arg(arg) + end + + # Convert the executable and argument array to a command that can be run in this command shell + # @param cmd_and_args [Array] The process path and the arguments to the process + def self.to_cmd(cmd_and_args) + escaped = cmd_and_args.map do |arg| + escape_arg(arg) + end + + escaped.join(' ') + end + + # Escape an individual argument per Unix shell rules + # @param arg [String] Shell argument + def self.escape_arg(arg) + quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';'] + result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'") + if result == '' + result = "''" + end + + result + end end end diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index b98667a758..a4397f96e4 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -1,4 +1,3 @@ - module Msf::Sessions class CommandShellWindows < CommandShell @@ -6,9 +5,115 @@ module Msf::Sessions self.platform = "windows" super end + + def self.space_chars + [' ', '\t', '\v'] + end + def shell_command_token(cmd,timeout = 10) shell_command_token_win32(cmd,timeout) end + + # Convert the executable and argument array to a command that can be run in this command shell + # @param cmd_and_args [Array] The process path and the arguments to the process + def to_cmd(cmd_and_args) + self.class.to_cmd(cmd_and_args) + end + + # Escape a process for the command line + # @param executable [String] The process to launch + def self.escape_cmd(executable) + needs_quoting = space_chars.any? do |char| + executable.include?(char) + end + + if needs_quoting + executable = "\"#{executable}\"" + end + + executable + end + + # Convert the executable and argument array to a commandline that can be passed to CreateProcessAsUserW. + # @param args [Array] The arguments to the process + # @remark The difference between this and `to_cmd` is that the output of `to_cmd` is expected to be passed + # to cmd.exe, whereas this is expected to be passed directly to the Win32 API, anticipating that it + # will in turn be interpreted by CommandLineToArgvW. + def self.argv_to_commandline(args) + escaped_args = args.map do |arg| + escape_arg(arg) + end + + escaped_args.join(' ') + end + + # Escape an individual argument per Windows shell rules + # @param arg [String] Shell argument + def self.escape_arg(arg) + needs_quoting = space_chars.any? do |char| + arg.include?(char) + end + + # Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote + # We need to send double the number of backslashes to make it work as expected + # See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks + arg = arg.gsub(/(\\*)"/, '\\1\\1"') + + # Quotes need to be escaped + arg = arg.gsub('"', '\\"') + + if needs_quoting + # At the end of the argument, we're about to add another quote - so any backslashes need to be doubled here too + arg = arg.gsub(/(\\*)$/, '\\1\\1') + arg = "\"#{arg}\"" + end + + # Empty string needs to be coerced to have a value + arg = '""' if arg == '' + + arg + end + + # Convert the executable and argument array to a command that can be run in this command shell + # @param cmd_and_args [Array] The process path and the arguments to the process + def self.to_cmd(cmd_and_args) + # The space, caret and quote chars need to be inside double-quoted strings. + # The percent character needs to be escaped using a caret char, while being outside a double-quoted string. + # + # Situations where these two situations combine are going to be the trickiest cases: something that has quote-requiring + # characters (e.g. spaces), but which also needs to avoid expanding an environment variable. In this case, + # the string needs to end up being partially quoted; with parts of the string in quotes, but others (i.e. bits with percents) not. + # For example: + # 'env var is %temp%, yes, %TEMP%' needs to end up as '"env var is "^%temp^%", yes, "^%TEMP^%' + # + # There is flexibility in how you might implement this, but I think this one looks the most "human" to me, + # which would make it less signaturable. + # + # To do this, we'll consider each argument character-by-character. Each time we encounter a percent sign, we break out of any quotes + # (if we've been inside them in the current "token"), and then start a new "token". + + quote_requiring = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|'] + + escaped_cmd_and_args = cmd_and_args.map do |arg| + # Escape quote chars by doubling them up, except those preceeded by a backslash (which are already effectively escaped, and handled below) + arg = arg.gsub(/([^\\])"/, '\\1""') + arg = arg.gsub(/^"/, '""') + + result = CommandShell._glue_cmdline_escape(arg, quote_requiring, '%', '^%', '"') + + # Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote + # We need to send double the number of backslashes to make it work as expected + # See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks + result.gsub!(/(\\*)"/, '\\1\\1"') + + # Empty string needs to be coerced to have a value + result = '""' if result == '' + + result + end + + escaped_cmd_and_args.join(' ') + end end end diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index cbf3bf08f0..3bde436f5c 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -38,6 +38,91 @@ class Msf::Sessions::PowerShell < Msf::Sessions::CommandShell include Mixin + # Convert the executable and argument array to a command that can be run in this command shell + # @param cmd_and_args [Array] The process path and the arguments to the process + def to_cmd(cmd_and_args) + self.class.to_cmd(cmd_and_args) + end + + # Convert the executable and argument array to a command that can be run in this command shell + # @param cmd_and_args [Array] The process path and the arguments to the process + def self.to_cmd(cmd_and_args) + # The principle here is that we want to launch a process such that it receives *exactly* what is in `args`. + # This means we need to: + # - Escape all special characters + # - Not escape environment variables + # - Side-step any PowerShell magic + # If someone specifically wants to use the PowerShell magic, they can use other APIs + + needs_wrapping_chars = ['$', '`', '(', ')', '@', '>', '<', '{','}', '&', ',', ' ', ';'] + + result = "" + cmd_and_args.each_with_index do |arg, index| + needs_single_quoting = false + if arg.include?("'") + arg = arg.gsub("'", "''") + needs_single_quoting = true + end + + if arg.include?('"') + # PowerShell acts weird around quotes and backslashes + # First we need to escape backslashes immediately prior to a double-quote, because + # they're treated differently than backslashes anywhere else + arg = arg.gsub(/(\\+)"/, '\\1\\1"') + + # Then we can safely prepend a backslash to escape our double-quote + arg = arg.gsub('"', '\\"') + needs_single_quoting = true + end + + needs_wrapping_chars.each do |char| + if arg.include?(char) + needs_single_quoting = true + end + end + + # PowerShell magic - https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters?view=powershell-7.4#stop-parsing-token--- + if arg == '--%' + needs_single_quoting = true + end + + will_be_double_quoted_by_powershell = [' ', '\t', '\v'].any? do |bad_char| + arg.include?(bad_char) + end + + if will_be_double_quoted_by_powershell + # This is horrible, and I'm so so sorry. + # If an argument ends with a series of backslashes, and it will be quoted by PowerShell when *it* launches the process (e.g. because the arg contains a space), + # PowerShell will not correctly handle backslashes immediately preceeding the quote that it *itself* adds. So we need to be responsible for this. + arg = arg.gsub(/(\\*)$/, '\\1\\1') + end + + if needs_single_quoting + arg = "'#{arg}'" + end + + if arg == '' + # Pass in empty strings + arg = '\'""\'' + end + + if index == 0 + if needs_single_quoting + # If the executable name (i.e. index 0) has beeen wrapped, then we'll have converted it to a string. + # We then need to use the call operator ('&') to call it. + # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-7.3#call-operator- + result = "& #{arg}" + else + result = arg + end + else + result = "#{result} #{arg}" + end + end + + result + end + # # Execute any specified auto-run scripts for this session # diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index cc7639ede0..dee213d649 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -52,6 +52,71 @@ module Msf::Post::Common "#{rhost}:#{rport}" end + # Create a new process, receiving the program's output + # @param executable [String] The path to the executable; either absolute or relative to the session's current directory + # @param args [Array] The arguments to the executable + # @time_out [Integer] Number of seconds before the call will time out + # @param opts [Hash] Optional settings to parameterise the process launch + # @option Hidden [Boolean] Is the process launched without creating a visible window + # @option Channelized [Boolean] The process is launched with pipes connected to a channel, e.g. for sending input/receiving output + # @option Suspended [Boolean] Start the process suspended + # @option UseThreadToken [Boolean] Use the thread token (as opposed to the process token) to launch the process + # @option Desktop [Boolean] Run on meterpreter's current desktop + # @option Session [Integer] Execute process in a given session as the session user + # @option Subshell [Boolean] Execute process in a subshell + # @option Pty [Boolean] Execute process in a pty (if available) + # @option ParentId [Integer] Spoof the parent PID (if possible) + # @option InMemory [Boolean,String] Execute from memory (`path` is treated as a local file to upload, and the actual path passed + # to meterpreter is this parameter's value, if provided as a String) + def create_process(executable, args: [], time_out: 15, opts: {}) + case session.type + when 'meterpreter' + session.response_timeout = time_out + opts = { + 'Hidden' => true, + 'Channelized' => true, + # Well-behaving meterpreters will ignore the Subshell flag when using arg arrays. + # This is still provided for supporting old meterpreters. + 'Subshell' => true + }.merge(opts) + + if session.platform == 'windows' + if session.arch == 'php' + opts[:legacy_args] = Msf::Sessions::CommandShellWindows.to_cmd(args) + opts[:legacy_path] = Msf::Sessions::CommandShellWindows.to_cmd([executable]) + elsif session.arch == 'python' + opts[:legacy_path] = executable + # Yes, Unix. Old Python meterp had a bug where it used posix shell splitting + # syntax even on Windows. For backwards-compatibility, we can trick it into + # doing the right thing by using Unix escaping. + opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(args) + else + opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(args) + opts[:legacy_path] = Msf::Sessions::CommandShellWindows.escape_cmd(executable) + end + else + opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(args) + opts[:legacy_path] = Msf::Sessions::CommandShellUnix.to_cmd([executable]) + end + + if opts['Channelized'] + o = session.sys.process.capture_output(executable, args, opts, time_out) + else + session.sys.process.execute(executable, args, opts) + end + when 'powershell' + cmd = session.to_cmd([executable] + args) + o = session.shell_command(cmd, time_out) + o.chomp! if o + when 'shell' + cmd = session.to_cmd([executable] + args) + o = session.shell_command_token(cmd, time_out) + o.chomp! if o + end + return "" if o.nil? + return o + end + # # Executes +cmd+ on the remote system # diff --git a/lib/msf/core/post/file.rb b/lib/msf/core/post/file.rb index 4ae5abdf46..a93711f0e2 100644 --- a/lib/msf/core/post/file.rb +++ b/lib/msf/core/post/file.rb @@ -580,7 +580,7 @@ module Msf::Post::File if session.type == 'meterpreter' && session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_FS_CHMOD) session.fs.file.chmod(path, mode) else - cmd_exec("chmod #{mode.to_s(8)} '#{path}'") + create_process('chmod', args: [mode.to_s(8), path]) end end @@ -740,6 +740,7 @@ module Msf::Post::File else file_mode = 'Create' end + file_name = file_name.gsub("'","''") pwsh_code = <<~PSH try { $encoded='#{encoded_chunk}'; @@ -911,7 +912,7 @@ protected success = _win_ansi_write_file(b64_filename, b64_data, chunk_size) return false unless success vprint_status("Uploaded Base64-encoded file. Decoding using certutil") - success = _shell_command_with_success_code("certutil -f -decode #{b64_filename} #{file_name}") + success = _shell_process_with_success_code('certutil', ['-f', '-decode', b64_filename, file_name]) return false unless success rescue ::Exception => e print_error("Exception while running #{__method__}: #{e}") @@ -937,10 +938,10 @@ protected success = _win_ansi_write_file(b64_filename, b64_data, chunk_size) return false unless success vprint_status("Uploaded Base64-encoded file. Decoding using certutil") - success = _shell_command_with_success_code("certutil -decode #{b64_filename} #{tmp_filename}") + success = _shell_process_with_success_code('certutil', ['-decode', b64_filename, tmp_filename]) return false unless success vprint_status("Certutil succeeded. Appending using copy") - success = _shell_command_with_success_code("copy /b #{file_name}+#{tmp_filename} #{file_name}") + success = _shell_process_with_success_code('copy', ['/b', "#{file_name}+#{tmp_filename}", file_name]) return false unless success rescue ::Exception => e print_error("Exception while running #{__method__}: #{e}") @@ -980,7 +981,7 @@ protected # Short-circuit an empty string. The : builtin is part of posix # standard and should theoretically exist everywhere. if data.empty? - return _shell_command_with_success_code(": #{redirect} #{file_name}") + return _shell_command_with_success_code(": #{redirect} #{session.escape_arg(file_name)}") end d = data.dup @@ -1081,7 +1082,7 @@ protected # The first command needs to use the provided redirection for either # appending or truncating. cmd = command.sub('CONTENTS') { chunks.shift } - succeeded = _shell_command_with_success_code("#{cmd} #{redirect} \"#{file_name}\"") + succeeded = _shell_command_with_success_code("#{cmd} #{redirect} #{session.escape_arg(file_name)}") return false unless succeeded # After creating/truncating or appending with the first command, we @@ -1090,7 +1091,7 @@ protected vprint_status("Next chunk is #{chunk.length} bytes") cmd = command.sub('CONTENTS') { chunk } - succeeded = _shell_command_with_success_code("#{cmd} >> '#{file_name}'") + succeeded = _shell_command_with_success_code("#{cmd} >> #{session.escape_arg(file_name)}") unless succeeded print_warning("Write partially succeeded then failed. May need to manually clean up #{file_name}") return false @@ -1107,6 +1108,15 @@ protected return result&.include?(token) end + def _shell_process_with_success_code(executable, args) + cmd = session.to_cmd([executable] + args) + token = "_#{::Rex::Text.rand_text_alpha(32)}" + result = session.shell_command_token("#{cmd} && echo #{token}") + + return result&.include?(token) + end + + # # Calculate the maximum line length for a unix shell. # diff --git a/lib/msf/core/session/provider/single_command_shell.rb b/lib/msf/core/session/provider/single_command_shell.rb index 4a166e48e2..9781f8a10f 100644 --- a/lib/msf/core/session/provider/single_command_shell.rb +++ b/lib/msf/core/session/provider/single_command_shell.rb @@ -93,11 +93,11 @@ module SingleCommandShell output end - def to_cmd(cmd, args) + def to_cmd(cmd_and_args) if platform == 'windows' - result = Msf::Sessions::CommandShellWindows.to_cmd(cmd, args) + result = Msf::Sessions::CommandShellWindows.to_cmd(cmd_and_args) else - result = Msf::Sessions::CommandShellUnix.to_cmd(cmd, args) + result = Msf::Sessions::CommandShellUnix.to_cmd(cmd_and_args) end end diff --git a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb index 85477d5f14..4c52036533 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb @@ -107,15 +107,24 @@ class Process < Rex::Post::Process # # Executes an application using the arguments provided + # @param path [String] Path on the remote system to the executable to run + # @param arguments [String,Array] Arguments to the process. When passed as a String (rather than an array of Strings), + # this is treated as a string containing all arguments. + # @param opts [Hash] Optional settings to parameterise the process launch + # @option Hidden [Boolean] Is the process launched without creating a visible window + # @option Channelized [Boolean] The process is launched with pipes connected to a channel, e.g. for sending input/receiving output + # @option Suspended [Boolean] Start the process suspended + # @option UseThreadToken [Boolean] Use the thread token (as opposed to the process token) to launch the process + # @option Desktop [Boolean] Run on meterpreter's current desktopt + # @option Session [Integer] Execute process in a given session as the session user + # @option Subshell [Boolean] Execute process in a subshell + # @option Pty [Boolean] Execute process in a pty (if available) + # @option ParentId [Integer] Spoof the parent PID (if possible) + # @option InMemory [Boolean,String] Execute from memory (`path` is treated as a local file to upload, and the actual path passed + # to meterpreter is this parameter's value, if provided as a String) + # @option :legacy_args [String] When arguments is an array, this is the command to execute if the receiving Meterpreter does not support arguments as an array # - # Hash arguments supported: - # - # Hidden => true/false - # Channelized => true/false - # Suspended => true/false - # InMemory => true/false - # - def Process.execute(path, arguments = nil, opts = nil) + def Process.execute(path, arguments = '', opts = nil) request = Packet.create_request(COMMAND_ID_STDAPI_SYS_PROCESS_EXECUTE) flags = 0 @@ -164,11 +173,26 @@ class Process < Rex::Post::Process end end - request.add_tlv(TLV_TYPE_PROCESS_PATH, client.unicode_filter_decode( path )); - + # Add arguments # If process arguments were supplied - if (arguments != nil) - request.add_tlv(TLV_TYPE_PROCESS_ARGUMENTS, arguments); + if arguments.kind_of?(Array) + request.add_tlv(TLV_TYPE_PROCESS_UNESCAPED_PATH, client.unicode_filter_decode( path )); + # This flag is needed to disambiguate how to handle escaping special characters in the path when no arguments are provided + flags |= PROCESS_EXECUTE_FLAG_ARG_ARRAY + arguments.each do |arg| + request.add_tlv(TLV_TYPE_PROCESS_ARGUMENT, arg); + end + if opts[:legacy_path] + request.add_tlv(TLV_TYPE_PROCESS_PATH, opts[:legacy_path]) + end + if opts[:legacy_args] + request.add_tlv(TLV_TYPE_PROCESS_ARGUMENTS, opts[:legacy_args]) + end + elsif arguments.kind_of?(String) + request.add_tlv(TLV_TYPE_PROCESS_PATH, client.unicode_filter_decode( path )); + request.add_tlv(TLV_TYPE_PROCESS_ARGUMENTS, arguments) + else + raise ArgumentError.new('Unknown type for arguments') end request.add_tlv(TLV_TYPE_PROCESS_FLAGS, flags); @@ -194,7 +218,7 @@ class Process < Rex::Post::Process # # Execute an application and capture the output # - def Process.capture_output(path, arguments = nil, opts = nil, time_out = 15) + def Process.capture_output(path, arguments = '', opts = nil, time_out = 15) start = Time.now.to_i process = execute(path, arguments, opts) data = "" diff --git a/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb b/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb index 924838a4af..b6495bb675 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb @@ -119,6 +119,7 @@ PROCESS_EXECUTE_FLAG_DESKTOP = (1 << 4) PROCESS_EXECUTE_FLAG_SESSION = (1 << 5) PROCESS_EXECUTE_FLAG_SUBSHELL = (1 << 6) PROCESS_EXECUTE_FLAG_PTY = (1 << 7) +PROCESS_EXECUTE_FLAG_ARG_ARRAY = (1 << 8) # Registry TLV_TYPE_HKEY = TLV_META_TYPE_QWORD | 1000 @@ -151,25 +152,27 @@ TLV_TYPE_ENV_GROUP = TLV_META_TYPE_GROUP | 1102 DELETE_KEY_FLAG_RECURSIVE = (1 << 0) # Process -TLV_TYPE_BASE_ADDRESS = TLV_META_TYPE_QWORD | 2000 -TLV_TYPE_ALLOCATION_TYPE = TLV_META_TYPE_UINT | 2001 -TLV_TYPE_PROTECTION = TLV_META_TYPE_UINT | 2002 -TLV_TYPE_PROCESS_PERMS = TLV_META_TYPE_UINT | 2003 -TLV_TYPE_PROCESS_MEMORY = TLV_META_TYPE_RAW | 2004 -TLV_TYPE_ALLOC_BASE_ADDRESS = TLV_META_TYPE_QWORD | 2005 -TLV_TYPE_MEMORY_STATE = TLV_META_TYPE_UINT | 2006 -TLV_TYPE_MEMORY_TYPE = TLV_META_TYPE_UINT | 2007 -TLV_TYPE_ALLOC_PROTECTION = TLV_META_TYPE_UINT | 2008 -TLV_TYPE_PID = TLV_META_TYPE_UINT | 2300 -TLV_TYPE_PROCESS_NAME = TLV_META_TYPE_STRING | 2301 -TLV_TYPE_PROCESS_PATH = TLV_META_TYPE_STRING | 2302 -TLV_TYPE_PROCESS_GROUP = TLV_META_TYPE_GROUP | 2303 -TLV_TYPE_PROCESS_FLAGS = TLV_META_TYPE_UINT | 2304 -TLV_TYPE_PROCESS_ARGUMENTS = TLV_META_TYPE_STRING | 2305 -TLV_TYPE_PROCESS_ARCH = TLV_META_TYPE_UINT | 2306 -TLV_TYPE_PARENT_PID = TLV_META_TYPE_UINT | 2307 -TLV_TYPE_PROCESS_SESSION = TLV_META_TYPE_UINT | 2308 -TLV_TYPE_PROCESS_ARCH_NAME = TLV_META_TYPE_STRING | 2309 +TLV_TYPE_BASE_ADDRESS = TLV_META_TYPE_QWORD | 2000 +TLV_TYPE_ALLOCATION_TYPE = TLV_META_TYPE_UINT | 2001 +TLV_TYPE_PROTECTION = TLV_META_TYPE_UINT | 2002 +TLV_TYPE_PROCESS_PERMS = TLV_META_TYPE_UINT | 2003 +TLV_TYPE_PROCESS_MEMORY = TLV_META_TYPE_RAW | 2004 +TLV_TYPE_ALLOC_BASE_ADDRESS = TLV_META_TYPE_QWORD | 2005 +TLV_TYPE_MEMORY_STATE = TLV_META_TYPE_UINT | 2006 +TLV_TYPE_MEMORY_TYPE = TLV_META_TYPE_UINT | 2007 +TLV_TYPE_ALLOC_PROTECTION = TLV_META_TYPE_UINT | 2008 +TLV_TYPE_PID = TLV_META_TYPE_UINT | 2300 +TLV_TYPE_PROCESS_NAME = TLV_META_TYPE_STRING | 2301 +TLV_TYPE_PROCESS_PATH = TLV_META_TYPE_STRING | 2302 +TLV_TYPE_PROCESS_GROUP = TLV_META_TYPE_GROUP | 2303 +TLV_TYPE_PROCESS_FLAGS = TLV_META_TYPE_UINT | 2304 +TLV_TYPE_PROCESS_ARGUMENTS = TLV_META_TYPE_STRING | 2305 +TLV_TYPE_PROCESS_ARCH = TLV_META_TYPE_UINT | 2306 +TLV_TYPE_PARENT_PID = TLV_META_TYPE_UINT | 2307 +TLV_TYPE_PROCESS_SESSION = TLV_META_TYPE_UINT | 2308 +TLV_TYPE_PROCESS_ARCH_NAME = TLV_META_TYPE_STRING | 2309 +TLV_TYPE_PROCESS_ARGUMENT = TLV_META_TYPE_STRING | 2310 +TLV_TYPE_PROCESS_UNESCAPED_PATH = TLV_META_TYPE_STRING | 2311 TLV_TYPE_DRIVER_ENTRY = TLV_META_TYPE_GROUP | 2320 TLV_TYPE_DRIVER_BASENAME = TLV_META_TYPE_STRING | 2321 diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index 2c8807bd8a..4a98d06f31 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -74,9 +74,9 @@ Gem::Specification.new do |spec| # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' # Needed for Meterpreter - spec.add_runtime_dependency 'metasploit-payloads', '2.0.175' + spec.add_runtime_dependency 'metasploit-payloads', '2.0.183' # Needed for the next-generation POSIX Meterpreter - spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.31' + spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.32' # Needed by msfgui and other rpc components # Locked until build env can handle newer version. See: https://github.com/msgpack/msgpack-ruby/issues/334 spec.add_runtime_dependency 'msgpack', '~> 1.6.0' diff --git a/modules/payloads/singles/cmd/unix/reverse_bash.rb b/modules/payloads/singles/cmd/unix/reverse_bash.rb index bb52610354..629ce1d7e4 100644 --- a/modules/payloads/singles/cmd/unix/reverse_bash.rb +++ b/modules/payloads/singles/cmd/unix/reverse_bash.rb @@ -26,7 +26,7 @@ module MetasploitModule 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Handler' => Msf::Handler::ReverseTcp, - 'Session' => Msf::Sessions::CommandShell, + 'Session' => Msf::Sessions::CommandShellUnix, 'PayloadType' => 'cmd_bash', 'RequiredCmd' => 'bash-tcp', 'Payload' => diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb index 433e65f6c5..0dc97e89ba 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061712 + CachedSize = 1061912 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb index eac837ca42..4c9590fe1a 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061712 + CachedSize = 1061912 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb index 82b9139436..f0d1bf1f7d 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061712 + CachedSize = 1061912 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb index 3e47e4126c..574c89da43 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061884 + CachedSize = 1062084 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb index 260dfc4a3a..95bc3d92c6 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061884 + CachedSize = 1062084 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb index 847692dba1..8ed41f2c63 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061884 + CachedSize = 1062084 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb index 133f739b72..7b9b1fe8ac 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1516268 + CachedSize = 1516524 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb index 473b904852..0904600aad 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1516268 + CachedSize = 1516524 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb index 59182fe523..f25c282a9f 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1516268 + CachedSize = 1516524 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb index 3fce57d345..0297a16cae 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1519288 + CachedSize = 1519544 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb index f897a77ab2..358e856c54 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1519288 + CachedSize = 1519544 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb index f7adc9d808..3536333417 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1519288 + CachedSize = 1519544 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_http.rb b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_http.rb index b8b74b349c..30693a1ea2 100644 --- a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_http.rb @@ -5,7 +5,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 813091 + CachedSize = 813075 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_https.rb b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_https.rb index cc7d9e3338..b6e2662255 100644 --- a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_https.rb @@ -5,7 +5,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 813091 + CachedSize = 813075 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb index bffb692091..9f1737fa74 100644 --- a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb @@ -5,7 +5,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 813091 + CachedSize = 813075 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb index bfd845d4c8..9604781ea0 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ module MetasploitModule - CachedSize = 34854 + CachedSize = 34928 include Msf::Payload::Single include Msf::Payload::Php::ReverseTcp diff --git a/modules/post/multi/general/execute.rb b/modules/post/multi/general/execute.rb index 417d08703c..76abe4ffad 100644 --- a/modules/post/multi/general/execute.rb +++ b/modules/post/multi/general/execute.rb @@ -27,6 +27,6 @@ class MetasploitModule < Msf::Post def run print_status("Executing #{datastore['COMMAND']} on #{session.inspect}...") res = cmd_exec(datastore['COMMAND']) - print_status("Response: #{res}") + print_status("Response: \n#{res}") end end diff --git a/spec/lib/msf/base/sessions/command_shell_unix_spec.rb b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb new file mode 100755 index 0000000000..b0d7c369e6 --- /dev/null +++ b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb @@ -0,0 +1,39 @@ +RSpec.describe Msf::Sessions::CommandShellUnix do + describe 'to_cmd processing' do + it 'should not do anything for simple args' do + expect(described_class.to_cmd(['./test'] + [])).to eq('./test') + expect(described_class.to_cmd(['sh'] + [])).to eq('sh') + expect(described_class.to_cmd(['./test'] + ['basic','args'])).to eq('./test basic args') + expect(described_class.to_cmd(['basic','args'])).to eq('basic args') + end + + it 'should escape spaces' do + expect(described_class.to_cmd(['/home/user/some folder/some program'] + [])).to eq("'/home/user/some folder/some program'") + expect(described_class.to_cmd(['./test'] + ['with space'])).to eq("./test 'with space'") + end + + it 'should escape logical operators' do + expect(described_class.to_cmd(['./test'] + ['&&', 'echo', 'words'])).to eq("./test '&&' echo words") + expect(described_class.to_cmd(['./test'] + ['||', 'echo', 'words'])).to eq("./test '||' echo words") + expect(described_class.to_cmd(['./test'] + ['&echo', 'words'])).to eq("./test '&echo' words") + expect(described_class.to_cmd(['./test'] + ['run&echo', 'words'])).to eq("./test 'run&echo' words") + end + + it 'should quote if single quotes are present' do + expect(described_class.to_cmd(['./test'] + ["it's"])).to eq("./test it\\'s") + expect(described_class.to_cmd(['./test'] + ["it's a param"])).to eq("./test it\\''s a param'") + end + + it 'should escape redirectors' do + expect(described_class.to_cmd(['./test'] + ['>', 'out.txt'])).to eq("./test '>' out.txt") + expect(described_class.to_cmd(['./test'] + ['<', 'in.txt'])).to eq("./test '<' in.txt") + end + + it 'should not expand env vars' do + expect(described_class.to_cmd(['./test'] + ['$PATH'])).to eq("./test '$PATH'") + expect(described_class.to_cmd(['./test'] + ["it's $PATH"])).to eq("./test it\\''s $PATH'") + expect(described_class.to_cmd(['./test'] + ["\"$PATH\""])).to eq("./test '\"$PATH\"'") + expect(described_class.to_cmd(['./test'] + ["it's \"$PATH\""])).to eq("./test it\\''s \"$PATH\"'") + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/base/sessions/command_shell_windows_spec.rb b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb new file mode 100755 index 0000000000..917e32253a --- /dev/null +++ b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb @@ -0,0 +1,88 @@ +RSpec.describe Msf::Sessions::CommandShellWindows do + + describe 'to_cmd processing' do + it 'should not do anything for simple args' do + expect(described_class.to_cmd(['test.exe'] + [])).to eq('test.exe') + expect(described_class.to_cmd(['test.exe'] + ['basic','args'])).to eq('test.exe basic args') + end + + it 'should quote spaces' do + expect(described_class.to_cmd(['C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE'] + [])).to eq('"C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE"') + expect(described_class.to_cmd(['test.exe'] + ['with space'])).to eq('test.exe "with space"') + end + + it 'should escape logical operators' do + expect(described_class.to_cmd(['test.exe'] + ['&&', 'echo', 'words'])).to eq('test.exe "&&" echo words') + expect(described_class.to_cmd(['test.exe'] + ['||', 'echo', 'words'])).to eq('test.exe "||" echo words') + expect(described_class.to_cmd(['test.exe'] + ['&echo', 'words'])).to eq('test.exe "&echo" words') + expect(described_class.to_cmd(['test.exe'] + ['run&echo', 'words'])).to eq('test.exe "run&echo" words') + end + + it 'should escape redirectors' do + expect(described_class.to_cmd(['test.exe'] + ['>', 'out.txt'])).to eq('test.exe ">" out.txt') + expect(described_class.to_cmd(['test.exe'] + ['<', 'in.txt'])).to eq('test.exe "<" in.txt') + end + + it 'should escape carets' do + expect(described_class.to_cmd(['test.exe'] + ['with^caret'])).to eq('test.exe "with^caret"') + expect(described_class.to_cmd(['test.exe'] + ['with^^carets'])).to eq('test.exe "with^^carets"') + end + + it 'should not expand env vars' do + expect(described_class.to_cmd(['test.exe'] + ['%temp%'])).to eq('test.exe ^%temp^%') + expect(described_class.to_cmd(['test.exe'] + ['env', 'var', 'is', '%temp%'])).to eq('test.exe env var is ^%temp^%') + end + + it 'should handle the weird backslash escaping behaviour in front of quotes' do + expect(described_class.to_cmd(['test.exe'] + ['quote\\\\"'])).to eq('test.exe "quote\\\\\\\\""') + expect(described_class.to_cmd(['test.exe'] + ['will be quoted\\\\'])).to eq('test.exe "will be quoted\\\\\\\\"') + expect(described_class.to_cmd(['test.exe'] + ['will be quoted\\\\ '])).to eq('test.exe "will be quoted\\\\ "') # Should not be doubled up + expect(described_class.to_cmd(['test.exe'] + ['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\'])).to eq('test.exe """test""" "test\\\\"" "test\\\\\\\\"" "test words\\\\\\\\\\\\\\\\" "test words\\\\\\\\\\\\" \\\\') + end + + it 'should handle combinations of quoting and percent-escaping' do + expect(described_class.to_cmd(['test.exe'] + ['env var is %temp%'])).to eq('test.exe "env var is "^%temp^%') + expect(described_class.to_cmd(['test.exe'] + ['env var is %temp%, yes, %TEMP%'])).to eq('test.exe "env var is "^%temp^%", yes, "^%TEMP^%') + expect(described_class.to_cmd(['test.exe'] + ['%temp%found at the start shouldn\'t %temp% be quoted'])).to eq('test.exe ^%temp^%"found at the start shouldn\'t "^%temp^%" be quoted"') + end + + it 'should handle single percents' do + expect(described_class.to_cmd(['test.exe'] + ['%single percent'])).to eq('test.exe ^%"single percent"') + expect(described_class.to_cmd(['test.exe'] + ['100%'])).to eq('test.exe 100^%') + end + + it 'should handle empty args' do + expect(described_class.to_cmd(['test.exe'] + [''])).to eq('test.exe ""') + expect(described_class.to_cmd(['test.exe'] + ['', ''])).to eq('test.exe "" ""') + end + end + + describe 'argv_to_commandline processing' do + it 'should not do anything for simple args' do + expect(described_class.argv_to_commandline([])).to eq('') + expect(described_class.argv_to_commandline(['basic','args'])).to eq('basic args') + expect(described_class.argv_to_commandline(['!@#$%^&*(){}><.,\''])).to eq('!@#$%^&*(){}><.,\'') + end + + it 'should quote space characters' do + expect(described_class.argv_to_commandline([])).to eq('') + expect(described_class.argv_to_commandline(['basic','args'])).to eq('basic args') + end + + it 'should escape double-quote characters' do + expect(described_class.argv_to_commandline(['"one','"two"'])).to eq('\\"one \\"two\\"') + expect(described_class.argv_to_commandline(['"one "two"'])).to eq('"\\"one \\"two\\""') + end + + it 'should handle the weird backslash escaping behaviour in front of quotes' do + expect(described_class.argv_to_commandline(['\\\\"'])).to eq('\\\\\\\\\\"') + expect(described_class.argv_to_commandline(['space \\\\'])).to eq('"space \\\\\\\\"') + expect(described_class.argv_to_commandline(['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\'])).to eq('\"test\" test\\\\\\" test\\\\\\\\\\" "test words\\\\\\\\\\\\\\\\" "test words\\\\\\\\\\\\" \\\\') + end + + it 'should handle empty args' do + expect(described_class.argv_to_commandline([''])).to eq('""') + expect(described_class.argv_to_commandline(['', ''])).to eq('"" ""') + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/base/sessions/powershell_spec.rb b/spec/lib/msf/base/sessions/powershell_spec.rb new file mode 100755 index 0000000000..65ba82578c --- /dev/null +++ b/spec/lib/msf/base/sessions/powershell_spec.rb @@ -0,0 +1,52 @@ +RSpec.describe Msf::Sessions::PowerShell do + describe 'to_cmd processing' do + it 'should not do anything for simple args' do + expect(described_class.to_cmd([".\\test.exe"] + ['abc', '123'])).to eq(".\\test.exe abc 123") + expect(described_class.to_cmd(["C:\\SysinternalsSuite\\procexp.exe"] + [])).to eq("C:\\SysinternalsSuite\\procexp.exe") + end + + it 'should double single-quotes' do + expect(described_class.to_cmd([".\\test.exe"] + ["'abc'"])).to eq(".\\test.exe '''abc'''") + end + + it 'should escape less than' do + expect(described_class.to_cmd([".\\test.exe"] + ["'abc'", '>', 'out.txt'])).to eq(".\\test.exe '''abc''' '>' out.txt") + end + + it 'should escape other special chars' do + expect(described_class.to_cmd([".\\test.exe"] + ["'abc'", '<', '(', ')', '$test', '`words`', 'abc,def'])).to eq(".\\test.exe '''abc''' '<' '(' ')' '$test' '`words`' 'abc,def'") + end + + it 'should backslash escape double-quotes' do + expect(described_class.to_cmd([".\\test.exe"] + ['"abc'])).to eq(".\\test.exe '\\\"abc'") + end + + it 'should correctly backslash escape backslashes and double-quotes' do + expect(described_class.to_cmd([".\\test.exe"] + ['\\"abc'])).to eq(".\\test.exe '\\\\\\\"abc'") + expect(described_class.to_cmd([".\\test.exe"] + ['\\\\"abc'])).to eq(".\\test.exe '\\\\\\\\\\\"abc'") + expect(described_class.to_cmd([".\\test.exe"] + ['\\\\"ab\\\\c'])).to eq(".\\test.exe '\\\\\\\\\\\"ab\\\\c'") + end + + it 'should quote the executable and add the call operator' do + expect(described_class.to_cmd([".\\test$.exe"] + ['abc'])).to eq("& '.\\test$.exe' abc") + expect(described_class.to_cmd([".\\test'.exe"] + ['abc'])).to eq("& '.\\test''.exe' abc") + expect(described_class.to_cmd(["C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE"] + [])).to eq("& 'C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE'") + end + + it 'should not expand environment variables' do + expect(described_class.to_cmd([".\\test.exe"] + ['$env:path'])).to eq(".\\test.exe '$env:path'") + end + + it 'should not respect PowerShell Magic' do + expect(described_class.to_cmd([".\\test.exe"] + ['--%', 'not', '$parsed'])).to eq(".\\test.exe '--%' not '$parsed'") + end + + it 'should not split comma args' do + expect(described_class.to_cmd([".\\test.exe"] + ['arg1,notarg2'])).to eq(".\\test.exe 'arg1,notarg2'") + end + + it 'should handle empty strings' do + expect(described_class.to_cmd([".\\test.exe"] + ['', 'a', '', 'b'])).to eq(".\\test.exe '\"\"' a '\"\"' b") + end + end +end \ No newline at end of file diff --git a/spec/support/acceptance/child_process.rb b/spec/support/acceptance/child_process.rb index cb70f50f30..ee7b4e16ee 100644 --- a/spec/support/acceptance/child_process.rb +++ b/spec/support/acceptance/child_process.rb @@ -22,7 +22,7 @@ module Acceptance def initialize super - @default_timeout = ENV['CI'] ? 120 : 40 + @default_timeout = ENV['CI'] ? 480 : 40 @debug = false @env ||= {} @cmd ||= [] diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 18258fa5cb..b34e25be5e 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -21,15 +21,42 @@ class MetasploitModule < Msf::Post ) end - def upload_show_args_binary + def upload_show_args_binary(details) print_status 'Uploading precompiled binaries' - upload_file(show_args_binary[:path], "data/cmd_exec/#{show_args_binary[:path]}") + upload_file(details[:upload_path], "data/cmd_exec/#{details[:path]}") unless session.platform.eql?('windows') - chmod(show_args_binary[:path]) + chmod(details[:upload_path]) end end + def show_args_binary_space + result = show_args_binary_base + result[:upload_path] = result[:path].gsub('_',' ') + result[:cmd] = result[:cmd].gsub('_',' ') + + result + end + + def show_args_binary_special + result = show_args_binary_base + chars = '~!@#$%^&*(){}`\'"<>,.;:=?+|' + if session.platform == 'windows' + chars = '~!@#$%^&(){}`\',.;=+' + end + result[:upload_path] = result[:path].gsub('show_args', chars) + result[:cmd] = result[:cmd].gsub('show_args', chars) + + result + end + def show_args_binary + result = show_args_binary_base + result[:upload_path] = result[:path] + + result + end + + def show_args_binary_base if session.platform == 'linux' || session.platform == 'unix' { path: 'show_args_linux', cmd: './show_args_linux' } elsif session.platform == 'osx' @@ -40,9 +67,10 @@ class MetasploitModule < Msf::Post { path: 'show_args.exe', cmd: 'show_args.exe' } elsif session.platform == 'windows' && session.arch == 'php' { path: 'show_args.exe', cmd: '.\\show_args.exe' } + elsif session.platform == 'windows' && session.arch == 'java' + { path: 'show_args.exe', cmd: '.\\show_args.exe' } elsif session.platform == 'windows' { path: 'show_args.exe', cmd: './show_args.exe' } - elsif session.type == 'meterpreter' && session.arch == 'java' else raise "unknown platform #{session.platform}" end @@ -56,7 +84,11 @@ class MetasploitModule < Msf::Post # Match the binary name, to support the binary name containing relative or absolute paths, i.e. # "show_args.exe\r\none\r\ntwo", - match = output_binary.match?(expected[0]) && output_args == expected[1..] + if output_binary.nil? + vprint_status("#{__method__}: Malformed output: no process binary returned") + return false + end + match = output_binary.include?(expected[0]) && output_args == expected[1..] if !match vprint_status("#{__method__}: expected: #{expected.inspect} - actual: #{output_lines.inspect}") end @@ -68,7 +100,7 @@ class MetasploitModule < Msf::Post # we are inconsistent reporting windows session types windows_strings = ['windows', 'win'] vprint_status("Starting cmd_exec tests") - upload_show_args_binary + upload_show_args_binary(show_args_binary) it "should return the result of echo" do test_string = Rex::Text.rand_text_alpha(4) @@ -82,11 +114,6 @@ class MetasploitModule < Msf::Post end it 'should execute the show_args binary with a string' do - # TODO: Fix this functionality - if session.type.eql?('meterpreter') && session.arch.eql?('python') - vprint_status("test skipped for Python Meterpreter - functionality not correct") - next true - end output = cmd_exec("#{show_args_binary[:cmd]} one two") valid_show_args_response?(output, expected: [show_args_binary[:path], 'one', 'two']) end @@ -130,12 +157,8 @@ class MetasploitModule < Msf::Post it "should return the result of echo with single quotes" do test_string = Rex::Text.rand_text_alpha(4) if session.platform.eql? 'windows' - if session.arch == ARCH_PYTHON - output = cmd_exec("cmd.exe", "/c echo \"#{test_string}\"") - output == test_string - # TODO: Fix this functionality - elsif session.type.eql?('shell') || session.type.eql?('powershell') - vprint_status("test skipped for Windows CMD and Powershell - functionality not correct") + if session.type.eql?('powershell') + vprint_status("test skipped for Powershell - functionality not correct") true else output = cmd_exec("cmd.exe", "/c echo '#{test_string}'") @@ -150,12 +173,8 @@ class MetasploitModule < Msf::Post it "should return the result of echo with double quotes" do test_string = Rex::Text.rand_text_alpha(4) if session.platform.eql? 'windows' - if session.platform.eql? 'windows' and session.arch == ARCH_PYTHON - output = cmd_exec("cmd.exe", "/c echo \"#{test_string}\"") - output == test_string - # TODO: Fix this functionality - elsif session.type.eql?('shell') || session.type.eql?('powershell') - vprint_status("test skipped for Windows CMD and Powershell - functionality not correct") + if session.type.eql?('powershell') + vprint_status("test skipped for Powershell - functionality not correct") true else output = cmd_exec("cmd.exe", "/c echo \"#{test_string}\"") @@ -189,96 +208,66 @@ class MetasploitModule < Msf::Post end end - # TODO: These tests are in preparation for Smashery's create process API - # def test_create_process - # upload_show_args_binary - # - # test_string = Rex::Text.rand_text_alpha(4) - # - # it 'should accept blank strings and return the create_process output' do - # if session.arch.eql?("php") - # # TODO: Fix this functionality - # - # vprint_status("test skipped for PHP - functionality not correct") - # true - # end - # output = create_process(show_args_binary[:cmd], args: [test_string, '', test_string, '', test_string]) - # valid_show_args_response?(output, expected: [show_args_binary[:path], test_string, '', test_string, '', test_string]) - # end - # - # it 'should accept multiple args and return the create_process output' do - # output = create_process(show_args_binary[:cmd], args: [test_string, test_string]) - # valid_show_args_response?(output, expected: [show_args_binary[:path], test_string, test_string]) - # end - # - # it 'should accept spaces and return the create_process output' do - # output = create_process(show_args_binary[:cmd], args: ['with spaces']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], 'with spaces']) - # end - # - # it 'should accept environment variables and return the create_process output' do - # output = create_process(show_args_binary[:cmd], args: ['$PATH']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], '$PATH']) - # end - # - # it 'should accept environment variables within a string and return the create_process output' do - # output = create_process(show_args_binary[:cmd], args: ["it's $PATH"]) - # valid_show_args_response?(output, expected: [show_args_binary[:path], "it's $PATH"]) - # end - # - # it 'should accept special characters and return the create_process output' do - # if session.platform.eql? 'windows' - # # TODO: Fix this functionality - # vprint_status('test skipped for Windows - functionality not correct') - # true - # end - # output = create_process(show_args_binary[:cmd], args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], '~!@#$%^&*(){`1234567890[]",.\'<>']) - # end - # - # it 'should accept command line commands and return the create_process output' do - # if session.arch.eql?("php") - # # TODO: Fix this functionality - # vprint_status("test skipped for PHP - functionality not correct") - # true - # end - # - # output = create_process(show_args_binary[:cmd], args: ['run&echo']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], 'run&echo']) - # end - # - # it 'should accept semicolons to separate multiple command on a single line and return the create_process output' do - # if session.arch.eql?("php") - # # TODO: Fix this functionality - # vprint_status("test skipped for PHP - functionality not correct") - # true - # end - # - # output = create_process(show_args_binary[:cmd], args: ['run&echo;test']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], 'run&echo;test']) - # end - # - # it 'should accept spaces in the filename and return the create_process output' do - # if session.platform.eql? 'windows' - # # TODO: Fix this functionality - # vprint_status('test skipped for Windows CMD - functionality not correct') - # true - # end - # - # output = create_process('./show_args file', args: [test_string, test_string]) - # valid_show_args_response?(output, expected: ['./show_args file', test_string, test_string]) - # end - # - # it 'should accept special characters in the filename and return the create_process output' do - # if session.platform.eql? 'windows' - # # TODO: Fix this functionality - # vprint_status('test skipped for Windows CMD - functionality not correct') - # true - # end - # - # output = create_process('./~!@#$%^&*(){}', args: [test_string, test_string]) - # valid_show_args_response?(output, expected: ['./~!@#$%^&*(){}', test_string, test_string]) - # end - # end - # end + def test_create_process + upload_show_args_binary(show_args_binary) + upload_show_args_binary(show_args_binary_space) + upload_show_args_binary(show_args_binary_special) + + test_string = Rex::Text.rand_text_alpha(4) + + it 'should accept blank strings and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: [test_string, '', test_string, '', test_string]) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], test_string, '', test_string, '', test_string]) + end + + it 'should accept multiple args and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: [test_string, test_string]) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], test_string, test_string]) + end + + it 'should accept spaces and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ['with spaces']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], 'with spaces']) + end + + it 'should accept environment variables and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ['$PATH']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '$PATH']) + end + + it 'should accept environment variables within a string and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ["it's $PATH"]) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], "it's $PATH"]) + end + + it 'should deal with weird windows edge cases' do + output = create_process(show_args_binary[:cmd], args: ['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\']) + end + + it 'should accept special characters and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ['~!@#$%^&*(){`1234567890[]",.\'<>\\']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '~!@#$%^&*(){`1234567890[]",.\'<>\\']) + end + + it 'should accept command line commands and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ['run&echo']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], 'run&echo']) + end + + it 'should accept semicolons to separate multiple command on a single line and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ['run&echo;test']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], 'run&echo;test']) + end + + it 'should accept spaces in the filename and return the create_process output' do + output = create_process(show_args_binary_space[:cmd], args: [test_string, test_string]) + valid_show_args_response?(output, expected: [show_args_binary_space[:cmd], test_string, test_string]) + end + + it 'should accept special characters in the filename and return the create_process output' do + output = create_process(show_args_binary_special[:cmd], args: [test_string, test_string]) + valid_show_args_response?(output, expected: [show_args_binary_special[:cmd], test_string, test_string]) + end + end end