364 lines
13 KiB
Ruby
364 lines
13 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
module Msf::Post::Common
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Compat' => {
|
|
'Meterpreter' => {
|
|
'Commands' => %w[
|
|
stdapi_sys_config_getenv
|
|
stdapi_sys_process_execute
|
|
]
|
|
}
|
|
}
|
|
)
|
|
)
|
|
end
|
|
|
|
def clear_screen
|
|
Gem.win_platform? ? (system "cls") : (system "clear")
|
|
end
|
|
|
|
def rhost
|
|
return super unless defined?(session) and session
|
|
|
|
case session.type.downcase
|
|
when 'meterpreter'
|
|
session.sock.peerhost
|
|
when 'shell', 'powershell'
|
|
session.session_host
|
|
end
|
|
rescue
|
|
return nil
|
|
end
|
|
|
|
def rport
|
|
return super unless defined?(session) and session
|
|
|
|
case session.type.downcase
|
|
when 'meterpreter'
|
|
session.sock.peerport
|
|
when 'shell', 'powershell'
|
|
session.session_port
|
|
end
|
|
rescue
|
|
return nil
|
|
end
|
|
|
|
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<String>] The arguments to the executable
|
|
# @time_out [Integer] Number of seconds before the call will time out
|
|
# @param opts [Hash] Optional settings to parameterise the process launch
|
|
# @option Hidden [Boolean] Is the process launched without creating a visible window
|
|
# @option Channelized [Boolean] The process is launched with pipes connected to a channel, e.g. for sending input/receiving output
|
|
# @option Suspended [Boolean] Start the process suspended
|
|
# @option UseThreadToken [Boolean] Use the thread token (as opposed to the process token) to launch the process
|
|
# @option Desktop [Boolean] Run on meterpreter's current desktop
|
|
# @option Session [Integer] Execute process in a given session as the session user
|
|
# @option Subshell [Boolean] Execute process in a subshell
|
|
# @option Pty [Boolean] Execute process in a pty (if available)
|
|
# @option ParentId [Integer] Spoof the parent PID (if possible)
|
|
# @option InMemory [Boolean,String] Execute from memory (`path` is treated as a local file to upload, and the actual path passed
|
|
# to meterpreter is this parameter's value, if provided as a String)
|
|
def create_process(executable, args: [], time_out: 15, opts: {})
|
|
case session.type
|
|
when 'meterpreter'
|
|
session.response_timeout = time_out
|
|
opts = {
|
|
'Hidden' => true,
|
|
'Channelized' => true,
|
|
# Well-behaving meterpreters will ignore the Subshell flag when using arg arrays.
|
|
# This is still provided for supporting old meterpreters.
|
|
'Subshell' => true
|
|
}.merge(opts)
|
|
|
|
if session.platform == 'windows'
|
|
if session.arch == 'php'
|
|
opts[:legacy_args] = Msf::Sessions::CommandShellWindows.to_cmd(args)
|
|
opts[:legacy_path] = Msf::Sessions::CommandShellWindows.to_cmd([executable])
|
|
elsif session.arch == 'python'
|
|
opts[:legacy_path] = executable
|
|
# Yes, Unix. Old Python meterp had a bug where it used posix shell splitting
|
|
# syntax even on Windows. For backwards-compatibility, we can trick it into
|
|
# doing the right thing by using Unix escaping.
|
|
opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(args)
|
|
else
|
|
opts[:legacy_args] = Msf::Sessions::CommandShellWindows.argv_to_commandline(args)
|
|
opts[:legacy_path] = Msf::Sessions::CommandShellWindows.escape_cmd(executable)
|
|
end
|
|
else
|
|
opts[:legacy_args] = Msf::Sessions::CommandShellUnix.to_cmd(args)
|
|
opts[:legacy_path] = Msf::Sessions::CommandShellUnix.to_cmd([executable])
|
|
end
|
|
|
|
if opts['Channelized']
|
|
o = session.sys.process.capture_output(executable, args, opts, time_out)
|
|
else
|
|
session.sys.process.execute(executable, args, opts)
|
|
end
|
|
when 'powershell'
|
|
cmd = session.to_cmd([executable] + args)
|
|
o = session.shell_command(cmd, time_out)
|
|
o.chomp! if o
|
|
when 'shell'
|
|
cmd = session.to_cmd([executable] + args)
|
|
o = session.shell_command_token(cmd, time_out)
|
|
o.chomp! if o
|
|
end
|
|
return "" if o.nil?
|
|
return o
|
|
end
|
|
|
|
#
|
|
# Executes +cmd+ on the remote system
|
|
#
|
|
# On Windows meterpreter, this will go through CreateProcess as the
|
|
# "commandLine" parameter. This means it will follow the same rules as
|
|
# Windows' path disambiguation. For example, if you were to call this method
|
|
# thusly:
|
|
#
|
|
# cmd_exec("c:\\program files\\sub dir\\program name")
|
|
#
|
|
# Windows would look for these executables, in this order, passing the rest
|
|
# of the line as arguments:
|
|
#
|
|
# c:\program.exe
|
|
# c:\program files\sub.exe
|
|
# c:\program files\sub dir\program.exe
|
|
# c:\program files\sub dir\program name.exe
|
|
#
|
|
# On POSIX meterpreter, if +args+ is set or if +cmd+ contains shell
|
|
# metacharacters, the server will run the whole thing in /bin/sh. Otherwise,
|
|
# (cmd is a single path and there are no arguments), it will execve the given
|
|
# executable.
|
|
#
|
|
# On Java, it is passed through Runtime.getRuntime().exec(String) and PHP
|
|
# uses proc_open() both of which have similar semantics to POSIX.
|
|
#
|
|
# On shell sessions, this passes +cmd+ directly the session's
|
|
# +shell_command_token+ method.
|
|
#
|
|
# Returns a (possibly multi-line) String.
|
|
#
|
|
def cmd_exec(cmd, args=nil, time_out=15, opts = {})
|
|
case session.type
|
|
when 'meterpreter'
|
|
#
|
|
# The meterpreter API requires arguments to come separately from the
|
|
# executable path. This has no effect on Windows where the two are just
|
|
# blithely concatenated and passed to CreateProcess or its brethren. On
|
|
# POSIX, this allows the server to execve just the executable when a
|
|
# shell is not needed. Determining when a shell is not needed is not
|
|
# always easy, so it assumes anything with arguments needs to go through
|
|
# /bin/sh.
|
|
#
|
|
# This problem was originally solved by using Shellwords.shellwords but
|
|
# unfortunately, it is unsuitable. When a backslash occurs inside double
|
|
# quotes (as is often the case with Windows commands) it inexplicably
|
|
# removes them. So. Shellwords is out.
|
|
#
|
|
# By setting +args+ to an empty string, we can get POSIX to send it
|
|
# through /bin/sh, solving all the pesky parsing troubles, without
|
|
# affecting Windows.
|
|
#
|
|
if args.nil? and cmd =~ /[^a-zA-Z0-9\/._-]/
|
|
args = ""
|
|
end
|
|
|
|
session.response_timeout = time_out
|
|
opts = {
|
|
'Hidden' => true,
|
|
'Channelized' => true,
|
|
'Subshell' => true
|
|
}.merge(opts)
|
|
|
|
if opts['Channelized']
|
|
o = session.sys.process.capture_output(cmd, args, opts, time_out)
|
|
else
|
|
session.sys.process.execute(cmd, args, opts)
|
|
end
|
|
when 'powershell'
|
|
if args.nil? || args.empty?
|
|
o = session.shell_command("#{cmd}", time_out)
|
|
else
|
|
o = session.shell_command("#{cmd} #{args}", time_out)
|
|
end
|
|
o.chomp! if o
|
|
when 'shell'
|
|
if args.nil? || args.empty?
|
|
o = session.shell_command_token("#{cmd}", time_out)
|
|
else
|
|
o = session.shell_command_token("#{cmd} #{args}", time_out)
|
|
end
|
|
o.chomp! if o
|
|
end
|
|
return "" if o.nil?
|
|
return o
|
|
end
|
|
|
|
def cmd_exec_get_pid(cmd, args=nil, time_out=15)
|
|
case session.type
|
|
when 'meterpreter'
|
|
if args.nil? and cmd =~ /[^a-zA-Z0-9\/._-]/
|
|
args = ""
|
|
end
|
|
session.response_timeout = time_out
|
|
process = session.sys.process.execute(cmd, args, {'Hidden' => true, 'Channelized' => true, 'Subshell' => true })
|
|
process.channel.close
|
|
pid = process.pid
|
|
process.close
|
|
pid
|
|
else
|
|
print_error "cmd_exec_get_pid is incompatible with non-meterpreter sessions"
|
|
end
|
|
end
|
|
|
|
#
|
|
# Reports to the database that the host is using virtualization and reports
|
|
# the type of virtualization it is (e.g VirtualBox, VMware, Xen, Docker)
|
|
#
|
|
def report_virtualization(virt)
|
|
return unless session
|
|
return unless virt
|
|
virt_normal = virt.to_s.strip
|
|
return if virt_normal.empty?
|
|
virt_data = {
|
|
:host => session.target_host,
|
|
:virtual_host => virt_normal
|
|
}
|
|
report_host(virt_data)
|
|
end
|
|
|
|
#
|
|
# Returns the value of the environment variable +env+
|
|
#
|
|
def get_env(env)
|
|
case session.type
|
|
when 'meterpreter'
|
|
return session.sys.config.getenv(env)
|
|
when 'powershell'
|
|
return cmd_exec("echo $env:#{env}").strip
|
|
when 'shell'
|
|
if session.platform == 'windows'
|
|
if env[0,1] == '%'
|
|
unless env[-1,1] == '%'
|
|
env << '%'
|
|
end
|
|
else
|
|
env = "%#{env}%"
|
|
end
|
|
|
|
return cmd_exec("echo #{env}")
|
|
else
|
|
unless env[0,1] == '$'
|
|
env = "$#{env}"
|
|
end
|
|
|
|
return cmd_exec("echo \"#{env}\"")
|
|
end
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
#
|
|
# Returns a hash of environment variables +envs+
|
|
#
|
|
def get_envs(*envs)
|
|
case session.type
|
|
when 'meterpreter'
|
|
return session.sys.config.getenvs(*envs)
|
|
when 'shell', 'powershell'
|
|
result = {}
|
|
envs.each do |env|
|
|
res = get_env(env)
|
|
result[env] = res unless res.blank?
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
# Checks if the specified command can be executed by the session. It should be
|
|
# noted that not all commands correspond to a binary file on disk. For example,
|
|
# a bash shell session will provide the `eval` command when there is no `eval`
|
|
# binary on disk. Likewise, a Powershell session will provide the `Get-Item`
|
|
# command when there is no `Get-Item` executable on disk.
|
|
#
|
|
# @param [String] cmd the command to check
|
|
# @return [Boolean] true when the command exists
|
|
def command_exists?(cmd)
|
|
verification_token = Rex::Text.rand_text_alpha_upper(8)
|
|
if session.type == 'powershell'
|
|
cmd_exec("try {if(Get-Command #{cmd}) {echo #{verification_token}}} catch {}").include?(verification_token)
|
|
elsif session.platform == 'windows'
|
|
# https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/where_1
|
|
# https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/if
|
|
cmd_exec("cmd /c where /q #{cmd} & if not errorlevel 1 echo #{verification_token}").to_s.include?(verification_token)
|
|
else
|
|
cmd_exec("command -v #{cmd} || which #{cmd} && echo #{verification_token}").include?(verification_token)
|
|
end
|
|
rescue
|
|
raise "Unable to check if command `#{cmd}' exists"
|
|
end
|
|
|
|
# Executes +cmd+ on the remote system and return an array containing the
|
|
# output and if it was successful or not.
|
|
#
|
|
# This is simply a wrapper around {#cmd_exec} that also checks the exit code
|
|
# to determine if the execution was successful or not.
|
|
#
|
|
# @param [String] cmd The command to execute
|
|
# @param args [String] The optional arguments of the command (can de included in +cmd+ instead)
|
|
# @param [Integer] timeout The time in sec. to wait before giving up
|
|
# @param [Hash] opts An Hash of options (see {#cmd_exec})
|
|
# @return [Array(String, Boolean)] Array containing the output string
|
|
# followed by a boolean indicating if the command succeeded or not. When
|
|
# this boolean is `true`, the first field contains the normal command
|
|
# output. When it is `false`, the first field contains the error message
|
|
# returned by the command or a timeout error message if the timeout
|
|
# expired.
|
|
def cmd_exec_with_result(cmd, args = nil, timeout = 15, opts = {})
|
|
# This token will be returned if the command succeeds.
|
|
# Redirection operators (`&&` and `||`) are the most reliable methods to
|
|
# detect success and failure. See these references for details:
|
|
# - https://ss64.com/nt/errorlevel.html
|
|
# - https://stackoverflow.com/questions/34936240/batch-goto-loses-errorlevel/34937706#34937706
|
|
# - https://stackoverflow.com/questions/10935693/foolproof-way-to-check-for-nonzero-error-return-code-in-windows-batch-file/10936093#10936093
|
|
verification_token = Rex::Text.rand_text_alphanumeric(8)
|
|
|
|
_cmd = cmd.dup
|
|
_cmd << " #{args}" if args
|
|
if session.platform == 'windows'
|
|
if session.type == 'powershell'
|
|
# The & operator is reserved by Powershell and needs to be wrapped in double quotes
|
|
result = cmd_exec('cmd', "/c #{_cmd} \"&&\" echo #{verification_token}", timeout, opts)
|
|
else
|
|
result = cmd_exec('cmd', "/c #{_cmd} && echo #{verification_token}", timeout, opts)
|
|
end
|
|
else
|
|
result = cmd_exec('command', "#{_cmd} && echo #{verification_token}", timeout, opts)
|
|
end
|
|
|
|
if result.include?(verification_token)
|
|
# Removing the verification token to cleanup the output string
|
|
[result.lines[0...-1].join.strip, true]
|
|
else
|
|
[result.strip, false]
|
|
end
|
|
rescue Rex::TimeoutError => e
|
|
[e.message, false]
|
|
end
|
|
|
|
end
|