Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New process launch API #19108

Merged
merged 38 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
955c675
Implement new cmd_exec API for PowerShell
smashery Apr 9, 2024
72e657a
Implement new cmd_exec API for Windows cmd
smashery Apr 9, 2024
e0aca71
Add unix shell to create_process API
smashery Apr 9, 2024
e9f86c4
Reworked unix create_process, as it was buggy
smashery Apr 10, 2024
0d4d6f3
create_process works for basic CommandShell instances
smashery Apr 12, 2024
85d019c
Handle CommandLineToArgv behaviour
smashery Apr 14, 2024
658c9fc
Comment function
smashery Apr 15, 2024
fe61e46
Changes from code review
smashery Apr 18, 2024
7d30c67
Fix error sending legacy args
smashery Apr 18, 2024
5d71aa2
Treat old-style path separately to new (unescaped) path
smashery Apr 18, 2024
d9ed8ec
Rework unix command line based on testing
smashery Apr 18, 2024
0ab16ae
Fix bug when no arguments are present
smashery Apr 18, 2024
880203b
Remove accidentally committed changes
smashery Apr 18, 2024
a69b777
Included tests for create_process API
smashery Sep 24, 2024
593d06e
Tests working on Windows 10 meterp
smashery Sep 25, 2024
602506b
Updated for PHP and Python
smashery Sep 25, 2024
ec4e944
Fix file upload in PowerShell
smashery Sep 25, 2024
6fc714c
Take stderr tests back out for now
smashery Sep 29, 2024
7a5471a
Fix bug in chmod for Java meterp
smashery Sep 30, 2024
75157f8
Fix test case for java on Windows
smashery Sep 30, 2024
2a8924e
Fix shell file upload when filename has interesting characters
smashery Sep 30, 2024
a32a302
Fix issue with windows command shells
smashery Sep 30, 2024
c543971
Support uploading files on linux shell containing quote characters
smashery Sep 30, 2024
b4da4e7
Use specific subclass of Command Shell for reverse bash
smashery Sep 30, 2024
27e3376
Allow longer acceptance tests - needed for PHP 5.3
smashery Sep 30, 2024
ac50ced
Remove unused line
smashery Sep 30, 2024
6d12d50
Run the Python tests (fixed in payloads repo)
smashery Oct 1, 2024
0cf227f
Change API. Support backwards compatibility for PHP
smashery Oct 3, 2024
6450a8f
Support backwards compatibility for Python
smashery Oct 3, 2024
1c4b220
Comment neatening from code review
smashery Oct 9, 2024
1b169ef
Update payload dependencies
smashery Oct 14, 2024
9b4cd22
Update payload sizes
smashery Oct 14, 2024
7890595
Add one more annoying test case
smashery Oct 15, 2024
bdfa1f3
Update metasploit-payloads gem to 2.0.180
smashery Oct 15, 2024
205adfe
Handle edge case in command shell when input contains backslash-quote…
smashery Oct 15, 2024
9972587
Handle weird PowerShell edge case
smashery Oct 16, 2024
1975956
Better timeout for PHP 5.3 tests, which apparently take forever
smashery Oct 16, 2024
94d72b2
Update metasploit-payloads gem to 2.0.183
smashery Oct 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ PATH
metasploit-concern
metasploit-credential
metasploit-model
metasploit-payloads (= 2.0.175)
metasploit-payloads (= 2.0.183)
metasploit_data_models
metasploit_payloads-mettle (= 1.0.31)
metasploit_payloads-mettle (= 1.0.32)
mqtt
msgpack (~> 1.6.0)
mutex_m
Expand Down Expand Up @@ -295,7 +295,7 @@ GEM
activemodel (~> 7.0)
activesupport (~> 7.0)
railties (~> 7.0)
metasploit-payloads (2.0.175)
metasploit-payloads (2.0.183)
metasploit_data_models (6.0.3)
activerecord (~> 7.0)
activesupport (~> 7.0)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions LICENSE_GEMS
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ metasploit-concern, 5.0.2, "New BSD"
metasploit-credential, 6.0.9, "New BSD"
metasploit-framework, 6.4.31, "New BSD"
metasploit-model, 5.0.2, "New BSD"
metasploit-payloads, 2.0.175, "3-clause (or ""modified"") BSD"
metasploit-payloads, 2.0.183, "3-clause (or ""modified"") BSD"
metasploit_data_models, 6.0.3, "New BSD"
metasploit_payloads-mettle, 1.0.31, "3-clause (or ""modified"") BSD"
metasploit_payloads-mettle, 1.0.32, "3-clause (or ""modified"") BSD"
method_source, 1.1.0, MIT
mime-types, 3.5.2, MIT
mime-types-data, 3.2024.0604, MIT
Expand Down
43 changes: 43 additions & 0 deletions lib/msf/base/sessions/command_shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>] The chars that can successfully be escaped inside quotes
# @param unquotable_char [String] The character that can't exist inside quotes
# @param escaped_unquotable_char [String] The escaped form of unquotable_char
# @param quote_char [String] The char used for quoting
def self._glue_cmdline_escape(arg, quote_requiring, unquotable_char, escaped_unquotable_char, quote_char)
current_token = ""
result = ""
in_quotes = false

arg.each_char do |char|
if char == unquotable_char
if in_quotes
# This token has been in an inside-quote context, so let's properly wrap that before continuing
current_token = "#{quote_char}#{current_token}#{quote_char}"
end
result += current_token
result += escaped_unquotable_char # Escape the offending percent

# Start a new token - we'll assume we're remaining outside quotes
current_token = ''
in_quotes = false
next
elsif quote_requiring.include?(char)
# Oh, it turns out we should have been inside quotes for this token.
# Let's note that, for when we actually append the token
in_quotes = true
end
current_token += char
end

if in_quotes
# The final token has been in an inside-quote context, so let's properly wrap that before continuing
current_token = "#{quote_char}#{current_token}#{quote_char}"
end
result += current_token

result
end

attr_accessor :arch
attr_accessor :platform
attr_accessor :max_threads
Expand Down
35 changes: 35 additions & 0 deletions lib/msf/base/sessions/command_shell_unix.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,44 @@ 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 cmd_and_args [Array<String>] The process path and the arguments to the process
def to_cmd(cmd_and_args)
self.class.to_cmd(cmd_and_args)
end

# Escape an individual argument per Unix shell rules
# @param arg [String] Shell argument
def escape_arg(arg)
self.class.escape_arg(arg)
end

# Convert the executable and argument array to a command that can be run in this command shell
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
def self.to_cmd(cmd_and_args)
escaped = cmd_and_args.map do |arg|
escape_arg(arg)
end

escaped.join(' ')
end

# Escape an individual argument per Unix shell rules
# @param arg [String] Shell argument
def self.escape_arg(arg)
quote_requiring = ['\\', '`', '(', ')', '<', '>', '&', '|', ' ', '@', '"', '$', ';']
result = CommandShell._glue_cmdline_escape(arg, quote_requiring, "'", "\\'", "'")
if result == ''
result = "''"
end

result
end
end

end
107 changes: 106 additions & 1 deletion lib/msf/base/sessions/command_shell_windows.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,119 @@

module Msf::Sessions

class CommandShellWindows < CommandShell
def initialize(*args)
self.platform = "windows"
super
end

def self.space_chars
[' ', '\t', '\v']
end

def shell_command_token(cmd,timeout = 10)
shell_command_token_win32(cmd,timeout)
end

# Convert the executable and argument array to a command that can be run in this command shell
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
def to_cmd(cmd_and_args)
self.class.to_cmd(cmd_and_args)
end

# Escape a process for the command line
# @param executable [String] The process to launch
def self.escape_cmd(executable)
needs_quoting = space_chars.any? do |char|
executable.include?(char)
end

if needs_quoting
executable = "\"#{executable}\""
end

executable
end

# Convert the executable and argument array to a commandline that can be passed to CreateProcessAsUserW.
# @param args [Array<String>] The arguments to the process
# @remark The difference between this and `to_cmd` is that the output of `to_cmd` is expected to be passed
# to cmd.exe, whereas this is expected to be passed directly to the Win32 API, anticipating that it
# will in turn be interpreted by CommandLineToArgvW.
def self.argv_to_commandline(args)
escaped_args = args.map do |arg|
escape_arg(arg)
end

escaped_args.join(' ')
end

# Escape an individual argument per Windows shell rules
# @param arg [String] Shell argument
def self.escape_arg(arg)
needs_quoting = space_chars.any? do |char|
arg.include?(char)
end

# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
# We need to send double the number of backslashes to make it work as expected
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
arg = arg.gsub(/(\\*)"/, '\\1\\1"')

# Quotes need to be escaped
arg = arg.gsub('"', '\\"')

if needs_quoting
# At the end of the argument, we're about to add another quote - so any backslashes need to be doubled here too
arg = arg.gsub(/(\\*)$/, '\\1\\1')
arg = "\"#{arg}\""
end

# Empty string needs to be coerced to have a value
arg = '""' if arg == ''

arg
end

# Convert the executable and argument array to a command that can be run in this command shell
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
def self.to_cmd(cmd_and_args)
# The space, caret and quote chars need to be inside double-quoted strings.
# The percent character needs to be escaped using a caret char, while being outside a double-quoted string.
#
# Situations where these two situations combine are going to be the trickiest cases: something that has quote-requiring
# characters (e.g. spaces), but which also needs to avoid expanding an environment variable. In this case,
# the string needs to end up being partially quoted; with parts of the string in quotes, but others (i.e. bits with percents) not.
# For example:
# 'env var is %temp%, yes, %TEMP%' needs to end up as '"env var is "^%temp^%", yes, "^%TEMP^%'
#
# There is flexibility in how you might implement this, but I think this one looks the most "human" to me,
# which would make it less signaturable.
#
# To do this, we'll consider each argument character-by-character. Each time we encounter a percent sign, we break out of any quotes
# (if we've been inside them in the current "token"), and then start a new "token".

quote_requiring = ['"', '^', ' ', "\t", "\v", '&', '<', '>', '|']

escaped_cmd_and_args = cmd_and_args.map do |arg|
# Escape quote chars by doubling them up, except those preceeded by a backslash (which are already effectively escaped, and handled below)
arg = arg.gsub(/([^\\])"/, '\\1""')
arg = arg.gsub(/^"/, '""')

result = CommandShell._glue_cmdline_escape(arg, quote_requiring, '%', '^%', '"')

# Fix the weird behaviour when backslashes are treated differently when immediately prior to a double-quote
# We need to send double the number of backslashes to make it work as expected
# See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
result.gsub!(/(\\*)"/, '\\1\\1"')

# Empty string needs to be coerced to have a value
result = '""' if result == ''

result
end

escaped_cmd_and_args.join(' ')
end
end

end
85 changes: 85 additions & 0 deletions lib/msf/base/sessions/powershell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,91 @@ 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 cmd_and_args [Array<String>] The process path and the arguments to the process
def to_cmd(cmd_and_args)
self.class.to_cmd(cmd_and_args)
end

# Convert the executable and argument array to a command that can be run in this command shell
# @param cmd_and_args [Array<String>] The process path and the arguments to the process
def self.to_cmd(cmd_and_args)
# The principle here is that we want to launch a process such that it receives *exactly* what is in `args`.
# This means we need to:
# - Escape all special characters
# - Not escape environment variables
# - Side-step any PowerShell magic
# If someone specifically wants to use the PowerShell magic, they can use other APIs

needs_wrapping_chars = ['$', '`', '(', ')', '@', '>', '<', '{','}', '&', ',', ' ', ';']

result = ""
cmd_and_args.each_with_index do |arg, index|
needs_single_quoting = false
if arg.include?("'")
arg = arg.gsub("'", "''")
needs_single_quoting = true
end

if arg.include?('"')
# PowerShell acts weird around quotes and backslashes
# First we need to escape backslashes immediately prior to a double-quote, because
# they're treated differently than backslashes anywhere else
arg = arg.gsub(/(\\+)"/, '\\1\\1"')

# Then we can safely prepend a backslash to escape our double-quote
arg = arg.gsub('"', '\\"')
needs_single_quoting = true
end

needs_wrapping_chars.each do |char|
if arg.include?(char)
needs_single_quoting = true
end
end

# PowerShell magic - https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters?view=powershell-7.4#stop-parsing-token---
if arg == '--%'
needs_single_quoting = true
end

will_be_double_quoted_by_powershell = [' ', '\t', '\v'].any? do |bad_char|
arg.include?(bad_char)
end

if will_be_double_quoted_by_powershell
# This is horrible, and I'm so so sorry.
# If an argument ends with a series of backslashes, and it will be quoted by PowerShell when *it* launches the process (e.g. because the arg contains a space),
# PowerShell will not correctly handle backslashes immediately preceeding the quote that it *itself* adds. So we need to be responsible for this.
arg = arg.gsub(/(\\*)$/, '\\1\\1')
end

if needs_single_quoting
arg = "'#{arg}'"
end

if arg == ''
# Pass in empty strings
arg = '\'""\''
end

if index == 0
if needs_single_quoting
# If the executable name (i.e. index 0) has beeen wrapped, then we'll have converted it to a string.
# We then need to use the call operator ('&') to call it.
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-7.3#call-operator-
result = "& #{arg}"
else
result = arg
end
else
result = "#{result} #{arg}"
end
end

result
end

#
# Execute any specified auto-run scripts for this session
#
Expand Down
Loading
Loading