From 38cb15d0ac66cb23f68f4261ce98f6a44a77d28e Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 9 Apr 2024 13:06:30 +1000 Subject: [PATCH 01/13] Implement new cmd_exec API for PowerShell --- lib/msf/base/sessions/command_shell_unix.rb | 7 ++ .../base/sessions/command_shell_windows.rb | 7 ++ lib/msf/base/sessions/powershell.rb | 66 +++++++++++++++++++ lib/msf/core/post/common.rb | 27 ++++++++ .../extensions/stdapi/sys/process.rb | 11 +++- .../post/meterpreter/extensions/stdapi/tlv.rb | 1 + .../base/sessions/powershell_mixin_spec.rb | 57 ++++++++++++++++ 7 files changed, 175 insertions(+), 1 deletion(-) create mode 100755 spec/lib/msf/base/sessions/powershell_mixin_spec.rb diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index c3bd2dd1350c..501b898bcd8a 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -5,9 +5,16 @@ def initialize(*args) 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 executable [String] The process to launch + # @param args [Array] The arguments to the process + def to_cmd(executable, args) + end end end diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index b98667a758e2..e67d70766e60 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -6,9 +6,16 @@ def initialize(*args) self.platform = "windows" super 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 executable [String] The process to launch + # @param args [Array] The arguments to the process + def to_cmd(executable, args) + end end end diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index cbf3bf08f077..e166b7a4f455 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -34,6 +34,72 @@ def shell_command(cmd, timeout = 1800) end buff end + + # Convert the executable and argument array to a command that can be run in this command shell + # @param executable [String] The process to launch + # @param args [Array] The arguments to the process + def to_cmd(executable, 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 = [executable] + args + cmd_and_args.each_with_index do |arg, index| + needs_single_quoting = false + if arg.include?("'") + 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.gsub!(/(\\+)"/, '\\1\\1"') + + # Then we can safely prepend a backslash to escape our double-quote + 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 + + if needs_single_quoting + arg = "'#{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 end include Mixin diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index cc7639ede019..d53cd4e5be3a 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -52,6 +52,33 @@ def peer "#{rhost}:#{rport}" end + def create_process(executable, args: [], time_out: 15, opts: {}) + case session.type + when 'meterpreter' + session.response_timeout = time_out + opts = { + 'Hidden' => true, + 'Channelized' => true, + }.merge(opts) + + 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/rex/post/meterpreter/extensions/stdapi/sys/process.rb b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb index 85477d5f145c..47d1547bcf30 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb @@ -166,9 +166,18 @@ def Process.execute(path, arguments = nil, opts = nil) 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) + arguments.each do |arg| + request.add_tlv(TLV_TYPE_PROCESS_ARGUMENT, arg); + end + elsif arguments.kind_of?(String) + request.add_tlv(TLV_TYPE_PROCESS_ARGUMENTS, arguments) + else + raise ArgumentError.new('Unknown type for arguments') + end end request.add_tlv(TLV_TYPE_PROCESS_FLAGS, flags); diff --git a/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb b/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb index 924838a4af55..dfcbdcf2e3b4 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb @@ -170,6 +170,7 @@ module Stdapi 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_DRIVER_ENTRY = TLV_META_TYPE_GROUP | 2320 TLV_TYPE_DRIVER_BASENAME = TLV_META_TYPE_STRING | 2321 diff --git a/spec/lib/msf/base/sessions/powershell_mixin_spec.rb b/spec/lib/msf/base/sessions/powershell_mixin_spec.rb new file mode 100755 index 000000000000..e697f99bcc51 --- /dev/null +++ b/spec/lib/msf/base/sessions/powershell_mixin_spec.rb @@ -0,0 +1,57 @@ + +RSpec.describe Msf::Sessions::PowerShell::Mixin do + let(:obj) do + o = Object.new + o.extend(described_class) + + o + end + + + describe 'to_cmd processing' do + it 'should not do anything for simple args' do + expect(obj.to_cmd(".\\test.exe", ['abc', '123'])).to eq(".\\test.exe abc 123") + expect(obj.to_cmd("C:\\SysinternalsSuite\\procexp.exe", [])).to eq("C:\\SysinternalsSuite\\procexp.exe") + end + + it 'should double single-quotes' do + expect(obj.to_cmd(".\\test.exe", ["'abc'"])).to eq(".\\test.exe '''abc'''") + end + + it 'should escape less than' do + expect(obj.to_cmd(".\\test.exe", ["'abc'", '>', 'out.txt'])).to eq(".\\test.exe '''abc''' '>' out.txt") + end + + it 'should escape other special chars' do + expect(obj.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(obj.to_cmd(".\\test.exe", ['"abc'])).to eq(".\\test.exe '\\\"abc'") + end + + it 'should correctly backslash escape backslashes and double-quotes' do + expect(obj.to_cmd(".\\test.exe", ['\\"abc'])).to eq(".\\test.exe '\\\\\\\"abc'") + expect(obj.to_cmd(".\\test.exe", ['\\\\"abc'])).to eq(".\\test.exe '\\\\\\\\\\\"abc'") + expect(obj.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(obj.to_cmd(".\\test$.exe", ['abc'])).to eq("& '.\\test$.exe' abc") + expect(obj.to_cmd(".\\test'.exe", ['abc'])).to eq("& '.\\test''.exe' abc") + expect(obj.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(obj.to_cmd(".\\test.exe", ['$env:path'])).to eq(".\\test.exe '$env:path'") + end + + it 'should not respect PowerShell Magic' do + expect(obj.to_cmd(".\\test.exe", ['--%', 'not', '$parsed'])).to eq(".\\test.exe '--%' not '$parsed'") + end + + it 'should not split comma args' do + expect(obj.to_cmd(".\\test.exe", ['arg1,notarg2'])).to eq(".\\test.exe 'arg1,notarg2'") + end + end +end \ No newline at end of file From 3ce5b660781cdff05f947faaee756f264aa77283 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 9 Apr 2024 16:11:52 +1000 Subject: [PATCH 02/13] Implement new cmd_exec API for Windows cmd --- .../base/sessions/command_shell_windows.rb | 76 +++++++++++++++++-- .../command_shell_windows_mixin_spec.rb | 53 +++++++++++++ 2 files changed, 122 insertions(+), 7 deletions(-) create mode 100755 spec/lib/msf/base/sessions/command_shell_windows_mixin_spec.rb diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index e67d70766e60..a566cc37410d 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -1,7 +1,75 @@ - module Msf::Sessions class CommandShellWindows < CommandShell + + module Mixin + # Convert the executable and argument array to a command that can be run in this command shell + # @param executable [String] The process to launch + # @param args [Array] The arguments to the process + def to_cmd(executable, 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". + + cmd_and_args = [executable] + args + quote_requiring = ['"', '^', ' ', '&', '<', '>', '|'] + + escaped_cmd_and_args = cmd_and_args.map do |arg| + # Double-up all quote chars + arg.gsub!('"', '""') + + # Now the fun begins + current_token = "" + result = "" + in_quotes = false + + arg.each_char do |char| + if char == '%' + if in_quotes + # This token has been in an inside-quote context, so let's properly wrap that before continuing + current_token = "\"#{current_token}\"" + end + result += current_token + result += '^%' # 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, so that when we actually append the token + in_quotes = true + end + current_token += char + end + + if in_quotes + # This token has been in an inside-quote context, so let's properly wrap that before continuing + current_token = "\"#{current_token}\"" + end + result += current_token + + result + end + + escaped_cmd_and_args.join(' ') + end + end + + include Mixin + def initialize(*args) self.platform = "windows" super @@ -10,12 +78,6 @@ def initialize(*args) 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 executable [String] The process to launch - # @param args [Array] The arguments to the process - def to_cmd(executable, args) - end end end diff --git a/spec/lib/msf/base/sessions/command_shell_windows_mixin_spec.rb b/spec/lib/msf/base/sessions/command_shell_windows_mixin_spec.rb new file mode 100755 index 000000000000..bb2581d1551f --- /dev/null +++ b/spec/lib/msf/base/sessions/command_shell_windows_mixin_spec.rb @@ -0,0 +1,53 @@ +RSpec.describe Msf::Sessions::CommandShellWindows::Mixin do + let(:obj) do + o = Object.new + o.extend(described_class) + + o + end + + describe 'to_cmd processing' do + it 'should not do anything for simple args' do + expect(obj.to_cmd('test.exe', [])).to eq('test.exe') + expect(obj.to_cmd('test.exe', ['basic','args'])).to eq('test.exe basic args') + end + + it 'should quote spaces' do + expect(obj.to_cmd('C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE', [])).to eq('"C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE"') + expect(obj.to_cmd('test.exe', ['with space'])).to eq('test.exe "with space"') + end + + it 'should escape logical operators' do + expect(obj.to_cmd('test.exe', ['&&', 'echo', 'words'])).to eq('test.exe "&&" echo words') + expect(obj.to_cmd('test.exe', ['||', 'echo', 'words'])).to eq('test.exe "||" echo words') + expect(obj.to_cmd('test.exe', ['&echo', 'words'])).to eq('test.exe "&echo" words') + expect(obj.to_cmd('test.exe', ['run&echo', 'words'])).to eq('test.exe "run&echo" words') + end + + it 'should escape redirectors' do + expect(obj.to_cmd('test.exe', ['>', 'out.txt'])).to eq('test.exe ">" out.txt') + expect(obj.to_cmd('test.exe', ['<', 'in.txt'])).to eq('test.exe "<" in.txt') + end + + it 'should escape carets' do + expect(obj.to_cmd('test.exe', ['with^caret'])).to eq('test.exe "with^caret"') + expect(obj.to_cmd('test.exe', ['with^^carets'])).to eq('test.exe "with^^carets"') + end + + it 'should not expand env vars' do + expect(obj.to_cmd('test.exe', ['%temp%'])).to eq('test.exe ^%temp^%') + expect(obj.to_cmd('test.exe', ['env', 'var', 'is', '%temp%'])).to eq('test.exe env var is ^%temp^%') + end + + it 'should handle combinations of quoting and percent-escaping' do + expect(obj.to_cmd('test.exe', ['env var is %temp%'])).to eq('test.exe "env var is "^%temp^%') + expect(obj.to_cmd('test.exe', ['env var is %temp%, yes, %TEMP%'])).to eq('test.exe "env var is "^%temp^%", yes, "^%TEMP^%') + expect(obj.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(obj.to_cmd('test.exe', ['%single percent'])).to eq('test.exe ^%"single percent"') + expect(obj.to_cmd('test.exe', ['100%'])).to eq('test.exe 100^%') + end + end +end \ No newline at end of file From 725accf4692244f534aefe3fe69e03ddf9f83cb1 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 10 Apr 2024 08:30:30 +1000 Subject: [PATCH 03/13] Add unix shell to create_process API --- lib/msf/base/sessions/command_shell_unix.rb | 34 ++++++++++++--- .../base/sessions/command_shell_windows.rb | 2 +- lib/msf/base/sessions/powershell.rb | 6 +-- .../sessions/command_shell_unix_mixin_spec.rb | 43 +++++++++++++++++++ .../base/sessions/powershell_mixin_spec.rb | 1 - 5 files changed, 75 insertions(+), 11 deletions(-) create mode 100755 spec/lib/msf/base/sessions/command_shell_unix_mixin_spec.rb diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index 501b898bcd8a..0b7105fc38e4 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -1,6 +1,34 @@ module Msf::Sessions class CommandShellUnix < CommandShell + + module Mixin + # Convert the executable and argument array to a command that can be run in this command shell + # @param executable [String] The process to launch + # @param args [Array] The arguments to the process + def to_cmd(executable, args) + needs_escaping = "'" + chars_need_quoting = ['"', '\\', '$', '`', '(', ')', ' ', '<', '>', '&', '|'] + cmd_and_args = [executable] + args + escaped = cmd_and_args.map do |arg| + needs_quoting = chars_need_quoting.any? do |char| + arg.include?(char) + end + + arg = arg.gsub("'", "\\\\'") + if needs_quoting + arg = "'#{arg}'" + end + + arg + end + + escaped.join(' ') + end + end + + include Mixin + def initialize(*args) self.platform = "unix" super @@ -9,12 +37,6 @@ def initialize(*args) 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 executable [String] The process to launch - # @param args [Array] The arguments to the process - def to_cmd(executable, args) - end end end diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index a566cc37410d..2f6246a94d85 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -27,7 +27,7 @@ def to_cmd(executable, args) escaped_cmd_and_args = cmd_and_args.map do |arg| # Double-up all quote chars - arg.gsub!('"', '""') + arg = arg.gsub('"', '""') # Now the fun begins current_token = "" diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index e166b7a4f455..dcd392e55c9b 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -54,7 +54,7 @@ def to_cmd(executable, args) cmd_and_args.each_with_index do |arg, index| needs_single_quoting = false if arg.include?("'") - arg.gsub!("'", "''") + arg = arg.gsub("'", "''") needs_single_quoting = true end @@ -62,10 +62,10 @@ def to_cmd(executable, args) # 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.gsub!(/(\\+)"/, '\\1\\1"') + arg = arg.gsub(/(\\+)"/, '\\1\\1"') # Then we can safely prepend a backslash to escape our double-quote - arg.gsub!('"', '\\"') + arg = arg.gsub('"', '\\"') needs_single_quoting = true end diff --git a/spec/lib/msf/base/sessions/command_shell_unix_mixin_spec.rb b/spec/lib/msf/base/sessions/command_shell_unix_mixin_spec.rb new file mode 100755 index 000000000000..23e9c1687cee --- /dev/null +++ b/spec/lib/msf/base/sessions/command_shell_unix_mixin_spec.rb @@ -0,0 +1,43 @@ +RSpec.describe Msf::Sessions::CommandShellUnix::Mixin do + let(:obj) do + o = Object.new + o.extend(described_class) + + o + end + + describe 'to_cmd processing' do + it 'should not do anything for simple args' do + expect(obj.to_cmd('./test', [])).to eq('./test') + expect(obj.to_cmd('sh', [])).to eq('sh') + expect(obj.to_cmd('./test', ['basic','args'])).to eq('./test basic args') + end + + it 'should quote spaces' do + expect(obj.to_cmd('/home/user/some folder/some program', [])).to eq("'/home/user/some folder/some program'") + expect(obj.to_cmd('./test', ['with space'])).to eq("./test 'with space'") + end + + it 'should quote logical operators' do + expect(obj.to_cmd('./test', ['&&', 'echo', 'words'])).to eq("./test '&&' echo words") + expect(obj.to_cmd('./test', ['||', 'echo', 'words'])).to eq("./test '||' echo words") + expect(obj.to_cmd('./test', ['&echo', 'words'])).to eq("./test '&echo' words") + expect(obj.to_cmd('./test', ['run&echo', 'words'])).to eq("./test 'run&echo' words") + end + + it 'should escape single quotes' do + expect(obj.to_cmd('./test', ["it's"])).to eq("./test it\\'s") + expect(obj.to_cmd('./test', ["it's a param"])).to eq("./test 'it\\'s a param'") + end + + it 'should quote redirectors' do + expect(obj.to_cmd('./test', ['>', 'out.txt'])).to eq("./test '>' out.txt") + expect(obj.to_cmd('./test', ['<', 'in.txt'])).to eq("./test '<' in.txt") + end + + it 'should not expand env vars' do + expect(obj.to_cmd('./test', ['$PATH'])).to eq("./test '$PATH'") + expect(obj.to_cmd('./test', ['env', 'var', 'is', '$PATH'])).to eq("./test env var is '$PATH'") + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/base/sessions/powershell_mixin_spec.rb b/spec/lib/msf/base/sessions/powershell_mixin_spec.rb index e697f99bcc51..9957c656bc0d 100755 --- a/spec/lib/msf/base/sessions/powershell_mixin_spec.rb +++ b/spec/lib/msf/base/sessions/powershell_mixin_spec.rb @@ -1,4 +1,3 @@ - RSpec.describe Msf::Sessions::PowerShell::Mixin do let(:obj) do o = Object.new From 580f8ca06d6ee310723804ef44dceaa840366358 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 10 Apr 2024 16:18:52 +1000 Subject: [PATCH 04/13] Reworked unix create_process, as it was buggy --- lib/msf/base/sessions/command_shell_unix.rb | 66 +++++---- .../base/sessions/command_shell_windows.rb | 126 +++++++++--------- lib/msf/base/sessions/powershell.rb | 121 +++++++++-------- lib/msf/core/post/common.rb | 6 + .../extensions/stdapi/sys/process.rb | 26 +++- .../sessions/command_shell_unix_mixin_spec.rb | 43 ------ .../base/sessions/command_shell_unix_spec.rb | 44 ++++++ .../command_shell_windows_mixin_spec.rb | 53 -------- .../sessions/command_shell_windows_spec.rb | 47 +++++++ .../base/sessions/powershell_mixin_spec.rb | 56 -------- spec/lib/msf/base/sessions/powershell_spec.rb | 48 +++++++ 11 files changed, 330 insertions(+), 306 deletions(-) delete mode 100755 spec/lib/msf/base/sessions/command_shell_unix_mixin_spec.rb create mode 100755 spec/lib/msf/base/sessions/command_shell_unix_spec.rb delete mode 100755 spec/lib/msf/base/sessions/command_shell_windows_mixin_spec.rb create mode 100755 spec/lib/msf/base/sessions/command_shell_windows_spec.rb delete mode 100755 spec/lib/msf/base/sessions/powershell_mixin_spec.rb create mode 100755 spec/lib/msf/base/sessions/powershell_spec.rb diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index 0b7105fc38e4..be2d93328bea 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -1,34 +1,6 @@ module Msf::Sessions class CommandShellUnix < CommandShell - - module Mixin - # Convert the executable and argument array to a command that can be run in this command shell - # @param executable [String] The process to launch - # @param args [Array] The arguments to the process - def to_cmd(executable, args) - needs_escaping = "'" - chars_need_quoting = ['"', '\\', '$', '`', '(', ')', ' ', '<', '>', '&', '|'] - cmd_and_args = [executable] + args - escaped = cmd_and_args.map do |arg| - needs_quoting = chars_need_quoting.any? do |char| - arg.include?(char) - end - - arg = arg.gsub("'", "\\\\'") - if needs_quoting - arg = "'#{arg}'" - end - - arg - end - - escaped.join(' ') - end - end - - include Mixin - def initialize(*args) self.platform = "unix" super @@ -37,6 +9,44 @@ def initialize(*args) 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 executable [String] The process to launch + # @param args [Array] The arguments to the process + def to_cmd(executable, args) + self.class.to_cmd(executable, args) + end + + # Convert the executable and argument array to a command that can be run in this command shell + # @param executable [String] The process to launch + # @param args [Array] The arguments to the process + def self.to_cmd(executable, args) + always_quote = /[']/ + always_escape = /([$"])/ + escape_if_not_quoted = /([\\`\(\)<>&| ])/ + cmd_and_args = [executable] + args + escaped = cmd_and_args.map do |arg| + needs_quoting = false + if arg.match(always_quote) + needs_quoting = true + else + arg = arg.gsub(escape_if_not_quoted, "\\\\\\1") + end + arg = arg.gsub(always_escape, "\\\\\\1") + + # Do this at the end, so we don't get confused between the double-quotes we're escaping, and the ones we're using to wrap. + if needs_quoting + arg = "\"#{arg}\"" + end + + if arg.include?("'") + end + + arg + end + + escaped.join(' ') + end end end diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index 2f6246a94d85..ac22f92248e0 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -1,82 +1,84 @@ module Msf::Sessions class CommandShellWindows < CommandShell + def initialize(*args) + self.platform = "windows" + super + end + + def shell_command_token(cmd,timeout = 10) + shell_command_token_win32(cmd,timeout) + end - module Mixin - # Convert the executable and argument array to a command that can be run in this command shell - # @param executable [String] The process to launch - # @param args [Array] The arguments to the process - def to_cmd(executable, 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". + # Convert the executable and argument array to a command that can be run in this command shell + # @param executable [String] The process to launch + # @param args [Array] The arguments to the process + def to_cmd(executable, args) + self.class.to_cmd + end - cmd_and_args = [executable] + args - quote_requiring = ['"', '^', ' ', '&', '<', '>', '|'] + # Convert the executable and argument array to a command that can be run in this command shell + # @param executable [String] The process to launch + # @param args [Array] The arguments to the process + def self.to_cmd(executable, 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". - escaped_cmd_and_args = cmd_and_args.map do |arg| - # Double-up all quote chars - arg = arg.gsub('"', '""') + cmd_and_args = [executable] + args + quote_requiring = ['"', '^', ' ', '&', '<', '>', '|'] - # Now the fun begins - current_token = "" - result = "" - in_quotes = false + escaped_cmd_and_args = cmd_and_args.map do |arg| + # Double-up all quote chars + arg = arg.gsub('"', '""') - arg.each_char do |char| - if char == '%' - if in_quotes - # This token has been in an inside-quote context, so let's properly wrap that before continuing - current_token = "\"#{current_token}\"" - end - result += current_token - result += '^%' # Escape the offending percent + # Now the fun begins + current_token = "" + result = "" + in_quotes = false - # 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, so that when we actually append the token - in_quotes = true + arg.each_char do |char| + if char == '%' + if in_quotes + # This token has been in an inside-quote context, so let's properly wrap that before continuing + current_token = "\"#{current_token}\"" end - current_token += char - end + result += current_token + result += '^%' # Escape the offending percent - if in_quotes - # This token has been in an inside-quote context, so let's properly wrap that before continuing - current_token = "\"#{current_token}\"" + # 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, so that when we actually append the token + in_quotes = true end - result += current_token + current_token += char + end - result + if in_quotes + # This token has been in an inside-quote context, so let's properly wrap that before continuing + current_token = "\"#{current_token}\"" end + result += current_token - escaped_cmd_and_args.join(' ') + result end - end - - include Mixin - - def initialize(*args) - self.platform = "windows" - super - end - def shell_command_token(cmd,timeout = 10) - shell_command_token_win32(cmd,timeout) + escaped_cmd_and_args.join(' ') end end diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index dcd392e55c9b..55c1cfd8abb1 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -34,75 +34,82 @@ def shell_command(cmd, timeout = 1800) end buff end + end + + include Mixin - # Convert the executable and argument array to a command that can be run in this command shell - # @param executable [String] The process to launch - # @param args [Array] The arguments to the process - def to_cmd(executable, args) + # Convert the executable and argument array to a command that can be run in this command shell + # @param executable [String] The process to launch + # @param args [Array] The arguments to the process + def to_cmd(executable, args) + self.class.to_cmd(executable, args) + end + + # Convert the executable and argument array to a command that can be run in this command shell + # @param executable [String] The process to launch + # @param args [Array] The arguments to the process + def self.to_cmd(executable, 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 + # 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 = ['$', '`', '(', ')', '@', '>', '<', '{','}', '&', ',', ' '] + needs_wrapping_chars = ['$', '`', '(', ')', '@', '>', '<', '{','}', '&', ',', ' '] - result = "" - cmd_and_args = [executable] + args - 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 == '--%' + result = "" + cmd_and_args = [executable] + args + 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 - - if needs_single_quoting - arg = "'#{arg}'" - 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 + + if needs_single_quoting + arg = "'#{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 + 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 = "#{result} #{arg}" + result = arg end + else + result = "#{result} #{arg}" end - - result end - end - include Mixin + 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 d53cd4e5be3a..5602ad00b12b 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -61,6 +61,12 @@ def create_process(executable, args: [], time_out: 15, opts: {}) 'Channelized' => true, }.merge(opts) + if session.platform == 'windows' + opts[:legacy_args] = Msf::Sessions::CommandShellWindows.to_cmd(executable, args) + else + opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(executable, args) + end + if opts['Channelized'] o = session.sys.process.capture_output(executable, args, opts, time_out) else diff --git a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb index 47d1547bcf30..268f3e4fea83 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb @@ -107,13 +107,22 @@ def Process._open(pid, perms, inherit = false) # # Executes an application using the arguments provided - # - # Hash arguments supported: - # - # Hidden => true/false - # Channelized => true/false - # Suspended => true/false - # InMemory => true/false + # @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 # def Process.execute(path, arguments = nil, opts = nil) request = Packet.create_request(COMMAND_ID_STDAPI_SYS_PROCESS_EXECUTE) @@ -173,6 +182,9 @@ def Process.execute(path, arguments = nil, opts = nil) arguments.each do |arg| request.add_tlv(TLV_TYPE_PROCESS_ARGUMENT, arg); 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_ARGUMENTS, arguments) else diff --git a/spec/lib/msf/base/sessions/command_shell_unix_mixin_spec.rb b/spec/lib/msf/base/sessions/command_shell_unix_mixin_spec.rb deleted file mode 100755 index 23e9c1687cee..000000000000 --- a/spec/lib/msf/base/sessions/command_shell_unix_mixin_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -RSpec.describe Msf::Sessions::CommandShellUnix::Mixin do - let(:obj) do - o = Object.new - o.extend(described_class) - - o - end - - describe 'to_cmd processing' do - it 'should not do anything for simple args' do - expect(obj.to_cmd('./test', [])).to eq('./test') - expect(obj.to_cmd('sh', [])).to eq('sh') - expect(obj.to_cmd('./test', ['basic','args'])).to eq('./test basic args') - end - - it 'should quote spaces' do - expect(obj.to_cmd('/home/user/some folder/some program', [])).to eq("'/home/user/some folder/some program'") - expect(obj.to_cmd('./test', ['with space'])).to eq("./test 'with space'") - end - - it 'should quote logical operators' do - expect(obj.to_cmd('./test', ['&&', 'echo', 'words'])).to eq("./test '&&' echo words") - expect(obj.to_cmd('./test', ['||', 'echo', 'words'])).to eq("./test '||' echo words") - expect(obj.to_cmd('./test', ['&echo', 'words'])).to eq("./test '&echo' words") - expect(obj.to_cmd('./test', ['run&echo', 'words'])).to eq("./test 'run&echo' words") - end - - it 'should escape single quotes' do - expect(obj.to_cmd('./test', ["it's"])).to eq("./test it\\'s") - expect(obj.to_cmd('./test', ["it's a param"])).to eq("./test 'it\\'s a param'") - end - - it 'should quote redirectors' do - expect(obj.to_cmd('./test', ['>', 'out.txt'])).to eq("./test '>' out.txt") - expect(obj.to_cmd('./test', ['<', 'in.txt'])).to eq("./test '<' in.txt") - end - - it 'should not expand env vars' do - expect(obj.to_cmd('./test', ['$PATH'])).to eq("./test '$PATH'") - expect(obj.to_cmd('./test', ['env', 'var', 'is', '$PATH'])).to eq("./test env var is '$PATH'") - end - end -end \ No newline at end of file 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 000000000000..3e5e25af6884 --- /dev/null +++ b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb @@ -0,0 +1,44 @@ +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') + 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") + # Still escape even when quoted: + 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 + + it 'should not quote and escape every character' 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 + end +end \ No newline at end of file diff --git a/spec/lib/msf/base/sessions/command_shell_windows_mixin_spec.rb b/spec/lib/msf/base/sessions/command_shell_windows_mixin_spec.rb deleted file mode 100755 index bb2581d1551f..000000000000 --- a/spec/lib/msf/base/sessions/command_shell_windows_mixin_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -RSpec.describe Msf::Sessions::CommandShellWindows::Mixin do - let(:obj) do - o = Object.new - o.extend(described_class) - - o - end - - describe 'to_cmd processing' do - it 'should not do anything for simple args' do - expect(obj.to_cmd('test.exe', [])).to eq('test.exe') - expect(obj.to_cmd('test.exe', ['basic','args'])).to eq('test.exe basic args') - end - - it 'should quote spaces' do - expect(obj.to_cmd('C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE', [])).to eq('"C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE"') - expect(obj.to_cmd('test.exe', ['with space'])).to eq('test.exe "with space"') - end - - it 'should escape logical operators' do - expect(obj.to_cmd('test.exe', ['&&', 'echo', 'words'])).to eq('test.exe "&&" echo words') - expect(obj.to_cmd('test.exe', ['||', 'echo', 'words'])).to eq('test.exe "||" echo words') - expect(obj.to_cmd('test.exe', ['&echo', 'words'])).to eq('test.exe "&echo" words') - expect(obj.to_cmd('test.exe', ['run&echo', 'words'])).to eq('test.exe "run&echo" words') - end - - it 'should escape redirectors' do - expect(obj.to_cmd('test.exe', ['>', 'out.txt'])).to eq('test.exe ">" out.txt') - expect(obj.to_cmd('test.exe', ['<', 'in.txt'])).to eq('test.exe "<" in.txt') - end - - it 'should escape carets' do - expect(obj.to_cmd('test.exe', ['with^caret'])).to eq('test.exe "with^caret"') - expect(obj.to_cmd('test.exe', ['with^^carets'])).to eq('test.exe "with^^carets"') - end - - it 'should not expand env vars' do - expect(obj.to_cmd('test.exe', ['%temp%'])).to eq('test.exe ^%temp^%') - expect(obj.to_cmd('test.exe', ['env', 'var', 'is', '%temp%'])).to eq('test.exe env var is ^%temp^%') - end - - it 'should handle combinations of quoting and percent-escaping' do - expect(obj.to_cmd('test.exe', ['env var is %temp%'])).to eq('test.exe "env var is "^%temp^%') - expect(obj.to_cmd('test.exe', ['env var is %temp%, yes, %TEMP%'])).to eq('test.exe "env var is "^%temp^%", yes, "^%TEMP^%') - expect(obj.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(obj.to_cmd('test.exe', ['%single percent'])).to eq('test.exe ^%"single percent"') - expect(obj.to_cmd('test.exe', ['100%'])).to eq('test.exe 100^%') - 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 000000000000..aaa7b44047f9 --- /dev/null +++ b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb @@ -0,0 +1,47 @@ +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 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 + end +end \ No newline at end of file diff --git a/spec/lib/msf/base/sessions/powershell_mixin_spec.rb b/spec/lib/msf/base/sessions/powershell_mixin_spec.rb deleted file mode 100755 index 9957c656bc0d..000000000000 --- a/spec/lib/msf/base/sessions/powershell_mixin_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -RSpec.describe Msf::Sessions::PowerShell::Mixin do - let(:obj) do - o = Object.new - o.extend(described_class) - - o - end - - - describe 'to_cmd processing' do - it 'should not do anything for simple args' do - expect(obj.to_cmd(".\\test.exe", ['abc', '123'])).to eq(".\\test.exe abc 123") - expect(obj.to_cmd("C:\\SysinternalsSuite\\procexp.exe", [])).to eq("C:\\SysinternalsSuite\\procexp.exe") - end - - it 'should double single-quotes' do - expect(obj.to_cmd(".\\test.exe", ["'abc'"])).to eq(".\\test.exe '''abc'''") - end - - it 'should escape less than' do - expect(obj.to_cmd(".\\test.exe", ["'abc'", '>', 'out.txt'])).to eq(".\\test.exe '''abc''' '>' out.txt") - end - - it 'should escape other special chars' do - expect(obj.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(obj.to_cmd(".\\test.exe", ['"abc'])).to eq(".\\test.exe '\\\"abc'") - end - - it 'should correctly backslash escape backslashes and double-quotes' do - expect(obj.to_cmd(".\\test.exe", ['\\"abc'])).to eq(".\\test.exe '\\\\\\\"abc'") - expect(obj.to_cmd(".\\test.exe", ['\\\\"abc'])).to eq(".\\test.exe '\\\\\\\\\\\"abc'") - expect(obj.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(obj.to_cmd(".\\test$.exe", ['abc'])).to eq("& '.\\test$.exe' abc") - expect(obj.to_cmd(".\\test'.exe", ['abc'])).to eq("& '.\\test''.exe' abc") - expect(obj.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(obj.to_cmd(".\\test.exe", ['$env:path'])).to eq(".\\test.exe '$env:path'") - end - - it 'should not respect PowerShell Magic' do - expect(obj.to_cmd(".\\test.exe", ['--%', 'not', '$parsed'])).to eq(".\\test.exe '--%' not '$parsed'") - end - - it 'should not split comma args' do - expect(obj.to_cmd(".\\test.exe", ['arg1,notarg2'])).to eq(".\\test.exe 'arg1,notarg2'") - 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 000000000000..3561a506db74 --- /dev/null +++ b/spec/lib/msf/base/sessions/powershell_spec.rb @@ -0,0 +1,48 @@ +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 + end +end \ No newline at end of file From eab9e235461c7b7162b6ea700b16c3beee8562d3 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 12 Apr 2024 12:35:46 +1000 Subject: [PATCH 05/13] create_process works for basic CommandShell instances --- lib/msf/base/sessions/command_shell_windows.rb | 2 +- lib/msf/base/sessions/powershell.rb | 1 - lib/msf/core/session/provider/single_command_shell.rb | 8 ++++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index ac22f92248e0..bc130f49e28a 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -14,7 +14,7 @@ def shell_command_token(cmd,timeout = 10) # @param executable [String] The process to launch # @param args [Array] The arguments to the process def to_cmd(executable, args) - self.class.to_cmd + self.class.to_cmd(executable, args) end # Convert the executable and argument array to a command that can be run in this command shell diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index 55c1cfd8abb1..87382744368e 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -49,7 +49,6 @@ def to_cmd(executable, args) # @param executable [String] The process to launch # @param args [Array] The arguments to the process def self.to_cmd(executable, 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 diff --git a/lib/msf/core/session/provider/single_command_shell.rb b/lib/msf/core/session/provider/single_command_shell.rb index 57a675f0d5f9..208ecf670f7b 100644 --- a/lib/msf/core/session/provider/single_command_shell.rb +++ b/lib/msf/core/session/provider/single_command_shell.rb @@ -93,6 +93,14 @@ def shell_command_token(cmd, timeout=10) output end + def to_cmd(cmd, args) + if platform == 'windows' + result = Msf::Sessions::CommandShellWindows.to_cmd(cmd, args) + else + result = Msf::Session::CommandShellUnix.to_cmd(cmd, args) + end + end + # We don't know initially whether the shell we have is one that # echos input back to the output stream. If it is, we need to # take this into account when using tokens to extract the data corresponding From 1700516d5bd5561bbf2b3148de6ca98ed7630b90 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 15 Apr 2024 09:27:26 +1000 Subject: [PATCH 06/13] Handle CommandLineToArgv behaviour --- .../base/sessions/command_shell_windows.rb | 58 ++++++++++++++++++- lib/msf/core/post/common.rb | 2 +- .../extensions/stdapi/sys/process.rb | 2 + .../post/meterpreter/extensions/stdapi/tlv.rb | 1 + modules/post/multi/general/execute.rb | 6 +- .../sessions/command_shell_windows_spec.rb | 40 +++++++++++++ 6 files changed, 105 insertions(+), 4 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index bc130f49e28a..546ba5b87072 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -17,6 +17,54 @@ def to_cmd(executable, args) self.class.to_cmd(executable, args) end + # Convert the executable and argument array to a commandline that can be passed to CreateProcessAsUserW. + # @param executable [String] The process to launch + # @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(executable, args) + space_chars = [' ', '\t', '\v'] + + # The first argument is treated differently for the purposes of backslash escaping (and should not contain double-quotes) + needs_quoting = space_chars.any? do |char| + executable.include?(char) + end + + if needs_quoting + executable = "\"#{executable}\"" + end + + escaped_args = args.map do |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 + + cmd_and_args = [executable] + escaped_args + + cmd_and_args.join(' ') + end + # Convert the executable and argument array to a command that can be run in this command shell # @param executable [String] The process to launch # @param args [Array] The arguments to the process @@ -37,7 +85,7 @@ def self.to_cmd(executable, args) # (if we've been inside them in the current "token"), and then start a new "token". cmd_and_args = [executable] + args - quote_requiring = ['"', '^', ' ', '&', '<', '>', '|'] + quote_requiring = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|'] escaped_cmd_and_args = cmd_and_args.map do |arg| # Double-up all quote chars @@ -75,6 +123,14 @@ def self.to_cmd(executable, args) end result += current_token + # 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 diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index 5602ad00b12b..46bf53e7277f 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -62,7 +62,7 @@ def create_process(executable, args: [], time_out: 15, opts: {}) }.merge(opts) if session.platform == 'windows' - opts[:legacy_args] = Msf::Sessions::CommandShellWindows.to_cmd(executable, args) + opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(executable, args) else opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(executable, args) end diff --git a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb index 268f3e4fea83..010793fe7bc2 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb @@ -179,6 +179,8 @@ def Process.execute(path, arguments = nil, opts = nil) # If process arguments were supplied if (arguments != nil) if arguments.kind_of?(Array) + # 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 diff --git a/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb b/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb index dfcbdcf2e3b4..da4a7f88e320 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb @@ -119,6 +119,7 @@ module Stdapi 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 diff --git a/modules/post/multi/general/execute.rb b/modules/post/multi/general/execute.rb index 417d08703cdd..96ef98a7d2ae 100644 --- a/modules/post/multi/general/execute.rb +++ b/modules/post/multi/general/execute.rb @@ -19,14 +19,16 @@ def initialize(info = {}) ) register_options( [ - OptString.new('COMMAND', [false, 'The entire command line to execute on the session']) + OptString.new('COMMAND', [false, 'The entire command line to execute on the session']), + OptString.new('ARG1', [false, 'The entire command line to execute on the session']), + OptString.new('ARG2', [false, 'The entire command line to execute on the session']) ] ) end def run print_status("Executing #{datastore['COMMAND']} on #{session.inspect}...") - res = cmd_exec(datastore['COMMAND']) + res = create_process(datastore['COMMAND'], args: [datastore['ARG1'], datastore['ARG2']]) print_status("Response: #{res}") end end diff --git a/spec/lib/msf/base/sessions/command_shell_windows_spec.rb b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb index aaa7b44047f9..da12f345bd1a 100755 --- a/spec/lib/msf/base/sessions/command_shell_windows_spec.rb +++ b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb @@ -33,6 +33,12 @@ 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 + 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^%') @@ -43,5 +49,39 @@ 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('test.exe', [])).to eq('test.exe') + expect(described_class.argv_to_commandline('test.exe', ['basic','args'])).to eq('test.exe basic args') + expect(described_class.argv_to_commandline('test.exe', ['!@#$%^&*(){}><.,\''])).to eq('test.exe !@#$%^&*(){}><.,\'') + end + + it 'should quote space characters' do + expect(described_class.argv_to_commandline('test.exe', [])).to eq('test.exe') + expect(described_class.argv_to_commandline('test.exe', ['basic','args'])).to eq('test.exe basic args') + end + + it 'should escape double-quote characters' do + expect(described_class.argv_to_commandline('test.exe', ['"one','"two"'])).to eq('test.exe \\"one \\"two\\"') + expect(described_class.argv_to_commandline('test.exe', ['"one "two"'])).to eq('test.exe "\\"one \\"two\\""') + end + + it 'should handle the weird backslash escaping behaviour in front of quotes' do + expect(described_class.argv_to_commandline('test.exe', ['\\\\"'])).to eq('test.exe \\\\\\\\\\"') + expect(described_class.argv_to_commandline('test.exe', ['space \\\\'])).to eq('test.exe "space \\\\\\\\"') + expect(described_class.argv_to_commandline('te st.exe\\', [])).to eq('"te st.exe\\"') # First arg shouldn't obey these strange rules + end + + it 'should handle empty args' do + expect(described_class.argv_to_commandline('test.exe', [''])).to eq('test.exe ""') + expect(described_class.argv_to_commandline('test.exe', ['', ''])).to eq('test.exe "" ""') + end end end \ No newline at end of file From 6967d289a0371def225dceb2a67149c704d56291 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 15 Apr 2024 16:10:32 +1000 Subject: [PATCH 07/13] Comment function --- lib/msf/core/post/common.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index 46bf53e7277f..15cdd14de285 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -52,6 +52,22 @@ def peer "#{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 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) def create_process(executable, args: [], time_out: 15, opts: {}) case session.type when 'meterpreter' From e9972ff988129b05b88b1bec6a8bd2847f03e19a Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 13:03:56 +1000 Subject: [PATCH 08/13] Changes from code review --- lib/msf/base/sessions/command_shell_unix.rb | 3 --- lib/msf/core/post/common.rb | 7 +++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index be2d93328bea..e1ae78caf09a 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -39,9 +39,6 @@ def self.to_cmd(executable, args) arg = "\"#{arg}\"" end - if arg.include?("'") - end - arg end diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index 15cdd14de285..97da43ceb947 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -75,6 +75,9 @@ def create_process(executable, args: [], time_out: 15, opts: {}) 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' @@ -90,11 +93,11 @@ def create_process(executable, args: [], time_out: 15, opts: {}) end when 'powershell' cmd = session.to_cmd(executable, args) - o = session.shell_command("#{cmd}", time_out) + 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 = session.shell_command_token(cmd, time_out) o.chomp! if o end return "" if o.nil? From fece91e19d20605ab89d1d41e0db9bc27f2ca0ac Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 14:04:53 +1000 Subject: [PATCH 09/13] Fix error sending legacy args --- lib/msf/base/sessions/command_shell_unix.rb | 10 ++++++-- .../base/sessions/command_shell_windows.rb | 17 ++------------ lib/msf/core/post/common.rb | 4 ++-- .../base/sessions/command_shell_unix_spec.rb | 6 +---- .../sessions/command_shell_windows_spec.rb | 23 +++++++++---------- 5 files changed, 24 insertions(+), 36 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index e1ae78caf09a..6f2a352c47e2 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -18,13 +18,19 @@ def to_cmd(executable, args) end # Convert the executable and argument array to a command that can be run in this command shell - # @param executable [String] The process to launch + # @param executable [String] The process to launch, or nil if only processing arguments # @param args [Array] The arguments to the process def self.to_cmd(executable, args) always_quote = /[']/ always_escape = /([$"])/ escape_if_not_quoted = /([\\`\(\)<>&| ])/ - cmd_and_args = [executable] + args + + if executable.nil? + cmd_and_args = args + else + cmd_and_args = [executable] + args + end + escaped = cmd_and_args.map do |arg| needs_quoting = false if arg.match(always_quote) diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index 546ba5b87072..f406b728f8dc 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -18,23 +18,12 @@ def to_cmd(executable, args) end # Convert the executable and argument array to a commandline that can be passed to CreateProcessAsUserW. - # @param executable [String] The process to launch # @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(executable, args) + def self.argv_to_commandline(args) space_chars = [' ', '\t', '\v'] - - # The first argument is treated differently for the purposes of backslash escaping (and should not contain double-quotes) - needs_quoting = space_chars.any? do |char| - executable.include?(char) - end - - if needs_quoting - executable = "\"#{executable}\"" - end - escaped_args = args.map do |arg| needs_quoting = space_chars.any? do |char| arg.include?(char) @@ -60,9 +49,7 @@ def self.argv_to_commandline(executable, args) arg end - cmd_and_args = [executable] + escaped_args - - cmd_and_args.join(' ') + escaped_args.join(' ') end # Convert the executable and argument array to a command that can be run in this command shell diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index 97da43ceb947..a5871a728fbf 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -81,9 +81,9 @@ def create_process(executable, args: [], time_out: 15, opts: {}) }.merge(opts) if session.platform == 'windows' - opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(executable, args) + opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(args) else - opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(executable, args) + opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(nil, args) end if opts['Channelized'] diff --git a/spec/lib/msf/base/sessions/command_shell_unix_spec.rb b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb index 3e5e25af6884..d20f9164e011 100755 --- a/spec/lib/msf/base/sessions/command_shell_unix_spec.rb +++ b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb @@ -4,6 +4,7 @@ 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(nil, ['basic','args'])).to eq('basic args') end it 'should escape spaces' do @@ -35,10 +36,5 @@ 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 - - it 'should not quote and escape every character' 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 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 index da12f345bd1a..7da786237f0d 100755 --- a/spec/lib/msf/base/sessions/command_shell_windows_spec.rb +++ b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb @@ -58,30 +58,29 @@ describe 'argv_to_commandline processing' do it 'should not do anything for simple args' do - expect(described_class.argv_to_commandline('test.exe', [])).to eq('test.exe') - expect(described_class.argv_to_commandline('test.exe', ['basic','args'])).to eq('test.exe basic args') - expect(described_class.argv_to_commandline('test.exe', ['!@#$%^&*(){}><.,\''])).to eq('test.exe !@#$%^&*(){}><.,\'') + 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('test.exe', [])).to eq('test.exe') - expect(described_class.argv_to_commandline('test.exe', ['basic','args'])).to eq('test.exe basic args') + 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('test.exe', ['"one','"two"'])).to eq('test.exe \\"one \\"two\\"') - expect(described_class.argv_to_commandline('test.exe', ['"one "two"'])).to eq('test.exe "\\"one \\"two\\""') + 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('test.exe', ['\\\\"'])).to eq('test.exe \\\\\\\\\\"') - expect(described_class.argv_to_commandline('test.exe', ['space \\\\'])).to eq('test.exe "space \\\\\\\\"') - expect(described_class.argv_to_commandline('te st.exe\\', [])).to eq('"te st.exe\\"') # First arg shouldn't obey these strange rules + expect(described_class.argv_to_commandline(['\\\\"'])).to eq('\\\\\\\\\\"') + expect(described_class.argv_to_commandline(['space \\\\'])).to eq('"space \\\\\\\\"') end it 'should handle empty args' do - expect(described_class.argv_to_commandline('test.exe', [''])).to eq('test.exe ""') - expect(described_class.argv_to_commandline('test.exe', ['', ''])).to eq('test.exe "" ""') + 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 From 20f547f8c26b564d16e75c67dfdaa843787d2e91 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 14:37:42 +1000 Subject: [PATCH 10/13] Treat old-style path separately to new (unescaped) path --- .../base/sessions/command_shell_windows.rb | 15 +++++++ lib/msf/core/post/common.rb | 2 + .../extensions/stdapi/sys/process.rb | 5 ++- .../post/meterpreter/extensions/stdapi/tlv.rb | 41 ++++++++++--------- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index f406b728f8dc..2776f2f038e1 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -17,6 +17,21 @@ def to_cmd(executable, args) self.class.to_cmd(executable, args) end + # Escape a process for the command line + # @param executable [String] The process to launch + def self.escape_cmd(executable) + space_chars = [' ', '\t', '\v'] + 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 diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index a5871a728fbf..0cdcc3f821b2 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -82,8 +82,10 @@ def create_process(executable, args: [], time_out: 15, opts: {}) if session.platform == 'windows' opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(args) + opts[:legacy_path] = Msf::Sessions::CommandShellWindows.escape_cmd(executable) else opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(nil, args) + opts[:legacy_path] = Msf::Sessions::CommandShellUnix.to_cmd(executable, []) end if opts['Channelized'] diff --git a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb index 010793fe7bc2..ef4467f62544 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb @@ -173,7 +173,7 @@ def Process.execute(path, arguments = nil, opts = nil) end end - request.add_tlv(TLV_TYPE_PROCESS_PATH, client.unicode_filter_decode( path )); + request.add_tlv(TLV_TYPE_PROCESS_UNESCAPED_PATH, client.unicode_filter_decode( path )); # Add arguments # If process arguments were supplied @@ -184,6 +184,9 @@ def Process.execute(path, arguments = nil, opts = nil) 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 diff --git a/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb b/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb index da4a7f88e320..b6495bb675c7 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb @@ -152,26 +152,27 @@ module Stdapi 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_PROCESS_ARGUMENT = TLV_META_TYPE_STRING | 2310 +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 From 5ba717b562e26a2a05050e95ea557ae45224a8e2 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 16:30:05 +1000 Subject: [PATCH 11/13] Rework unix command line based on testing --- lib/msf/base/sessions/command_shell.rb | 43 +++++++++++++++++++ lib/msf/base/sessions/command_shell_unix.rb | 19 +------- .../base/sessions/command_shell_windows.rb | 32 +------------- .../base/sessions/command_shell_unix_spec.rb | 29 ++++++------- 4 files changed, 60 insertions(+), 63 deletions(-) diff --git a/lib/msf/base/sessions/command_shell.rb b/lib/msf/base/sessions/command_shell.rb index b14fa26240d8..7fe35636b60d 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -733,6 +733,49 @@ def process_autoruns(datastore) 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, so that when we actually append the token + in_quotes = true + end + current_token += char + end + + 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 + 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 6f2a352c47e2..31ab824b01a7 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -21,9 +21,7 @@ def to_cmd(executable, args) # @param executable [String] The process to launch, or nil if only processing arguments # @param args [Array] The arguments to the process def self.to_cmd(executable, args) - always_quote = /[']/ - always_escape = /([$"])/ - escape_if_not_quoted = /([\\`\(\)<>&| ])/ + quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';'] if executable.nil? cmd_and_args = args @@ -32,20 +30,7 @@ def self.to_cmd(executable, args) end escaped = cmd_and_args.map do |arg| - needs_quoting = false - if arg.match(always_quote) - needs_quoting = true - else - arg = arg.gsub(escape_if_not_quoted, "\\\\\\1") - end - arg = arg.gsub(always_escape, "\\\\\\1") - - # Do this at the end, so we don't get confused between the double-quotes we're escaping, and the ones we're using to wrap. - if needs_quoting - arg = "\"#{arg}\"" - end - - arg + CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'") end escaped.join(' ') diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index 2776f2f038e1..99df9f02e0d1 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -93,37 +93,7 @@ def self.to_cmd(executable, args) # Double-up all quote chars arg = arg.gsub('"', '""') - # Now the fun begins - current_token = "" - result = "" - in_quotes = false - - arg.each_char do |char| - if char == '%' - if in_quotes - # This token has been in an inside-quote context, so let's properly wrap that before continuing - current_token = "\"#{current_token}\"" - end - result += current_token - result += '^%' # 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, so that when we actually append the token - in_quotes = true - end - current_token += char - end - - if in_quotes - # This token has been in an inside-quote context, so let's properly wrap that before continuing - current_token = "\"#{current_token}\"" - end - result += current_token + 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 diff --git a/spec/lib/msf/base/sessions/command_shell_unix_spec.rb b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb index d20f9164e011..5bd7b17b8cf0 100755 --- a/spec/lib/msf/base/sessions/command_shell_unix_spec.rb +++ b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb @@ -8,33 +8,32 @@ 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') + 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') + 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\"") + 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') + 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") - # Still escape even when quoted: - 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\\\"\"") + 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 From c3dfc72da3952ba182c126b3069c7694094b7ded Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 20:42:01 +1000 Subject: [PATCH 12/13] Fix bug when no arguments are present --- lib/msf/base/sessions/command_shell_unix.rb | 7 +++- lib/msf/base/sessions/powershell.rb | 5 +++ .../extensions/stdapi/sys/process.rb | 40 +++++++++---------- spec/lib/msf/base/sessions/powershell_spec.rb | 4 ++ 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index 31ab824b01a7..52e3b8b11a5d 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -30,7 +30,12 @@ def self.to_cmd(executable, args) end escaped = cmd_and_args.map do |arg| - CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'") + result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'") + if result == '' + result = "''" + end + + result end escaped.join(' ') diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index 87382744368e..13c35a80b86e 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -92,6 +92,11 @@ def self.to_cmd(executable, args) if needs_single_quoting arg = "'#{arg}'" end + + if arg == '' + # Pass in empty strings + arg = '\'""\'' + end if index == 0 if needs_single_quoting diff --git a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb index ef4467f62544..4c5203653321 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/sys/process.rb @@ -124,7 +124,7 @@ def Process._open(pid, perms, inherit = false) # 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 # - 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 @@ -173,28 +173,26 @@ def Process.execute(path, arguments = nil, opts = nil) end end - request.add_tlv(TLV_TYPE_PROCESS_UNESCAPED_PATH, client.unicode_filter_decode( path )); - # Add arguments # If process arguments were supplied - if (arguments != nil) - if arguments.kind_of?(Array) - # 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_ARGUMENTS, arguments) - else - raise ArgumentError.new('Unknown type for 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); @@ -220,7 +218,7 @@ def Process.execute(path, arguments = nil, opts = nil) # # 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/spec/lib/msf/base/sessions/powershell_spec.rb b/spec/lib/msf/base/sessions/powershell_spec.rb index 3561a506db74..0c3d5cf8713d 100755 --- a/spec/lib/msf/base/sessions/powershell_spec.rb +++ b/spec/lib/msf/base/sessions/powershell_spec.rb @@ -44,5 +44,9 @@ 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 From c4bf2da2e7075a49d281c5cb1852b4913f99da38 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 20:43:50 +1000 Subject: [PATCH 13/13] Remove accidentally committed changes --- modules/post/multi/general/execute.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/post/multi/general/execute.rb b/modules/post/multi/general/execute.rb index 96ef98a7d2ae..76abe4ffadfc 100644 --- a/modules/post/multi/general/execute.rb +++ b/modules/post/multi/general/execute.rb @@ -19,16 +19,14 @@ def initialize(info = {}) ) register_options( [ - OptString.new('COMMAND', [false, 'The entire command line to execute on the session']), - OptString.new('ARG1', [false, 'The entire command line to execute on the session']), - OptString.new('ARG2', [false, 'The entire command line to execute on the session']) + OptString.new('COMMAND', [false, 'The entire command line to execute on the session']) ] ) end def run print_status("Executing #{datastore['COMMAND']} on #{session.inspect}...") - res = create_process(datastore['COMMAND'], args: [datastore['ARG1'], datastore['ARG2']]) - print_status("Response: #{res}") + res = cmd_exec(datastore['COMMAND']) + print_status("Response: \n#{res}") end end