From 955c675334495a4ebddc93ddc5a0537d0a4f1ba2 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 9 Apr 2024 13:06:30 +1000 Subject: [PATCH 01/38] 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 72e657a19c4b9b482b1679d05c3428f78f4f7e3c Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 9 Apr 2024 16:11:52 +1000 Subject: [PATCH 02/38] 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 e0aca710293626763c8d4b551045e2d5552a3e23 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 10 Apr 2024 08:30:30 +1000 Subject: [PATCH 03/38] 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 e9f86c48653fd15112783e5e6ca583f69db08599 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 10 Apr 2024 16:18:52 +1000 Subject: [PATCH 04/38] 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 0d4d6f345d2b57c50f53a86a6e6034b566b27f4a Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 12 Apr 2024 12:35:46 +1000 Subject: [PATCH 05/38] create_process works for basic CommandShell instances --- lib/msf/base/sessions/command_shell_windows.rb | 2 +- lib/msf/base/sessions/powershell.rb | 1 - 2 files changed, 1 insertion(+), 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 From 85d019cd3c40717b456aeb3fbbf48635bd731326 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 15 Apr 2024 09:27:26 +1000 Subject: [PATCH 06/38] 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 658c9fcc322835d7175f9b203a078ac3e39ad68b Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 15 Apr 2024 16:10:32 +1000 Subject: [PATCH 07/38] 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 fe61e46475c76e381986d92d5d7623e0ebe51a5c Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 13:03:56 +1000 Subject: [PATCH 08/38] 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 7d30c67b0182a89597dcdf8924de2853596ca504 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 14:04:53 +1000 Subject: [PATCH 09/38] 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 5d71aa26e3bd1acc552bb318f46b556d99535689 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 14:37:42 +1000 Subject: [PATCH 10/38] 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 d9ed8ec4dc3c8ad5820e29faf98a6a1e216c3508 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 16:30:05 +1000 Subject: [PATCH 11/38] 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 c606e85ace67..fb399994f29e 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -735,6 +735,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 0ab16ae3af076d839bdae9c8c100e5ee580960d6 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 20:42:01 +1000 Subject: [PATCH 12/38] 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 880203b5039c9dcf17cf4b9d2c8c95a15028d124 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 18 Apr 2024 20:43:50 +1000 Subject: [PATCH 13/38] 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 From a69b777a9e9944658b709055629530b7051bc9e3 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 25 Sep 2024 09:05:35 +1000 Subject: [PATCH 14/38] Included tests for create_process API --- test/modules/post/test/cmd_exec.rb | 218 ++++++++++++++++------------- 1 file changed, 121 insertions(+), 97 deletions(-) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 18258fa5cb02..03754f1ab533 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -21,15 +21,38 @@ def initialize(info = {}) ) end - def upload_show_args_binary + def upload_show_args_binary(details) print_status 'Uploading precompiled binaries' - upload_file(show_args_binary[:path], "data/cmd_exec/#{show_args_binary[:path]}") + upload_file(details[:upload_path], "data/cmd_exec/#{details[:path]}") unless session.platform.eql?('windows') - chmod(show_args_binary[:path]) + chmod(details[:upload_path]) end end + def show_args_binary_space + result = show_args_binary_base + result[:upload_path] = result[:path].gsub('_',' ') + result[:cmd] = result[:cmd].gsub('_',' ') + + result + end + + def show_args_binary_special + result = show_args_binary_base + result[:upload_path] = result[:path].gsub('show_args','~!@#$%^&*(){}') + result[:cmd] = result[:cmd].gsub('show_args','~!@#$%^&*(){}') + + result + end + def show_args_binary + result = show_args_binary_base + result[:upload_path] = result[:path] + + result + end + + def show_args_binary_base if session.platform == 'linux' || session.platform == 'unix' { path: 'show_args_linux', cmd: './show_args_linux' } elsif session.platform == 'osx' @@ -56,7 +79,7 @@ def valid_show_args_response?(output, expected:) # Match the binary name, to support the binary name containing relative or absolute paths, i.e. # "show_args.exe\r\none\r\ntwo", - match = output_binary.match?(expected[0]) && output_args == expected[1..] + match = output_binary.include?(expected[0]) && output_args == expected[1..] if !match vprint_status("#{__method__}: expected: #{expected.inspect} - actual: #{output_lines.inspect}") end @@ -68,7 +91,7 @@ def test_cmd_exec # we are inconsistent reporting windows session types windows_strings = ['windows', 'win'] vprint_status("Starting cmd_exec tests") - upload_show_args_binary + upload_show_args_binary(show_args_binary) it "should return the result of echo" do test_string = Rex::Text.rand_text_alpha(4) @@ -189,96 +212,97 @@ def test_cmd_exec_stderr end end - # TODO: These tests are in preparation for Smashery's create process API - # def test_create_process - # upload_show_args_binary - # - # test_string = Rex::Text.rand_text_alpha(4) - # - # it 'should accept blank strings and return the create_process output' do - # if session.arch.eql?("php") - # # TODO: Fix this functionality - # - # vprint_status("test skipped for PHP - functionality not correct") - # true - # end - # output = create_process(show_args_binary[:cmd], args: [test_string, '', test_string, '', test_string]) - # valid_show_args_response?(output, expected: [show_args_binary[:path], test_string, '', test_string, '', test_string]) - # end - # - # it 'should accept multiple args and return the create_process output' do - # output = create_process(show_args_binary[:cmd], args: [test_string, test_string]) - # valid_show_args_response?(output, expected: [show_args_binary[:path], test_string, test_string]) - # end - # - # it 'should accept spaces and return the create_process output' do - # output = create_process(show_args_binary[:cmd], args: ['with spaces']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], 'with spaces']) - # end - # - # it 'should accept environment variables and return the create_process output' do - # output = create_process(show_args_binary[:cmd], args: ['$PATH']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], '$PATH']) - # end - # - # it 'should accept environment variables within a string and return the create_process output' do - # output = create_process(show_args_binary[:cmd], args: ["it's $PATH"]) - # valid_show_args_response?(output, expected: [show_args_binary[:path], "it's $PATH"]) - # end - # - # it 'should accept special characters and return the create_process output' do - # if session.platform.eql? 'windows' - # # TODO: Fix this functionality - # vprint_status('test skipped for Windows - functionality not correct') - # true - # end - # output = create_process(show_args_binary[:cmd], args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], '~!@#$%^&*(){`1234567890[]",.\'<>']) - # end - # - # it 'should accept command line commands and return the create_process output' do - # if session.arch.eql?("php") - # # TODO: Fix this functionality - # vprint_status("test skipped for PHP - functionality not correct") - # true - # end - # - # output = create_process(show_args_binary[:cmd], args: ['run&echo']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], 'run&echo']) - # end - # - # it 'should accept semicolons to separate multiple command on a single line and return the create_process output' do - # if session.arch.eql?("php") - # # TODO: Fix this functionality - # vprint_status("test skipped for PHP - functionality not correct") - # true - # end - # - # output = create_process(show_args_binary[:cmd], args: ['run&echo;test']) - # valid_show_args_response?(output, expected: [show_args_binary[:path], 'run&echo;test']) - # end - # - # it 'should accept spaces in the filename and return the create_process output' do - # if session.platform.eql? 'windows' - # # TODO: Fix this functionality - # vprint_status('test skipped for Windows CMD - functionality not correct') - # true - # end - # - # output = create_process('./show_args file', args: [test_string, test_string]) - # valid_show_args_response?(output, expected: ['./show_args file', test_string, test_string]) - # end - # - # it 'should accept special characters in the filename and return the create_process output' do - # if session.platform.eql? 'windows' - # # TODO: Fix this functionality - # vprint_status('test skipped for Windows CMD - functionality not correct') - # true - # end - # - # output = create_process('./~!@#$%^&*(){}', args: [test_string, test_string]) - # valid_show_args_response?(output, expected: ['./~!@#$%^&*(){}', test_string, test_string]) - # end - # end - # end + def test_create_process + space_filename = 'showargs file' + upload_show_args_binary(show_args_binary) + upload_show_args_binary(show_args_binary_space) + upload_show_args_binary(show_args_binary_special) + + test_string = Rex::Text.rand_text_alpha(4) + + it 'should accept blank strings and return the create_process output' do + if session.arch.eql?("php") + # TODO: Fix this functionality + + vprint_status("test skipped for PHP - functionality not correct") + true + end + output = create_process(show_args_binary[:cmd], args: [test_string, '', test_string, '', test_string]) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], test_string, '', test_string, '', test_string]) + end + + it 'should accept multiple args and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: [test_string, test_string]) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], test_string, test_string]) + end + + it 'should accept spaces and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ['with spaces']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], 'with spaces']) + end + + it 'should accept environment variables and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ['$PATH']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '$PATH']) + end + + it 'should accept environment variables within a string and return the create_process output' do + output = create_process(show_args_binary[:cmd], args: ["it's $PATH"]) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], "it's $PATH"]) + end + + it 'should accept special characters and return the create_process output' do + if session.platform.eql? 'windows' + # TODO: Fix this functionality + vprint_status('test skipped for Windows - functionality not correct') + true + end + output = create_process(show_args_binary[:cmd], args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '~!@#$%^&*(){`1234567890[]",.\'<>']) + end + + it 'should accept command line commands and return the create_process output' do + if session.arch.eql?("php") + # TODO: Fix this functionality + vprint_status("test skipped for PHP - functionality not correct") + true + end + + output = create_process(show_args_binary[:cmd], args: ['run&echo']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], 'run&echo']) + end + + it 'should accept semicolons to separate multiple command on a single line and return the create_process output' do + if session.arch.eql?("php") + # TODO: Fix this functionality + vprint_status("test skipped for PHP - functionality not correct") + true + end + + output = create_process(show_args_binary[:cmd], args: ['run&echo;test']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], 'run&echo;test']) + end + + it 'should accept spaces in the filename and return the create_process output' do + if session.platform.eql? 'windows' + # TODO: Fix this functionality + vprint_status('test skipped for Windows CMD - functionality not correct') + true + end + + output = create_process(show_args_binary_space[:cmd], args: [test_string, test_string]) + valid_show_args_response?(output, expected: [show_args_binary_space[:cmd], test_string, test_string]) + end + + it 'should accept special characters in the filename and return the create_process output' do + if session.platform.eql? 'windows' + # TODO: Fix this functionality + vprint_status('test skipped for Windows CMD - functionality not correct') + true + end + + output = create_process(show_args_binary_special[:cmd], args: [test_string, test_string]) + valid_show_args_response?(output, expected: [show_args_binary_special[:cmd], test_string, test_string]) + end + end end From 593d06e1f5639b65dc632af10202cb34984b6d05 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 25 Sep 2024 12:25:33 +1000 Subject: [PATCH 15/38] Tests working on Windows 10 meterp --- test/modules/post/test/cmd_exec.rb | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 03754f1ab533..bc728be20112 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -39,8 +39,12 @@ def show_args_binary_space def show_args_binary_special result = show_args_binary_base - result[:upload_path] = result[:path].gsub('show_args','~!@#$%^&*(){}') - result[:cmd] = result[:cmd].gsub('show_args','~!@#$%^&*(){}') + chars = '~!@#$%^&*(){}`\'"<>,.;:=?+|' + if session.platform == 'windows' + chars = '~!@#$%^&(){}`\',.;=+' + end + result[:upload_path] = result[:path].gsub('show_args', chars) + result[:cmd] = result[:cmd].gsub('show_args', chars) result end @@ -252,11 +256,6 @@ def test_create_process end it 'should accept special characters and return the create_process output' do - if session.platform.eql? 'windows' - # TODO: Fix this functionality - vprint_status('test skipped for Windows - functionality not correct') - true - end output = create_process(show_args_binary[:cmd], args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '~!@#$%^&*(){`1234567890[]",.\'<>']) end @@ -284,23 +283,11 @@ def test_create_process end it 'should accept spaces in the filename and return the create_process output' do - if session.platform.eql? 'windows' - # TODO: Fix this functionality - vprint_status('test skipped for Windows CMD - functionality not correct') - true - end - output = create_process(show_args_binary_space[:cmd], args: [test_string, test_string]) valid_show_args_response?(output, expected: [show_args_binary_space[:cmd], test_string, test_string]) end it 'should accept special characters in the filename and return the create_process output' do - if session.platform.eql? 'windows' - # TODO: Fix this functionality - vprint_status('test skipped for Windows CMD - functionality not correct') - true - end - output = create_process(show_args_binary_special[:cmd], args: [test_string, test_string]) valid_show_args_response?(output, expected: [show_args_binary_special[:cmd], test_string, test_string]) end From 602506bdb9c863a280c4f4864267a0a5410653d9 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 25 Sep 2024 15:18:11 +1000 Subject: [PATCH 16/38] Updated for PHP and Python --- .../session/provider/single_command_shell.rb | 2 +- test/modules/post/test/cmd_exec.rb | 36 ++++--------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/lib/msf/core/session/provider/single_command_shell.rb b/lib/msf/core/session/provider/single_command_shell.rb index 4a166e48e261..37d95bb4f61b 100644 --- a/lib/msf/core/session/provider/single_command_shell.rb +++ b/lib/msf/core/session/provider/single_command_shell.rb @@ -123,7 +123,7 @@ def set_is_echo_shell(timeout, command_separator) end def shell_command_token_win32(cmd, timeout=10) - shell_command_token_base(cmd, timeout, '&') + shell_command_token_base(cmd, timeout, ' & ') end def shell_command_token_unix(cmd, timeout=10) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index bc728be20112..63c26a3af829 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -109,11 +109,6 @@ def test_cmd_exec end it 'should execute the show_args binary with a string' do - # TODO: Fix this functionality - if session.type.eql?('meterpreter') && session.arch.eql?('python') - vprint_status("test skipped for Python Meterpreter - functionality not correct") - next true - end output = cmd_exec("#{show_args_binary[:cmd]} one two") valid_show_args_response?(output, expected: [show_args_binary[:path], 'one', 'two']) end @@ -161,8 +156,8 @@ def test_cmd_exec_quotes output = cmd_exec("cmd.exe", "/c echo \"#{test_string}\"") output == test_string # TODO: Fix this functionality - elsif session.type.eql?('shell') || session.type.eql?('powershell') - vprint_status("test skipped for Windows CMD and Powershell - functionality not correct") + elsif session.type.eql?('powershell') + vprint_status("test skipped for Powershell - functionality not correct") true else output = cmd_exec("cmd.exe", "/c echo '#{test_string}'") @@ -181,8 +176,8 @@ def test_cmd_exec_quotes output = cmd_exec("cmd.exe", "/c echo \"#{test_string}\"") output == test_string # TODO: Fix this functionality - elsif session.type.eql?('shell') || session.type.eql?('powershell') - vprint_status("test skipped for Windows CMD and Powershell - functionality not correct") + elsif session.type.eql?('powershell') + vprint_status("test skipped for Powershell - functionality not correct") true else output = cmd_exec("cmd.exe", "/c echo \"#{test_string}\"") @@ -202,11 +197,12 @@ def test_cmd_exec_stderr test_string = Rex::Text.rand_text_alpha(4) if session.platform.eql? 'windows' # TODO: Fix this functionality - if session.type.eql?('shell') || session.arch.eql?("php") || session.type.eql?("powershell") - vprint_status("test skipped for Windows CMD, Powershell and PHP - functionality not correct") + if session.type.eql?("powershell") + vprint_status("test skipped for Powershell - functionality not correct") true else output = cmd_exec("cmd.exe", "/c echo #{test_string} 1>&2") + print_status("line is #{output}") output.rstrip == test_string end else @@ -225,12 +221,6 @@ def test_create_process test_string = Rex::Text.rand_text_alpha(4) it 'should accept blank strings and return the create_process output' do - if session.arch.eql?("php") - # TODO: Fix this functionality - - vprint_status("test skipped for PHP - functionality not correct") - true - end output = create_process(show_args_binary[:cmd], args: [test_string, '', test_string, '', test_string]) valid_show_args_response?(output, expected: [show_args_binary[:upload_path], test_string, '', test_string, '', test_string]) end @@ -261,23 +251,11 @@ def test_create_process end it 'should accept command line commands and return the create_process output' do - if session.arch.eql?("php") - # TODO: Fix this functionality - vprint_status("test skipped for PHP - functionality not correct") - true - end - output = create_process(show_args_binary[:cmd], args: ['run&echo']) valid_show_args_response?(output, expected: [show_args_binary[:upload_path], 'run&echo']) end it 'should accept semicolons to separate multiple command on a single line and return the create_process output' do - if session.arch.eql?("php") - # TODO: Fix this functionality - vprint_status("test skipped for PHP - functionality not correct") - true - end - output = create_process(show_args_binary[:cmd], args: ['run&echo;test']) valid_show_args_response?(output, expected: [show_args_binary[:upload_path], 'run&echo;test']) end From ec4e944405ed12d29928645be1375ddeb3166dc9 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 25 Sep 2024 15:56:24 +1000 Subject: [PATCH 17/38] Fix file upload in PowerShell --- lib/msf/base/sessions/powershell.rb | 2 +- lib/msf/core/post/file.rb | 1 + test/modules/post/test/cmd_exec.rb | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index 13c35a80b86e..fb04e1231164 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -56,7 +56,7 @@ def self.to_cmd(executable, args) # - 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 diff --git a/lib/msf/core/post/file.rb b/lib/msf/core/post/file.rb index 4ae5abdf4645..503ffa697ea2 100644 --- a/lib/msf/core/post/file.rb +++ b/lib/msf/core/post/file.rb @@ -740,6 +740,7 @@ def _write_file_powershell_fragment(file_name, data, offset, chunk_size, append else file_mode = 'Create' end + file_name = file_name.gsub("'","''") pwsh_code = <<~PSH try { $encoded='#{encoded_chunk}'; diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 63c26a3af829..ce583a440994 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -202,7 +202,6 @@ def test_cmd_exec_stderr true else output = cmd_exec("cmd.exe", "/c echo #{test_string} 1>&2") - print_status("line is #{output}") output.rstrip == test_string end else From 6fc714c95434219e7f9293ee71e7ed33b6964a04 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 08:12:27 +1000 Subject: [PATCH 18/38] Take stderr tests back out for now --- test/modules/post/test/cmd_exec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index ce583a440994..ea58e0ae7b4b 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -197,8 +197,8 @@ def test_cmd_exec_stderr test_string = Rex::Text.rand_text_alpha(4) if session.platform.eql? 'windows' # TODO: Fix this functionality - if session.type.eql?("powershell") - vprint_status("test skipped for Powershell - functionality not correct") + if session.type.eql?('shell') || session.arch.eql?("php") || session.type.eql?("powershell") + vprint_status("test skipped for Windows CMD, Powershell and PHP - functionality not correct") true else output = cmd_exec("cmd.exe", "/c echo #{test_string} 1>&2") From 7a5471a67398ab79747854372da5031262530a7b Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 10:37:15 +1000 Subject: [PATCH 19/38] Fix bug in chmod for Java meterp --- lib/msf/core/post/file.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/post/file.rb b/lib/msf/core/post/file.rb index 503ffa697ea2..9aa2ae587aff 100644 --- a/lib/msf/core/post/file.rb +++ b/lib/msf/core/post/file.rb @@ -580,7 +580,7 @@ def chmod(path, mode = 0o700) if session.type == 'meterpreter' && session.commands.include?(Rex::Post::Meterpreter::Extensions::Stdapi::COMMAND_ID_STDAPI_FS_CHMOD) session.fs.file.chmod(path, mode) else - cmd_exec("chmod #{mode.to_s(8)} '#{path}'") + create_process('chmod', args: [mode.to_s(8), path]) end end From 75157f875907be5d68cdf213b17bf3f5cbbd5d1b Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 10:37:52 +1000 Subject: [PATCH 20/38] Fix test case for java on Windows --- test/modules/post/test/cmd_exec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index ea58e0ae7b4b..1db7005b8ab0 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -67,9 +67,10 @@ def show_args_binary_base { path: 'show_args.exe', cmd: 'show_args.exe' } elsif session.platform == 'windows' && session.arch == 'php' { path: 'show_args.exe', cmd: '.\\show_args.exe' } + elsif session.platform == 'windows' && session.arch == 'java' + { path: 'show_args.exe', cmd: '.\\show_args.exe' } elsif session.platform == 'windows' { path: 'show_args.exe', cmd: './show_args.exe' } - elsif session.type == 'meterpreter' && session.arch == 'java' else raise "unknown platform #{session.platform}" end From 2a8924ead348d3f59ac1edd480e3e9ab3e11a967 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 10:38:20 +1000 Subject: [PATCH 21/38] Fix shell file upload when filename has interesting characters --- lib/msf/core/post/file.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/post/file.rb b/lib/msf/core/post/file.rb index 9aa2ae587aff..c5b0518e6f7a 100644 --- a/lib/msf/core/post/file.rb +++ b/lib/msf/core/post/file.rb @@ -912,7 +912,7 @@ def _win_bin_write_file(file_name, data, chunk_size = 5000) success = _win_ansi_write_file(b64_filename, b64_data, chunk_size) return false unless success vprint_status("Uploaded Base64-encoded file. Decoding using certutil") - success = _shell_command_with_success_code("certutil -f -decode #{b64_filename} #{file_name}") + success = _shell_process_with_success_code('certutil', ['-f', '-decode', b64_filename, file_name]) return false unless success rescue ::Exception => e print_error("Exception while running #{__method__}: #{e}") @@ -938,10 +938,10 @@ def _win_bin_append_file(file_name, data, chunk_size = 5000) success = _win_ansi_write_file(b64_filename, b64_data, chunk_size) return false unless success vprint_status("Uploaded Base64-encoded file. Decoding using certutil") - success = _shell_command_with_success_code("certutil -decode #{b64_filename} #{tmp_filename}") + success = _shell_process_with_success_code('certutil', ['-decode', b64_filename, tmp_filename]) return false unless success vprint_status("Certutil succeeded. Appending using copy") - success = _shell_command_with_success_code("copy /b #{file_name}+#{tmp_filename} #{file_name}") + success = _shell_process_with_success_code('copy', ['/b', "#{file_name}+#{tmp_filename}", file_name]) return false unless success rescue ::Exception => e print_error("Exception while running #{__method__}: #{e}") @@ -1108,6 +1108,15 @@ def _shell_command_with_success_code(cmd) return result&.include?(token) end + def _shell_process_with_success_code(executable, args) + cmd = session.to_cmd(executable, args) + token = "_#{::Rex::Text.rand_text_alpha(32)}" + result = session.shell_command_token("#{cmd} && echo #{token}") + + return result&.include?(token) + end + + # # Calculate the maximum line length for a unix shell. # From a32a302f09c6dca4d0c203b2adab49753a852e94 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 10:38:36 +1000 Subject: [PATCH 22/38] Fix issue with windows command shells --- lib/msf/core/session/provider/single_command_shell.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msf/core/session/provider/single_command_shell.rb b/lib/msf/core/session/provider/single_command_shell.rb index 37d95bb4f61b..4a166e48e261 100644 --- a/lib/msf/core/session/provider/single_command_shell.rb +++ b/lib/msf/core/session/provider/single_command_shell.rb @@ -123,7 +123,7 @@ def set_is_echo_shell(timeout, command_separator) end def shell_command_token_win32(cmd, timeout=10) - shell_command_token_base(cmd, timeout, ' & ') + shell_command_token_base(cmd, timeout, '&') end def shell_command_token_unix(cmd, timeout=10) From c543971b8ae1339a986f9a4b2276e913c3fdd965 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 14:08:50 +1000 Subject: [PATCH 23/38] Support uploading files on linux shell containing quote characters --- lib/msf/base/sessions/command_shell_unix.rb | 29 ++++++++++++++----- .../base/sessions/command_shell_windows.rb | 19 ++++++++---- lib/msf/core/post/file.rb | 6 ++-- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index 52e3b8b11a5d..a9296d4e62d3 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -17,12 +17,17 @@ def to_cmd(executable, args) self.class.to_cmd(executable, args) end + # + # Escape an individual argument per Unix shell rules + # @param arg [String] Shell argument + def escape_arg(arg) + self.class.escape_arg(arg) + end + # Convert the executable and argument array to a command that can be run in this command shell # @param 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) - quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';'] - if executable.nil? cmd_and_args = args else @@ -30,16 +35,24 @@ def self.to_cmd(executable, args) end escaped = cmd_and_args.map do |arg| - result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'") - if result == '' - result = "''" - end - - result + escape_arg(arg) end escaped.join(' ') end + + # + # Escape an individual argument per Unix shell rules + # @param arg [String] Shell argument + def self.escape_arg(arg) + quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';'] + result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'") + if result == '' + result = "''" + end + + result + end end end diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index 99df9f02e0d1..6bafad69be24 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -6,6 +6,10 @@ def initialize(*args) super end + def self.space_chars + [' ', '\t', '\v'] + end + def shell_command_token(cmd,timeout = 10) shell_command_token_win32(cmd,timeout) end @@ -20,7 +24,6 @@ def to_cmd(executable, args) # 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 @@ -38,8 +41,17 @@ def self.escape_cmd(executable) # to cmd.exe, whereas this is expected to be passed directly to the Win32 API, anticipating that it # will in turn be interpreted by CommandLineToArgvW. def self.argv_to_commandline(args) - space_chars = [' ', '\t', '\v'] escaped_args = args.map do |arg| + escape_arg(arg) + end + + escaped_args.join(' ') + end + + # + # Escape an individual argument per Windows shell rules + # @param arg [String] Shell argument + def self.escape_arg(arg) needs_quoting = space_chars.any? do |char| arg.include?(char) end @@ -62,9 +74,6 @@ def self.argv_to_commandline(args) arg = '""' if arg == '' arg - end - - 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/file.rb b/lib/msf/core/post/file.rb index c5b0518e6f7a..e392d2d26833 100644 --- a/lib/msf/core/post/file.rb +++ b/lib/msf/core/post/file.rb @@ -981,7 +981,7 @@ def _write_file_unix_shell(file_name, data, append = false) # Short-circuit an empty string. The : builtin is part of posix # standard and should theoretically exist everywhere. if data.empty? - return _shell_command_with_success_code(": #{redirect} #{file_name}") + return _shell_command_with_success_code(": #{redirect} #{session.escape_arg(file_name)}") end d = data.dup @@ -1082,7 +1082,7 @@ def _write_file_unix_shell(file_name, data, append = false) # The first command needs to use the provided redirection for either # appending or truncating. cmd = command.sub('CONTENTS') { chunks.shift } - succeeded = _shell_command_with_success_code("#{cmd} #{redirect} \"#{file_name}\"") + succeeded = _shell_command_with_success_code("#{cmd} #{redirect} #{session.escape_arg(file_name)}") return false unless succeeded # After creating/truncating or appending with the first command, we @@ -1091,7 +1091,7 @@ def _write_file_unix_shell(file_name, data, append = false) vprint_status("Next chunk is #{chunk.length} bytes") cmd = command.sub('CONTENTS') { chunk } - succeeded = _shell_command_with_success_code("#{cmd} >> '#{file_name}'") + succeeded = _shell_command_with_success_code("#{cmd} >> #{session.escape_arg(file_name)}") unless succeeded print_warning("Write partially succeeded then failed. May need to manually clean up #{file_name}") return false From b4da4e74af41f5a224f8f2484aba042888d72b4d Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 15:45:40 +1000 Subject: [PATCH 24/38] Use specific subclass of Command Shell for reverse bash --- modules/payloads/singles/cmd/unix/reverse_bash.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/payloads/singles/cmd/unix/reverse_bash.rb b/modules/payloads/singles/cmd/unix/reverse_bash.rb index bb52610354a8..629ce1d7e460 100644 --- a/modules/payloads/singles/cmd/unix/reverse_bash.rb +++ b/modules/payloads/singles/cmd/unix/reverse_bash.rb @@ -26,7 +26,7 @@ def initialize(info = {}) 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Handler' => Msf::Handler::ReverseTcp, - 'Session' => Msf::Sessions::CommandShell, + 'Session' => Msf::Sessions::CommandShellUnix, 'PayloadType' => 'cmd_bash', 'RequiredCmd' => 'bash-tcp', 'Payload' => From 27e3376fba18709b5c55d66a5b6c0d52e1c6fc89 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 16:20:03 +1000 Subject: [PATCH 25/38] Allow longer acceptance tests - needed for PHP 5.3 --- spec/support/acceptance/child_process.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/acceptance/child_process.rb b/spec/support/acceptance/child_process.rb index cb70f50f3043..93b08845d11e 100644 --- a/spec/support/acceptance/child_process.rb +++ b/spec/support/acceptance/child_process.rb @@ -22,7 +22,7 @@ class ChildProcess def initialize super - @default_timeout = ENV['CI'] ? 120 : 40 + @default_timeout = ENV['CI'] ? 240 : 40 @debug = false @env ||= {} @cmd ||= [] From ac50cede6fe1eee79b6a1d542eaf140974043739 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 30 Sep 2024 20:48:14 +1000 Subject: [PATCH 26/38] Remove unused line --- test/modules/post/test/cmd_exec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 1db7005b8ab0..067978c8dd1c 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -213,7 +213,6 @@ def test_cmd_exec_stderr end def test_create_process - space_filename = 'showargs file' upload_show_args_binary(show_args_binary) upload_show_args_binary(show_args_binary_space) upload_show_args_binary(show_args_binary_special) From 6d12d506ddb7c8fb3206a2d9cbaa9ee832f9cb16 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 1 Oct 2024 15:51:32 +1000 Subject: [PATCH 27/38] Run the Python tests (fixed in payloads repo) --- test/modules/post/test/cmd_exec.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 067978c8dd1c..ac9a05b8a540 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -153,11 +153,7 @@ def test_cmd_exec_quotes it "should return the result of echo with single quotes" do test_string = Rex::Text.rand_text_alpha(4) if session.platform.eql? 'windows' - if session.arch == ARCH_PYTHON - output = cmd_exec("cmd.exe", "/c echo \"#{test_string}\"") - output == test_string - # TODO: Fix this functionality - elsif session.type.eql?('powershell') + if session.type.eql?('powershell') vprint_status("test skipped for Powershell - functionality not correct") true else @@ -173,11 +169,7 @@ def test_cmd_exec_quotes it "should return the result of echo with double quotes" do test_string = Rex::Text.rand_text_alpha(4) if session.platform.eql? 'windows' - if session.platform.eql? 'windows' and session.arch == ARCH_PYTHON - output = cmd_exec("cmd.exe", "/c echo \"#{test_string}\"") - output == test_string - # TODO: Fix this functionality - elsif session.type.eql?('powershell') + if session.type.eql?('powershell') vprint_status("test skipped for Powershell - functionality not correct") true else From 0cf227ff00e655fbe12a100959272b1e7490f362 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 3 Oct 2024 22:00:26 +1000 Subject: [PATCH 28/38] Change API. Support backwards compatibility for PHP --- lib/msf/base/sessions/command_shell_unix.rb | 18 ++----- .../base/sessions/command_shell_windows.rb | 13 ++--- lib/msf/base/sessions/powershell.rb | 13 ++--- lib/msf/core/post/common.rb | 17 ++++--- lib/msf/core/post/file.rb | 2 +- .../session/provider/single_command_shell.rb | 6 +-- .../base/sessions/command_shell_unix_spec.rb | 36 +++++++------- .../sessions/command_shell_windows_spec.rb | 48 +++++++++---------- spec/lib/msf/base/sessions/powershell_spec.rb | 32 ++++++------- 9 files changed, 88 insertions(+), 97 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index a9296d4e62d3..d3e599fd968b 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -11,10 +11,9 @@ def shell_command_token(cmd,timeout = 10) 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) + # @param cmd_and_args [Array] The process path and the arguments to the process + def to_cmd(cmd_and_args) + self.class.to_cmd(cmd_and_args) end # @@ -25,15 +24,8 @@ def escape_arg(arg) 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, or nil if only processing arguments - # @param args [Array] The arguments to the process - def self.to_cmd(executable, args) - if executable.nil? - cmd_and_args = args - else - cmd_and_args = [executable] + args - end - + # @param cmd_and_args [Array] The process path and the arguments to the process + def self.to_cmd(cmd_and_args) escaped = cmd_and_args.map do |arg| escape_arg(arg) end diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index 6bafad69be24..45812122e7e0 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -15,10 +15,9 @@ def shell_command_token(cmd,timeout = 10) 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) + # @param cmd_and_args [Array] The process path and the arguments to the process + def to_cmd(cmd_and_args) + self.class.to_cmd(cmd_and_args) end # Escape a process for the command line @@ -77,9 +76,8 @@ def self.escape_arg(arg) 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) + # @param cmd_and_args [Array] The process path and the arguments to the process + def self.to_cmd(cmd_and_args) # The space, caret and quote chars need to be inside double-quoted strings. # The percent character needs to be escaped using a caret char, while being outside a double-quoted string. # @@ -95,7 +93,6 @@ def self.to_cmd(executable, args) # 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 = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|'] escaped_cmd_and_args = cmd_and_args.map do |arg| diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index fb04e1231164..5b152ba92e9e 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -39,16 +39,14 @@ def shell_command(cmd, timeout = 1800) 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) - self.class.to_cmd(executable, args) + # @param cmd_and_args [Array] The process path and the arguments to the process + def to_cmd(cmd_and_args) + self.class.to_cmd(cmd_and_args) end # Convert the executable and argument array to a command that can be run in this command shell - # @param executable [String] The process to launch - # @param args [Array] The arguments to the process - def self.to_cmd(executable, args) + # @param cmd_and_args [Array] The process path and the arguments to the process + def self.to_cmd(cmd_and_args) # The principle here is that we want to launch a process such that it receives *exactly* what is in `args`. # This means we need to: # - Escape all special characters @@ -59,7 +57,6 @@ def self.to_cmd(executable, args) 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?("'") diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index 0cdcc3f821b2..d647474562e7 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -81,11 +81,16 @@ def create_process(executable, args: [], time_out: 15, opts: {}) }.merge(opts) if session.platform == 'windows' - opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(args) - opts[:legacy_path] = Msf::Sessions::CommandShellWindows.escape_cmd(executable) + if session.arch == 'php' + opts[:legacy_args] = Msf::Sessions::CommandShellWindows.to_cmd(args) + opts[:legacy_path] = Msf::Sessions::CommandShellWindows.to_cmd([executable]) + else + opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(args) + opts[:legacy_path] = Msf::Sessions::CommandShellWindows.escape_cmd(executable) + end else - opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(nil, args) - opts[:legacy_path] = Msf::Sessions::CommandShellUnix.to_cmd(executable, []) + opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(args) + opts[:legacy_path] = Msf::Sessions::CommandShellUnix.to_cmd([executable]) end if opts['Channelized'] @@ -94,11 +99,11 @@ def create_process(executable, args: [], time_out: 15, opts: {}) session.sys.process.execute(executable, args, opts) end when 'powershell' - cmd = session.to_cmd(executable, args) + 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) + cmd = session.to_cmd([executable] + args) o = session.shell_command_token(cmd, time_out) o.chomp! if o end diff --git a/lib/msf/core/post/file.rb b/lib/msf/core/post/file.rb index e392d2d26833..a93711f0e2b5 100644 --- a/lib/msf/core/post/file.rb +++ b/lib/msf/core/post/file.rb @@ -1109,7 +1109,7 @@ def _shell_command_with_success_code(cmd) end def _shell_process_with_success_code(executable, args) - cmd = session.to_cmd(executable, args) + cmd = session.to_cmd([executable] + args) token = "_#{::Rex::Text.rand_text_alpha(32)}" result = session.shell_command_token("#{cmd} && echo #{token}") diff --git a/lib/msf/core/session/provider/single_command_shell.rb b/lib/msf/core/session/provider/single_command_shell.rb index 4a166e48e261..9781f8a10fc0 100644 --- a/lib/msf/core/session/provider/single_command_shell.rb +++ b/lib/msf/core/session/provider/single_command_shell.rb @@ -93,11 +93,11 @@ def shell_command_token(cmd, timeout=10) output end - def to_cmd(cmd, args) + def to_cmd(cmd_and_args) if platform == 'windows' - result = Msf::Sessions::CommandShellWindows.to_cmd(cmd, args) + result = Msf::Sessions::CommandShellWindows.to_cmd(cmd_and_args) else - result = Msf::Sessions::CommandShellUnix.to_cmd(cmd, args) + result = Msf::Sessions::CommandShellUnix.to_cmd(cmd_and_args) end end diff --git a/spec/lib/msf/base/sessions/command_shell_unix_spec.rb b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb index 5bd7b17b8cf0..b0d7c369e6ac 100755 --- a/spec/lib/msf/base/sessions/command_shell_unix_spec.rb +++ b/spec/lib/msf/base/sessions/command_shell_unix_spec.rb @@ -1,39 +1,39 @@ RSpec.describe Msf::Sessions::CommandShellUnix do describe 'to_cmd processing' do it 'should not do anything for simple args' do - expect(described_class.to_cmd('./test', [])).to eq('./test') - expect(described_class.to_cmd('sh', [])).to eq('sh') - expect(described_class.to_cmd('./test', ['basic','args'])).to eq('./test basic args') - expect(described_class.to_cmd(nil, ['basic','args'])).to eq('basic args') + expect(described_class.to_cmd(['./test'] + [])).to eq('./test') + expect(described_class.to_cmd(['sh'] + [])).to eq('sh') + expect(described_class.to_cmd(['./test'] + ['basic','args'])).to eq('./test basic args') + expect(described_class.to_cmd(['basic','args'])).to eq('basic args') end it 'should escape spaces' do - expect(described_class.to_cmd('/home/user/some folder/some program', [])).to eq("'/home/user/some folder/some program'") - expect(described_class.to_cmd('./test', ['with space'])).to eq("./test 'with space'") + 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'") - 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 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 7da786237f0d..6b57e3ecf1d8 100755 --- a/spec/lib/msf/base/sessions/command_shell_windows_spec.rb +++ b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb @@ -2,57 +2,57 @@ 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') + 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"') + 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') + 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') + 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"') + 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^%') + expect(described_class.to_cmd(['test.exe'] + ['%temp%'])).to eq('test.exe ^%temp^%') + expect(described_class.to_cmd(['test.exe'] + ['env', 'var', 'is', '%temp%'])).to eq('test.exe env var is ^%temp^%') end it 'should handle the weird backslash escaping behaviour in front of quotes' do - expect(described_class.to_cmd('test.exe', ['quote\\\\"'])).to eq('test.exe "quote\\\\\\\\"""') - expect(described_class.to_cmd('test.exe', ['will be quoted\\\\'])).to eq('test.exe "will be quoted\\\\\\\\"') - expect(described_class.to_cmd('test.exe', ['will be quoted\\\\ '])).to eq('test.exe "will be quoted\\\\ "') # Should not be doubled up + expect(described_class.to_cmd(['test.exe'] + ['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^%') - 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"') + 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^%') + 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 "" ""') + expect(described_class.to_cmd(['test.exe'] + [''])).to eq('test.exe ""') + expect(described_class.to_cmd(['test.exe'] + ['', ''])).to eq('test.exe "" ""') end end diff --git a/spec/lib/msf/base/sessions/powershell_spec.rb b/spec/lib/msf/base/sessions/powershell_spec.rb index 0c3d5cf8713d..65ba82578c19 100755 --- a/spec/lib/msf/base/sessions/powershell_spec.rb +++ b/spec/lib/msf/base/sessions/powershell_spec.rb @@ -1,52 +1,52 @@ RSpec.describe Msf::Sessions::PowerShell do describe 'to_cmd processing' do it 'should not do anything for simple args' do - expect(described_class.to_cmd(".\\test.exe", ['abc', '123'])).to eq(".\\test.exe abc 123") - expect(described_class.to_cmd("C:\\SysinternalsSuite\\procexp.exe", [])).to eq("C:\\SysinternalsSuite\\procexp.exe") + 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'''") + 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") + 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'") + 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'") + 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'") + 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'") + 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'") + 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'") + 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'") + 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") + 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 6450a8f916afe2688d7e6f9e00682bd2c4b77ced Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 4 Oct 2024 09:15:01 +1000 Subject: [PATCH 29/38] Support backwards compatibility for Python --- lib/msf/core/post/common.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index d647474562e7..b80618892346 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -84,6 +84,12 @@ def create_process(executable, args: [], time_out: 15, opts: {}) if session.arch == 'php' opts[:legacy_args] = Msf::Sessions::CommandShellWindows.to_cmd(args) opts[:legacy_path] = Msf::Sessions::CommandShellWindows.to_cmd([executable]) + elsif session.arch == 'python' + opts[:legacy_path] = executable + # Yes, Unix. Old Python meterp had a bug where it used posix shell splitting + # syntax even on Windows. For backwards-compatibility, we can trick it into + # doing the right thing by using Unix escaping. + opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(args) else opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(args) opts[:legacy_path] = Msf::Sessions::CommandShellWindows.escape_cmd(executable) From 1c4b22028d14ec674b2d2c4737691873d546d70a Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 9 Oct 2024 11:32:51 +1100 Subject: [PATCH 30/38] Comment neatening from code review --- lib/msf/base/sessions/command_shell_unix.rb | 2 -- lib/msf/base/sessions/command_shell_windows.rb | 1 - lib/msf/core/post/common.rb | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/msf/base/sessions/command_shell_unix.rb b/lib/msf/base/sessions/command_shell_unix.rb index d3e599fd968b..16a999357fbd 100644 --- a/lib/msf/base/sessions/command_shell_unix.rb +++ b/lib/msf/base/sessions/command_shell_unix.rb @@ -16,7 +16,6 @@ def to_cmd(cmd_and_args) self.class.to_cmd(cmd_and_args) end - # # Escape an individual argument per Unix shell rules # @param arg [String] Shell argument def escape_arg(arg) @@ -33,7 +32,6 @@ def self.to_cmd(cmd_and_args) escaped.join(' ') end - # # Escape an individual argument per Unix shell rules # @param arg [String] Shell argument def self.escape_arg(arg) diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index 45812122e7e0..2ea49ba92712 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -47,7 +47,6 @@ def self.argv_to_commandline(args) escaped_args.join(' ') end - # # Escape an individual argument per Windows shell rules # @param arg [String] Shell argument def self.escape_arg(arg) diff --git a/lib/msf/core/post/common.rb b/lib/msf/core/post/common.rb index b80618892346..dee213d649b3 100644 --- a/lib/msf/core/post/common.rb +++ b/lib/msf/core/post/common.rb @@ -61,7 +61,7 @@ def peer # @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 Desktop [Boolean] Run on meterpreter's current desktop # @option Session [Integer] Execute process in a given session as the session user # @option Subshell [Boolean] Execute process in a subshell # @option Pty [Boolean] Execute process in a pty (if available) From 1b169efe3d159e230ac032af9e656819b342a610 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 14 Oct 2024 15:27:15 +1100 Subject: [PATCH 31/38] Update payload dependencies --- Gemfile.lock | 8 ++++---- LICENSE_GEMS | 4 ++-- metasploit-framework.gemspec | 4 ++-- modules/payloads/singles/php/meterpreter_reverse_tcp.rb | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d91b47f92c05..e2d2e7fda82f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,9 +41,9 @@ PATH metasploit-concern metasploit-credential metasploit-model - metasploit-payloads (= 2.0.175) + metasploit-payloads (= 2.0.178) metasploit_data_models - metasploit_payloads-mettle (= 1.0.31) + metasploit_payloads-mettle (= 1.0.32) mqtt msgpack (~> 1.6.0) mutex_m @@ -295,7 +295,7 @@ GEM activemodel (~> 7.0) activesupport (~> 7.0) railties (~> 7.0) - metasploit-payloads (2.0.175) + metasploit-payloads (2.0.178) metasploit_data_models (6.0.3) activerecord (~> 7.0) activesupport (~> 7.0) @@ -306,7 +306,7 @@ GEM railties (~> 7.0) recog webrick - metasploit_payloads-mettle (1.0.31) + metasploit_payloads-mettle (1.0.32) method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) diff --git a/LICENSE_GEMS b/LICENSE_GEMS index 63ab30283a64..ad4c9e829420 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -88,9 +88,9 @@ metasploit-concern, 5.0.2, "New BSD" metasploit-credential, 6.0.9, "New BSD" metasploit-framework, 6.4.31, "New BSD" metasploit-model, 5.0.2, "New BSD" -metasploit-payloads, 2.0.175, "3-clause (or ""modified"") BSD" +metasploit-payloads, 2.0.178, "3-clause (or ""modified"") BSD" metasploit_data_models, 6.0.3, "New BSD" -metasploit_payloads-mettle, 1.0.31, "3-clause (or ""modified"") BSD" +metasploit_payloads-mettle, 1.0.32, "3-clause (or ""modified"") BSD" method_source, 1.1.0, MIT mime-types, 3.5.2, MIT mime-types-data, 3.2024.0604, MIT diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index 2c8807bd8a5e..5b0802c0f068 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -74,9 +74,9 @@ Gem::Specification.new do |spec| # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' # Needed for Meterpreter - spec.add_runtime_dependency 'metasploit-payloads', '2.0.175' + spec.add_runtime_dependency 'metasploit-payloads', '2.0.178' # Needed for the next-generation POSIX Meterpreter - spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.31' + spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.32' # Needed by msfgui and other rpc components # Locked until build env can handle newer version. See: https://github.com/msgpack/msgpack-ruby/issues/334 spec.add_runtime_dependency 'msgpack', '~> 1.6.0' diff --git a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb index bfd845d4c8db..9604781ea0bb 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ module MetasploitModule - CachedSize = 34854 + CachedSize = 34928 include Msf::Payload::Single include Msf::Payload::Php::ReverseTcp From 9b4cd2241d26f31d2960954e770485c6433f8a9a Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 14 Oct 2024 15:43:40 +1100 Subject: [PATCH 32/38] Update payload sizes --- .../payloads/singles/linux/armbe/meterpreter_reverse_http.rb | 2 +- .../payloads/singles/linux/armbe/meterpreter_reverse_https.rb | 2 +- modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb | 2 +- .../payloads/singles/linux/armle/meterpreter_reverse_http.rb | 2 +- .../payloads/singles/linux/armle/meterpreter_reverse_https.rb | 2 +- modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb | 2 +- .../payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb | 2 +- .../payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb | 2 +- .../payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb | 2 +- .../payloads/singles/linux/mipsle/meterpreter_reverse_http.rb | 2 +- .../payloads/singles/linux/mipsle/meterpreter_reverse_https.rb | 2 +- .../payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb | 2 +- .../payloads/singles/osx/aarch64/meterpreter_reverse_http.rb | 2 +- .../payloads/singles/osx/aarch64/meterpreter_reverse_https.rb | 2 +- modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb index 433e65f6c574..0dc97e89ba39 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_http.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061712 + CachedSize = 1061912 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb index eac837ca4244..4c9590fe1a76 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_https.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061712 + CachedSize = 1061912 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb index 82b91394366f..f0d1bf1f7de6 100644 --- a/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/armbe/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061712 + CachedSize = 1061912 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb index 3e47e4126c2b..574c89da4360 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_http.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061884 + CachedSize = 1062084 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb index 260dfc4a3ac3..95bc3d92c6ca 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_https.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061884 + CachedSize = 1062084 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb index 847692dba105..8ed41f2c6383 100644 --- a/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/armle/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1061884 + CachedSize = 1062084 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb index 133f739b72f6..7b9b1fe8accd 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_http.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1516268 + CachedSize = 1516524 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb index 473b904852c0..0904600aad2a 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_https.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1516268 + CachedSize = 1516524 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb index 59182fe52389..f25c282a9f89 100644 --- a/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mipsbe/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1516268 + CachedSize = 1516524 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb index 3fce57d34557..0297a16cae6c 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_http.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1519288 + CachedSize = 1519544 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb index f897a77ab2c5..358e856c5482 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_https.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1519288 + CachedSize = 1519544 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb index f7adc9d8085d..35363334175d 100644 --- a/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/linux/mipsle/meterpreter_reverse_tcp.rb @@ -7,7 +7,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 1519288 + CachedSize = 1519544 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_http.rb b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_http.rb index b8b74b349c34..30693a1ea2fd 100644 --- a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_http.rb @@ -5,7 +5,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 813091 + CachedSize = 813075 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_https.rb b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_https.rb index cc7d9e3338d3..b6e266225523 100644 --- a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_https.rb @@ -5,7 +5,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 813091 + CachedSize = 813075 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions diff --git a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb index bffb6920916e..9f1737fa744a 100644 --- a/modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb +++ b/modules/payloads/singles/osx/aarch64/meterpreter_reverse_tcp.rb @@ -5,7 +5,7 @@ # Module generated by tools/modules/generate_mettle_payloads.rb module MetasploitModule - CachedSize = 813091 + CachedSize = 813075 include Msf::Payload::Single include Msf::Sessions::MeterpreterOptions From 7890595dd9acb2c9e2257d441bdb3bc3807a9682 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 15 Oct 2024 21:21:01 +1100 Subject: [PATCH 33/38] Add one more annoying test case --- test/modules/post/test/cmd_exec.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index ac9a05b8a540..41a38a3e06bd 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -236,6 +236,11 @@ def test_create_process valid_show_args_response?(output, expected: [show_args_binary[:upload_path], "it's $PATH"]) end + it 'should deal with weird windows edge cases' do + output = create_process(show_args_binary[:cmd], args: ['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\ ', 'test words\\\\\\ ', '\\\\']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\ ', 'test words\\\\\\ ', '\\\\']) + end + it 'should accept special characters and return the create_process output' do output = create_process(show_args_binary[:cmd], args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '~!@#$%^&*(){`1234567890[]",.\'<>']) From bdfa1f3a3f02930daf21b9cf82044f016745e4b9 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 15 Oct 2024 23:43:17 +1100 Subject: [PATCH 34/38] Update metasploit-payloads gem to 2.0.180 --- Gemfile.lock | 4 ++-- LICENSE_GEMS | 2 +- metasploit-framework.gemspec | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e2d2e7fda82f..50fa946f0b32 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,7 +41,7 @@ PATH metasploit-concern metasploit-credential metasploit-model - metasploit-payloads (= 2.0.178) + metasploit-payloads (= 2.0.180) metasploit_data_models metasploit_payloads-mettle (= 1.0.32) mqtt @@ -295,7 +295,7 @@ GEM activemodel (~> 7.0) activesupport (~> 7.0) railties (~> 7.0) - metasploit-payloads (2.0.178) + metasploit-payloads (2.0.180) metasploit_data_models (6.0.3) activerecord (~> 7.0) activesupport (~> 7.0) diff --git a/LICENSE_GEMS b/LICENSE_GEMS index ad4c9e829420..f3983a3918f3 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -88,7 +88,7 @@ metasploit-concern, 5.0.2, "New BSD" metasploit-credential, 6.0.9, "New BSD" metasploit-framework, 6.4.31, "New BSD" metasploit-model, 5.0.2, "New BSD" -metasploit-payloads, 2.0.178, "3-clause (or ""modified"") BSD" +metasploit-payloads, 2.0.180, "3-clause (or ""modified"") BSD" metasploit_data_models, 6.0.3, "New BSD" metasploit_payloads-mettle, 1.0.32, "3-clause (or ""modified"") BSD" method_source, 1.1.0, MIT diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index 5b0802c0f068..0753c92daf7e 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -74,7 +74,7 @@ Gem::Specification.new do |spec| # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' # Needed for Meterpreter - spec.add_runtime_dependency 'metasploit-payloads', '2.0.178' + spec.add_runtime_dependency 'metasploit-payloads', '2.0.180' # Needed for the next-generation POSIX Meterpreter spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.32' # Needed by msfgui and other rpc components From 205adfe2fded6e55a166b01a7c7d49953e8d9766 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 16 Oct 2024 10:13:16 +1100 Subject: [PATCH 35/38] Handle edge case in command shell when input contains backslash-quote combination already --- lib/msf/base/sessions/command_shell.rb | 4 ++-- lib/msf/base/sessions/command_shell_windows.rb | 5 +++-- spec/lib/msf/base/sessions/command_shell_windows_spec.rb | 2 ++ test/modules/post/test/cmd_exec.rb | 8 ++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/msf/base/sessions/command_shell.rb b/lib/msf/base/sessions/command_shell.rb index fb399994f29e..bb9b04472aa7 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -763,14 +763,14 @@ def self._glue_cmdline_escape(arg, quote_requiring, unquotable_char, escaped_unq 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 + # Let's note that, for 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 + # The final token has been in an inside-quote context, so let's properly wrap that before continuing current_token = "#{quote_char}#{current_token}#{quote_char}" end result += current_token diff --git a/lib/msf/base/sessions/command_shell_windows.rb b/lib/msf/base/sessions/command_shell_windows.rb index 2ea49ba92712..a4397f96e4db 100644 --- a/lib/msf/base/sessions/command_shell_windows.rb +++ b/lib/msf/base/sessions/command_shell_windows.rb @@ -95,8 +95,9 @@ def self.to_cmd(cmd_and_args) quote_requiring = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|'] escaped_cmd_and_args = cmd_and_args.map do |arg| - # Double-up all quote chars - arg = arg.gsub('"', '""') + # Escape quote chars by doubling them up, except those preceeded by a backslash (which are already effectively escaped, and handled below) + arg = arg.gsub(/([^\\])"/, '\\1""') + arg = arg.gsub(/^"/, '""') result = CommandShell._glue_cmdline_escape(arg, quote_requiring, '%', '^%', '"') 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 6b57e3ecf1d8..6e0de2c9edae 100755 --- a/spec/lib/msf/base/sessions/command_shell_windows_spec.rb +++ b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb @@ -37,6 +37,7 @@ expect(described_class.to_cmd(['test.exe'] + ['quote\\\\"'])).to eq('test.exe "quote\\\\\\\\"""') expect(described_class.to_cmd(['test.exe'] + ['will be quoted\\\\'])).to eq('test.exe "will be quoted\\\\\\\\"') expect(described_class.to_cmd(['test.exe'] + ['will be quoted\\\\ '])).to eq('test.exe "will be quoted\\\\ "') # Should not be doubled up + expect(described_class.to_cmd(['test.exe'] + ['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\'])).to eq('test.exe """test""" "test\\\\"" "test\\\\\\\\"" "test words\\\\\\\\\\\\\\\\" "test words\\\\\\\\\\\\" \\\\') end it 'should handle combinations of quoting and percent-escaping' do @@ -76,6 +77,7 @@ it 'should handle the weird backslash escaping behaviour in front of quotes' do expect(described_class.argv_to_commandline(['\\\\"'])).to eq('\\\\\\\\\\"') expect(described_class.argv_to_commandline(['space \\\\'])).to eq('"space \\\\\\\\"') + expect(described_class.argv_to_commandline(['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\'])).to eq('\"test\" test\\\\\\" test\\\\\\\\\\" "test words\\\\\\\\\\\\\\\\" "test words\\\\\\\\\\\\" \\\\') end it 'should handle empty args' do diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 41a38a3e06bd..23748780c465 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -237,13 +237,13 @@ def test_create_process end it 'should deal with weird windows edge cases' do - output = create_process(show_args_binary[:cmd], args: ['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\ ', 'test words\\\\\\ ', '\\\\']) - valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\ ', 'test words\\\\\\ ', '\\\\']) + output = create_process(show_args_binary[:cmd], args: ['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\']) end it 'should accept special characters and return the create_process output' do - output = create_process(show_args_binary[:cmd], args: ['~!@#$%^&*(){`1234567890[]",.\'<>']) - valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '~!@#$%^&*(){`1234567890[]",.\'<>']) + output = create_process(show_args_binary[:cmd], args: ['~!@#$%^&*(){`1234567890[]",.\'<>\\']) + valid_show_args_response?(output, expected: [show_args_binary[:upload_path], '~!@#$%^&*(){`1234567890[]",.\'<>\\']) end it 'should accept command line commands and return the create_process output' do From 9972587fefb85c1a998537cc376e27a75b855013 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 16 Oct 2024 15:46:35 +1100 Subject: [PATCH 36/38] Handle weird PowerShell edge case --- lib/msf/base/sessions/powershell.rb | 11 +++++++++++ .../msf/base/sessions/command_shell_windows_spec.rb | 2 +- test/modules/post/test/cmd_exec.rb | 4 ++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/msf/base/sessions/powershell.rb b/lib/msf/base/sessions/powershell.rb index 5b152ba92e9e..3bde436f5c29 100644 --- a/lib/msf/base/sessions/powershell.rb +++ b/lib/msf/base/sessions/powershell.rb @@ -86,6 +86,17 @@ def self.to_cmd(cmd_and_args) needs_single_quoting = true end + will_be_double_quoted_by_powershell = [' ', '\t', '\v'].any? do |bad_char| + arg.include?(bad_char) + end + + if will_be_double_quoted_by_powershell + # This is horrible, and I'm so so sorry. + # If an argument ends with a series of backslashes, and it will be quoted by PowerShell when *it* launches the process (e.g. because the arg contains a space), + # PowerShell will not correctly handle backslashes immediately preceeding the quote that it *itself* adds. So we need to be responsible for this. + arg = arg.gsub(/(\\*)$/, '\\1\\1') + end + if needs_single_quoting arg = "'#{arg}'" end 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 6e0de2c9edae..917e32253a5d 100755 --- a/spec/lib/msf/base/sessions/command_shell_windows_spec.rb +++ b/spec/lib/msf/base/sessions/command_shell_windows_spec.rb @@ -34,7 +34,7 @@ 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'] + ['quote\\\\"'])).to eq('test.exe "quote\\\\\\\\""') expect(described_class.to_cmd(['test.exe'] + ['will be quoted\\\\'])).to eq('test.exe "will be quoted\\\\\\\\"') expect(described_class.to_cmd(['test.exe'] + ['will be quoted\\\\ '])).to eq('test.exe "will be quoted\\\\ "') # Should not be doubled up expect(described_class.to_cmd(['test.exe'] + ['"test"', 'test\\"', 'test\\\\"', 'test words\\\\\\\\', 'test words\\\\\\', '\\\\'])).to eq('test.exe """test""" "test\\\\"" "test\\\\\\\\"" "test words\\\\\\\\\\\\\\\\" "test words\\\\\\\\\\\\" \\\\') diff --git a/test/modules/post/test/cmd_exec.rb b/test/modules/post/test/cmd_exec.rb index 23748780c465..b34e25be5e30 100644 --- a/test/modules/post/test/cmd_exec.rb +++ b/test/modules/post/test/cmd_exec.rb @@ -84,6 +84,10 @@ def valid_show_args_response?(output, expected:) # Match the binary name, to support the binary name containing relative or absolute paths, i.e. # "show_args.exe\r\none\r\ntwo", + if output_binary.nil? + vprint_status("#{__method__}: Malformed output: no process binary returned") + return false + end match = output_binary.include?(expected[0]) && output_args == expected[1..] if !match vprint_status("#{__method__}: expected: #{expected.inspect} - actual: #{output_lines.inspect}") From 197595659e8fc12a7683bef568ace7387c4282fd Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 16 Oct 2024 16:53:53 +1100 Subject: [PATCH 37/38] Better timeout for PHP 5.3 tests, which apparently take forever --- spec/support/acceptance/child_process.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/acceptance/child_process.rb b/spec/support/acceptance/child_process.rb index 93b08845d11e..ee7b4e16ee82 100644 --- a/spec/support/acceptance/child_process.rb +++ b/spec/support/acceptance/child_process.rb @@ -22,7 +22,7 @@ class ChildProcess def initialize super - @default_timeout = ENV['CI'] ? 240 : 40 + @default_timeout = ENV['CI'] ? 480 : 40 @debug = false @env ||= {} @cmd ||= [] From 94d72b2b8b6a660326257121bae503dbd45da091 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 17 Oct 2024 07:01:00 +1100 Subject: [PATCH 38/38] Update metasploit-payloads gem to 2.0.183 --- Gemfile.lock | 4 ++-- LICENSE_GEMS | 2 +- metasploit-framework.gemspec | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 50fa946f0b32..fdfc0f4102a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,7 +41,7 @@ PATH metasploit-concern metasploit-credential metasploit-model - metasploit-payloads (= 2.0.180) + metasploit-payloads (= 2.0.183) metasploit_data_models metasploit_payloads-mettle (= 1.0.32) mqtt @@ -295,7 +295,7 @@ GEM activemodel (~> 7.0) activesupport (~> 7.0) railties (~> 7.0) - metasploit-payloads (2.0.180) + metasploit-payloads (2.0.183) metasploit_data_models (6.0.3) activerecord (~> 7.0) activesupport (~> 7.0) diff --git a/LICENSE_GEMS b/LICENSE_GEMS index f3983a3918f3..703c6c2ed60a 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -88,7 +88,7 @@ metasploit-concern, 5.0.2, "New BSD" metasploit-credential, 6.0.9, "New BSD" metasploit-framework, 6.4.31, "New BSD" metasploit-model, 5.0.2, "New BSD" -metasploit-payloads, 2.0.180, "3-clause (or ""modified"") BSD" +metasploit-payloads, 2.0.183, "3-clause (or ""modified"") BSD" metasploit_data_models, 6.0.3, "New BSD" metasploit_payloads-mettle, 1.0.32, "3-clause (or ""modified"") BSD" method_source, 1.1.0, MIT diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index 0753c92daf7e..4a98d06f31e3 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -74,7 +74,7 @@ Gem::Specification.new do |spec| # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' # Needed for Meterpreter - spec.add_runtime_dependency 'metasploit-payloads', '2.0.180' + spec.add_runtime_dependency 'metasploit-payloads', '2.0.183' # Needed for the next-generation POSIX Meterpreter spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.32' # Needed by msfgui and other rpc components