Land #3224 - Fixes large-string expansion in JSObfu
commit
68a50e3663
|
@ -1,25 +1,25 @@
|
||||||
|
|
||||||
// Case matters, see lib/msf/core/constants.rb
|
// Case matters, see lib/msf/core/constants.rb
|
||||||
// All of these should match up with constants in ::Msf::HttpClients
|
// All of these should match up with constants in ::Msf::HttpClients
|
||||||
clients_opera = "Opera";
|
var clients_opera = "Opera";
|
||||||
clients_ie = "MSIE";
|
var clients_ie = "MSIE";
|
||||||
clients_ff = "Firefox";
|
var clients_ff = "Firefox";
|
||||||
clients_chrome= "Chrome";
|
var clients_chrome= "Chrome";
|
||||||
clients_safari= "Safari";
|
var clients_safari= "Safari";
|
||||||
|
|
||||||
// All of these should match up with constants in ::Msf::OperatingSystems
|
// All of these should match up with constants in ::Msf::OperatingSystems
|
||||||
oses_linux = "Linux";
|
var oses_linux = "Linux";
|
||||||
oses_windows = "Microsoft Windows";
|
var oses_windows = "Microsoft Windows";
|
||||||
oses_mac_osx = "Mac OS X";
|
var oses_mac_osx = "Mac OS X";
|
||||||
oses_freebsd = "FreeBSD";
|
var oses_freebsd = "FreeBSD";
|
||||||
oses_netbsd = "NetBSD";
|
var oses_netbsd = "NetBSD";
|
||||||
oses_openbsd = "OpenBSD";
|
var oses_openbsd = "OpenBSD";
|
||||||
|
|
||||||
// All of these should match up with the ARCH_* constants
|
// All of these should match up with the ARCH_* constants
|
||||||
arch_armle = "armle";
|
var arch_armle = "armle";
|
||||||
arch_x86 = "x86";
|
var arch_x86 = "x86";
|
||||||
arch_x86_64 = "x86_64";
|
var arch_x86_64 = "x86_64";
|
||||||
arch_ppc = "ppc";
|
var arch_ppc = "ppc";
|
||||||
|
|
||||||
window.os_detect = {};
|
window.os_detect = {};
|
||||||
|
|
||||||
|
|
|
@ -48,14 +48,71 @@ module Exploitation
|
||||||
#
|
#
|
||||||
class JSObfu
|
class JSObfu
|
||||||
|
|
||||||
# these keywords should never be used as a random var name
|
# A single Javascript scope, used as a key-value store
|
||||||
# source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words
|
# to maintain uniqueness of members in generated closures.
|
||||||
RESERVED_KEYWORDS = %w(
|
# For speed this class is implemented as a subclass of Hash.
|
||||||
break case catch continue debugger default delete do else finally
|
class Scope < Hash
|
||||||
for function if in instanceof new return switch this throw try
|
|
||||||
typeof var void while with class enum export extends import super
|
# these keywords should never be used as a random var name
|
||||||
implements interface let package private protected public static yield
|
# source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words
|
||||||
)
|
RESERVED_KEYWORDS = %w(
|
||||||
|
break case catch continue debugger default delete do else finally
|
||||||
|
for function if in instanceof new return switch this throw try
|
||||||
|
typeof var void while with class enum export extends import super
|
||||||
|
implements interface let package private protected public static yield
|
||||||
|
)
|
||||||
|
|
||||||
|
# these vars should not be shadowed as they may be used in other obfuscated code
|
||||||
|
BUILTIN_VARS = %w(
|
||||||
|
String window unescape
|
||||||
|
)
|
||||||
|
|
||||||
|
# @param [Rex::Exploitation::JSObfu::Scope] parent an optional parent scope,
|
||||||
|
# sometimes necessary to prevent needless var shadowing
|
||||||
|
def initialize(parent=nil)
|
||||||
|
@parent = parent
|
||||||
|
@rand_gen = Rex::RandomIdentifierGenerator.new(
|
||||||
|
:max_length => 15,
|
||||||
|
:first_char_set => Rex::Text::Alpha+"_$",
|
||||||
|
:char_set => Rex::Text::AlphaNumeric+"_$",
|
||||||
|
:min_length => 1
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String] a unique random var name that is not a reserved keyword
|
||||||
|
def random_var_name
|
||||||
|
len = 1
|
||||||
|
loop do
|
||||||
|
text = random_string(len)
|
||||||
|
unless has_key?(text) or
|
||||||
|
RESERVED_KEYWORDS.include?(text) or
|
||||||
|
BUILTIN_VARS.include?(text)
|
||||||
|
|
||||||
|
self[text] = nil
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
len += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Boolean] var is in scope
|
||||||
|
def has_key?(key)
|
||||||
|
super or (@parent and @parent.has_key?(key))
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String] a random string that can be used as a var
|
||||||
|
def random_string(len)
|
||||||
|
@rand_gen.generate(len)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# The maximum length of a string that can be passed through
|
||||||
|
# #transform_string without being chopped up into separate
|
||||||
|
# expressions and concatenated
|
||||||
|
#
|
||||||
|
MAX_STRING_CHUNK = 10000
|
||||||
|
|
||||||
#
|
#
|
||||||
# Abstract Syntax Tree generated by RKelly::Parser#parse
|
# Abstract Syntax Tree generated by RKelly::Parser#parse
|
||||||
|
@ -69,12 +126,8 @@ class JSObfu
|
||||||
@code = code
|
@code = code
|
||||||
@funcs = {}
|
@funcs = {}
|
||||||
@vars = {}
|
@vars = {}
|
||||||
|
@scope = Scope.new
|
||||||
@debug = false
|
@debug = false
|
||||||
@rand_gen = Rex::RandomIdentifierGenerator.new(
|
|
||||||
:max_length => 15,
|
|
||||||
:first_char_set => Rex::Text::Alpha+"_$",
|
|
||||||
:char_set => Rex::Text::AlphaNumeric+"_$"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -122,23 +175,8 @@ class JSObfu
|
||||||
obfuscate_r(@ast)
|
obfuscate_r(@ast)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [String] a unique random var name that is not a reserved keyword
|
|
||||||
def random_var_name
|
|
||||||
loop do
|
|
||||||
text = random_string
|
|
||||||
unless @vars.has_value?(text) or RESERVED_KEYWORDS.include?(text)
|
|
||||||
return text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# @return [String] a random string
|
|
||||||
def random_string
|
|
||||||
@rand_gen.generate
|
|
||||||
end
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Recursive method to obfuscate the given +ast+.
|
# Recursive method to obfuscate the given +ast+.
|
||||||
#
|
#
|
||||||
|
@ -182,12 +220,12 @@ protected
|
||||||
# Variables
|
# Variables
|
||||||
when ::RKelly::Nodes::VarDeclNode
|
when ::RKelly::Nodes::VarDeclNode
|
||||||
if @vars[node.name].nil?
|
if @vars[node.name].nil?
|
||||||
@vars[node.name] = random_var_name
|
@vars[node.name] = @scope.random_var_name
|
||||||
end
|
end
|
||||||
node.name = @vars[node.name]
|
node.name = @vars[node.name]
|
||||||
when ::RKelly::Nodes::ParameterNode
|
when ::RKelly::Nodes::ParameterNode
|
||||||
if @vars[node.value].nil?
|
if @vars[node.value].nil?
|
||||||
@vars[node.value] = random_var_name
|
@vars[node.value] = @scope.random_var_name
|
||||||
end
|
end
|
||||||
node.value = @vars[node.value]
|
node.value = @vars[node.value]
|
||||||
when ::RKelly::Nodes::ResolveNode
|
when ::RKelly::Nodes::ResolveNode
|
||||||
|
@ -209,7 +247,7 @@ protected
|
||||||
# Functions can also act as objects, so store them in the vars
|
# Functions can also act as objects, so store them in the vars
|
||||||
# and the functions list so we can replace them in both places
|
# and the functions list so we can replace them in both places
|
||||||
if @funcs[node.value].nil? and not @funcs.values.include?(node.value)
|
if @funcs[node.value].nil? and not @funcs.values.include?(node.value)
|
||||||
@funcs[node.value] = random_var_name
|
@funcs[node.value] = @scope.random_var_name
|
||||||
if @vars[node.value].nil?
|
if @vars[node.value].nil?
|
||||||
@vars[node.value] = @funcs[node.value]
|
@vars[node.value] = @funcs[node.value]
|
||||||
end
|
end
|
||||||
|
@ -311,14 +349,15 @@ protected
|
||||||
str = str[1,str.length - 2]
|
str = str[1,str.length - 2]
|
||||||
return quote*2 if str.length == 0
|
return quote*2 if str.length == 0
|
||||||
|
|
||||||
case rand(2)
|
if str.length > MAX_STRING_CHUNK
|
||||||
|
return safe_split(str, quote).map { |args| transform_string(args[1]) }.join('+')
|
||||||
|
end
|
||||||
|
|
||||||
|
transformed = case rand(2)
|
||||||
when 0
|
when 0
|
||||||
transformed = transform_string_split_concat(str, quote)
|
transform_string_split_concat(str, quote)
|
||||||
when 1
|
when 1
|
||||||
transformed = transform_string_fromCharCode(str)
|
transform_string_fromCharCode(str)
|
||||||
#when 2
|
|
||||||
# # Currently no-op
|
|
||||||
# transformed = transform_string_unescape(str)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
#$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}"
|
#$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}"
|
||||||
|
@ -353,12 +392,12 @@ protected
|
||||||
break unless str[len]
|
break unless str[len]
|
||||||
break if len > max_len
|
break if len > max_len
|
||||||
# randomize the length of each part
|
# randomize the length of each part
|
||||||
break if (rand(4) == 0)
|
break if (rand(max_len) == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
part = str.slice!(0, len)
|
part = str.slice!(0, len)
|
||||||
|
|
||||||
var = Rex::Text.rand_text_alpha(4)
|
var = @scope.random_var_name
|
||||||
parts.push( [ var, "#{quote}#{part}#{quote}" ] )
|
parts.push( [ var, "#{quote}#{part}#{quote}" ] )
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -409,12 +448,6 @@ protected
|
||||||
final
|
final
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
#def transform_string_unescape(str)
|
|
||||||
# str
|
|
||||||
#end
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Return a call to String.fromCharCode() with each char of the input as arguments
|
# Return a call to String.fromCharCode() with each char of the input as arguments
|
||||||
#
|
#
|
||||||
|
@ -423,9 +456,17 @@ protected
|
||||||
# output: String.fromCharCode(0x41, 10)
|
# output: String.fromCharCode(0x41, 10)
|
||||||
#
|
#
|
||||||
def transform_string_fromCharCode(str)
|
def transform_string_fromCharCode(str)
|
||||||
buf = "String.fromCharCode("
|
"String.fromCharCode(#{string_to_bytes(str)})"
|
||||||
bytes = str.unpack("C*")
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Return a comma-separated list of byte values with random encodings (decimal/hex/octal)
|
||||||
|
#
|
||||||
|
def string_to_bytes(str)
|
||||||
len = 0
|
len = 0
|
||||||
|
bytes = str.unpack("C*")
|
||||||
|
encoded_bytes = []
|
||||||
|
|
||||||
while str.length > 0
|
while str.length > 0
|
||||||
if str[0,1] == "\\"
|
if str[0,1] == "\\"
|
||||||
str.slice!(0,1)
|
str.slice!(0,1)
|
||||||
|
@ -472,16 +513,12 @@ protected
|
||||||
else
|
else
|
||||||
char = str.slice!(0,1).unpack("C").first
|
char = str.slice!(0,1).unpack("C").first
|
||||||
end
|
end
|
||||||
buf << "#{rand_base(char)},"
|
encoded_bytes << rand_base(char)
|
||||||
end
|
end
|
||||||
# Strip off the last comma
|
|
||||||
buf = buf[0,buf.length-1] + ")"
|
|
||||||
transformed = buf
|
|
||||||
|
|
||||||
transformed
|
encoded_bytes.join(',')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'rex/exploitation/jsobfu'
|
||||||
|
|
||||||
|
describe Rex::Exploitation::JSObfu::Scope do
|
||||||
|
|
||||||
|
subject(:scope) do
|
||||||
|
described_class.new()
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#random_var_name' do
|
||||||
|
subject(:random_var_name) { scope.random_var_name }
|
||||||
|
|
||||||
|
it { should be_a String }
|
||||||
|
it { should_not be_empty }
|
||||||
|
|
||||||
|
it 'is composed of _, $, alphanumeric chars' do
|
||||||
|
20.times { expect(scope.random_var_name).to match(/\A[a-zA-Z0-9$_]+\Z/) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not start with a number' do
|
||||||
|
20.times { expect(scope.random_var_name).not_to match(/\A[0-9]/) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a reserved word is generated' do
|
||||||
|
let(:reserved) { described_class::RESERVED_KEYWORDS.first }
|
||||||
|
let(:random) { 'abcdef' }
|
||||||
|
let(:generated) { [reserved, reserved, reserved, random] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
scope.stub(:random_string) { generated.shift }
|
||||||
|
end
|
||||||
|
|
||||||
|
it { should eq random }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a non-unique random var is generated' do
|
||||||
|
let(:preexisting) { 'preexist' }
|
||||||
|
let(:random) { 'abcdef' }
|
||||||
|
let(:generated) { [preexisting, preexisting, preexisting, random] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
scope.stub(:random_string) { generated.shift }
|
||||||
|
scope[preexisting] = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it { should eq random }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -7,44 +7,66 @@ describe Rex::Exploitation::JSObfu do
|
||||||
described_class.new("")
|
described_class.new("")
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#random_var_name' do
|
# surround the string in quotes
|
||||||
subject(:random_var_name) { jsobfu.random_var_name }
|
def quote(str, q='"'); "#{q}#{str}#{q}" end
|
||||||
|
|
||||||
it { should be_a String }
|
describe '#transform_string' do
|
||||||
it { should_not be_empty }
|
context 'when given a string of length > MAX_STRING_CHUNK' do
|
||||||
|
let(:js_string) { quote "ABC"*Rex::Exploitation::JSObfu::MAX_STRING_CHUNK }
|
||||||
|
|
||||||
it 'is composed of _, $, alphanumeric chars' do
|
it 'calls itself recursively' do
|
||||||
20.times { expect(jsobfu.random_var_name).to match(/\A[a-zA-Z0-9$_]+\Z/) }
|
expect(jsobfu).to receive(:transform_string).at_least(2).times.and_call_original
|
||||||
|
jsobfu.send(:transform_string, js_string.dup)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not start with a number' do
|
context 'when given a string of length < MAX_STRING_CHUNK' do
|
||||||
20.times { expect(jsobfu.random_var_name).not_to match(/\A[0-9]/) }
|
let(:js_string) { quote "A"*(Rex::Exploitation::JSObfu::MAX_STRING_CHUNK/2).to_i }
|
||||||
|
|
||||||
|
it 'does not call itself recursively' do
|
||||||
|
expect(jsobfu).to receive(:transform_string).once.and_call_original
|
||||||
|
jsobfu.send(:transform_string, js_string.dup)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when a reserved word is generated' do
|
describe '#safe_split' do
|
||||||
let(:reserved) { described_class::RESERVED_KEYWORDS.first }
|
let(:js_string) { Rex::Text.to_hex("ABCDEFG"*100, "\\x") }
|
||||||
let(:random) { 'abcdef' }
|
let(:quote) { '"' }
|
||||||
let(:generated) { [reserved, reserved, reserved, random] }
|
let(:parts) { 50.times.map { jsobfu.send(:safe_split, js_string.dup, quote).map{ |a| a[1] } } }
|
||||||
|
|
||||||
before do
|
describe 'quoting' do
|
||||||
jsobfu.stub(:random_string) { generated.shift }
|
context 'when given a double-quote' do
|
||||||
|
let(:quote) { '"' }
|
||||||
|
it 'surrounds all the split strings with the same quote' do
|
||||||
|
expect(parts.flatten.all? { |part| part.start_with?(quote) }).to be_true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it { should be random }
|
context 'when given a single-quote' do
|
||||||
|
let(:quote) { "'" }
|
||||||
|
it 'surrounds all the split strings with the same quote' do
|
||||||
|
expect(parts.flatten.all? { |part| part.start_with?(quote) }).to be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a non-unique random var is generated' do
|
describe 'splitting' do
|
||||||
let(:preexisting) { 'preexist' }
|
context 'when given a hex-escaped series of bytes' do
|
||||||
let(:random) { 'abcdef' }
|
let(:js_string) { Rex::Text.to_hex("ABCDEFG"*100, "\\x") }
|
||||||
let(:vars) { { 'jQuery' => preexisting } }
|
|
||||||
let(:generated) { [preexisting, preexisting, preexisting, random] }
|
|
||||||
|
|
||||||
before do
|
it 'never splits in the middle of a hex escape' do
|
||||||
jsobfu.stub(:random_string) { generated.shift }
|
expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be_true
|
||||||
jsobfu.instance_variable_set("@vars", vars)
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it { should be random }
|
context 'when given a unicode-escaped series of bytes' do
|
||||||
|
let(:js_string) { Rex::Text.to_unescape("ABCDEFG"*100).gsub!('%', '\\') }
|
||||||
|
|
||||||
|
it 'never splits in the middle of a unicode escape' do
|
||||||
|
expect(parts.flatten.all? { |part| part.start_with?('"\\') }).to be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue