Skip to content

Commit 7a1892e

Browse files
authored
Land rapid7#19745, applying argument escaping to other shells
Apply escaping args to other command shells
2 parents d626886 + fa4dd1d commit 7a1892e

File tree

7 files changed

+149
-146
lines changed

7 files changed

+149
-146
lines changed

lib/metasploit/framework/ssh/platform.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ def self.get_platform_info(ssh_socket, timeout: 10)
9494
info
9595
end
9696

97+
def self.is_posix(platform)
98+
return ['unifi','linux','osx','solaris','bsd','hpux','aix'].include?(platform)
99+
end
100+
97101
def self.get_platform_from_info(info)
98102
case info
99103
when /unifi\.version|UniFiSecurityGateway/i # Ubiquiti Unifi. uname -a is left in, so we got to pull before Linux

lib/msf/base/sessions/command_shell.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ def cmd_background_help
215215
print_line
216216
end
217217

218+
def escape_arg(arg)
219+
# By default we don't know what the escaping is. It's not ideal, but subclasses should do their own appropriate escaping
220+
arg
221+
end
222+
218223
def cmd_background(*args)
219224
if !args.empty?
220225
# We assume that background does not need arguments

lib/msf/base/sessions/command_shell_unix.rb

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,8 @@ def initialize(*args)
66
super
77
end
88

9-
def shell_command_token(cmd,timeout = 10)
10-
shell_command_token_unix(cmd,timeout)
11-
end
12-
13-
# Convert the executable and argument array to a command that can be run in this command shell
14-
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
15-
def to_cmd(cmd_and_args)
16-
self.class.to_cmd(cmd_and_args)
17-
end
18-
19-
# Escape an individual argument per Unix shell rules
20-
# @param arg [String] Shell argument
21-
def escape_arg(arg)
22-
self.class.escape_arg(arg)
23-
end
24-
25-
# Convert the executable and argument array to a command that can be run in this command shell
26-
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
27-
def self.to_cmd(cmd_and_args)
28-
escaped = cmd_and_args.map do |arg|
29-
escape_arg(arg)
30-
end
31-
32-
escaped.join(' ')
33-
end
34-
35-
# Escape an individual argument per Unix shell rules
36-
# @param arg [String] Shell argument
37-
def self.escape_arg(arg)
38-
quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';']
39-
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'")
40-
if result == ''
41-
result = "''"
42-
end
43-
44-
result
45-
end
9+
include Msf::Sessions::UnixEscaping
10+
extend Msf::Sessions::UnixEscaping
4611
end
4712

4813
end

lib/msf/base/sessions/command_shell_windows.rb

Lines changed: 2 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -6,114 +6,7 @@ def initialize(*args)
66
super
77
end
88

9-
def self.space_chars
10-
[' ', '\t', '\v']
11-
end
12-
13-
def shell_command_token(cmd,timeout = 10)
14-
shell_command_token_win32(cmd,timeout)
15-
end
16-
17-
# Convert the executable and argument array to a command that can be run in this command shell
18-
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
19-
def to_cmd(cmd_and_args)
20-
self.class.to_cmd(cmd_and_args)
21-
end
22-
23-
# Escape a process for the command line
24-
# @param executable [String] The process to launch
25-
def self.escape_cmd(executable)
26-
needs_quoting = space_chars.any? do |char|
27-
executable.include?(char)
28-
end
29-
30-
if needs_quoting
31-
executable = "\"#{executable}\""
32-
end
33-
34-
executable
35-
end
36-
37-
# Convert the executable and argument array to a commandline that can be passed to CreateProcessAsUserW.
38-
# @param args [Array<String>] The arguments to the process
39-
# @remark The difference between this and `to_cmd` is that the output of `to_cmd` is expected to be passed
40-
# to cmd.exe, whereas this is expected to be passed directly to the Win32 API, anticipating that it
41-
# will in turn be interpreted by CommandLineToArgvW.
42-
def self.argv_to_commandline(args)
43-
escaped_args = args.map do |arg|
44-
escape_arg(arg)
45-
end
46-
47-
escaped_args.join(' ')
48-
end
49-
50-
# Escape an individual argument per Windows shell rules
51-
# @param arg [String] Shell argument
52-
def self.escape_arg(arg)
53-
needs_quoting = space_chars.any? do |char|
54-
arg.include?(char)
55-
end
56-
57-
# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
58-
# We need to send double the number of backslashes to make it work as expected
59-
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
60-
arg = arg.gsub(/(\\*)"/, '\\1\\1"')
61-
62-
# Quotes need to be escaped
63-
arg = arg.gsub('"', '\\"')
64-
65-
if needs_quoting
66-
# At the end of the argument, we're about to add another quote - so any backslashes need to be doubled here too
67-
arg = arg.gsub(/(\\*)$/, '\\1\\1')
68-
arg = "\"#{arg}\""
69-
end
70-
71-
# Empty string needs to be coerced to have a value
72-
arg = '""' if arg == ''
73-
74-
arg
75-
end
76-
77-
# Convert the executable and argument array to a command that can be run in this command shell
78-
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
79-
def self.to_cmd(cmd_and_args)
80-
# The space, caret and quote chars need to be inside double-quoted strings.
81-
# The percent character needs to be escaped using a caret char, while being outside a double-quoted string.
82-
#
83-
# Situations where these two situations combine are going to be the trickiest cases: something that has quote-requiring
84-
# characters (e.g. spaces), but which also needs to avoid expanding an environment variable. In this case,
85-
# the string needs to end up being partially quoted; with parts of the string in quotes, but others (i.e. bits with percents) not.
86-
# For example:
87-
# 'env var is %temp%, yes, %TEMP%' needs to end up as '"env var is "^%temp^%", yes, "^%TEMP^%'
88-
#
89-
# There is flexibility in how you might implement this, but I think this one looks the most "human" to me,
90-
# which would make it less signaturable.
91-
#
92-
# To do this, we'll consider each argument character-by-character. Each time we encounter a percent sign, we break out of any quotes
93-
# (if we've been inside them in the current "token"), and then start a new "token".
94-
95-
quote_requiring = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|']
96-
97-
escaped_cmd_and_args = cmd_and_args.map do |arg|
98-
# Escape quote chars by doubling them up, except those preceeded by a backslash (which are already effectively escaped, and handled below)
99-
arg = arg.gsub(/([^\\])"/, '\\1""')
100-
arg = arg.gsub(/^"/, '""')
101-
102-
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, '%', '^%', '"')
103-
104-
# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
105-
# We need to send double the number of backslashes to make it work as expected
106-
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
107-
result.gsub!(/(\\*)"/, '\\1\\1"')
108-
109-
# Empty string needs to be coerced to have a value
110-
result = '""' if result == ''
111-
112-
result
113-
end
114-
115-
escaped_cmd_and_args.join(' ')
116-
end
9+
include Msf::Sessions::WindowsEscaping
10+
extend Msf::Sessions::WindowsEscaping
11711
end
118-
11912
end

lib/msf/base/sessions/ssh_command_shell_bind.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ def initialize(ssh_connection, opts = {})
238238
def bootstrap(datastore = {}, handler = nil)
239239
# this won't work after the rstream is initialized, so do it first
240240
@platform = Metasploit::Framework::Ssh::Platform.get_platform(ssh_connection)
241+
if @platform == 'windows'
242+
extend(Msf::Sessions::WindowsEscaping)
243+
elsif Metasploit::Framework::Ssh::Platform.is_posix(@platform)
244+
extend(Msf::Sessions::UnixEscaping)
245+
else
246+
raise ::Net::SSH::Exception.new("Unknown platform: #{platform}")
247+
end
241248

242249
# if the platform is known, it was recovered by communicating with the device, so skip verification, also not all
243250
# shells accessed through SSH may respond to the echo command issued for verification as expected
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module Msf::Sessions
2+
module UnixEscaping
3+
def shell_command_token(cmd,timeout = 10)
4+
shell_command_token_unix(cmd,timeout)
5+
end
6+
7+
# Convert the executable and argument array to a command that can be run in this command shell
8+
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
9+
def to_cmd(cmd_and_args)
10+
escaped = cmd_and_args.map { |arg| escape_arg(arg) }
11+
12+
escaped.join(' ')
13+
end
14+
15+
# Escape an individual argument per Unix shell rules
16+
# @param arg [String] Shell argument
17+
def escape_arg(arg)
18+
quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';']
19+
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'")
20+
if result == ''
21+
result = "''"
22+
end
23+
24+
result
25+
end
26+
end
27+
end
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
module Msf::Sessions
2+
module WindowsEscaping
3+
def space_chars
4+
[' ', '\t', '\v']
5+
end
6+
7+
def shell_command_token(cmd,timeout = 10)
8+
shell_command_token_win32(cmd,timeout)
9+
end
10+
11+
# Escape a process for the command line
12+
# @param executable [String] The process to launch
13+
def escape_cmd(executable)
14+
needs_quoting = space_chars.any? do |char|
15+
executable.include?(char)
16+
end
17+
18+
if needs_quoting
19+
executable = "\"#{executable}\""
20+
end
21+
22+
executable
23+
end
24+
25+
# Convert the executable and argument array to a commandline that can be passed to CreateProcessAsUserW.
26+
# @param args [Array<String>] The arguments to the process
27+
# @remark The difference between this and `to_cmd` is that the output of `to_cmd` is expected to be passed
28+
# to cmd.exe, whereas this is expected to be passed directly to the Win32 API, anticipating that it
29+
# will in turn be interpreted by CommandLineToArgvW.
30+
def argv_to_commandline(args)
31+
escaped_args = args.map { |arg| escape_arg(arg) }
32+
33+
escaped_args.join(' ')
34+
end
35+
36+
# Escape an individual argument per Windows shell rules
37+
# @param arg [String] Shell argument
38+
def escape_arg(arg)
39+
needs_quoting = space_chars.any? { |char| arg.include?(char) }
40+
41+
# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
42+
# We need to send double the number of backslashes to make it work as expected
43+
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
44+
arg = arg.gsub(/(\\*)"/, '\\1\\1"')
45+
46+
# Quotes need to be escaped
47+
arg = arg.gsub('"', '\\"')
48+
49+
if needs_quoting
50+
# At the end of the argument, we're about to add another quote - so any backslashes need to be doubled here too
51+
arg = arg.gsub(/(\\*)$/, '\\1\\1')
52+
arg = "\"#{arg}\""
53+
end
54+
55+
# Empty string needs to be coerced to have a value
56+
arg = '""' if arg == ''
57+
58+
arg
59+
end
60+
61+
# Convert the executable and argument array to a command that can be run in this command shell
62+
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
63+
def to_cmd(cmd_and_args)
64+
# The space, caret and quote chars need to be inside double-quoted strings.
65+
# The percent character needs to be escaped using a caret char, while being outside a double-quoted string.
66+
#
67+
# Situations where these two situations combine are going to be the trickiest cases: something that has quote-requiring
68+
# characters (e.g. spaces), but which also needs to avoid expanding an environment variable. In this case,
69+
# the string needs to end up being partially quoted; with parts of the string in quotes, but others (i.e. bits with percents) not.
70+
# For example:
71+
# 'env var is %temp%, yes, %TEMP%' needs to end up as '"env var is "^%temp^%", yes, "^%TEMP^%'
72+
#
73+
# There is flexibility in how you might implement this, but I think this one looks the most "human" to me,
74+
# which would make it less signaturable.
75+
#
76+
# To do this, we'll consider each argument character-by-character. Each time we encounter a percent sign, we break out of any quotes
77+
# (if we've been inside them in the current "token"), and then start a new "token".
78+
79+
quote_requiring = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|']
80+
81+
escaped_cmd_and_args = cmd_and_args.map do |arg|
82+
# Escape quote chars by doubling them up, except those preceeded by a backslash (which are already effectively escaped, and handled below)
83+
arg = arg.gsub(/([^\\])"/, '\\1""')
84+
arg = arg.gsub(/^"/, '""')
85+
86+
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, '%', '^%', '"')
87+
88+
# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
89+
# We need to send double the number of backslashes to make it work as expected
90+
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
91+
result.gsub!(/(\\*)"/, '\\1\\1"')
92+
93+
# Empty string needs to be coerced to have a value
94+
result = '""' if result == ''
95+
96+
result
97+
end
98+
99+
escaped_cmd_and_args.join(' ')
100+
end
101+
end
102+
end

0 commit comments

Comments
 (0)