diff --git a/Gemfile.lock b/Gemfile.lock index e5fa675a6f..6a14e2ed64 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ PATH actionpack (< 4.0.0) activesupport (>= 3.0.0, < 4.0.0) bcrypt + jsobfu (~> 0.1.6) json metasploit-model (~> 0.26.1) meterpreter_bins (= 0.0.7) @@ -12,7 +13,6 @@ PATH nokogiri packetfu (= 1.1.9) railties - rkelly-remix (= 0.0.6) robots rubyzip (~> 1.1) sqlite3 @@ -90,6 +90,8 @@ GEM hike (1.2.3) i18n (0.6.11) journey (1.0.4) + jsobfu (0.1.6) + rkelly-remix (= 0.0.6) json (1.8.1) mail (2.5.4) mime-types (~> 1.16) diff --git a/lib/rex/exploitation/jsobfu.rb b/lib/rex/exploitation/jsobfu.rb index c722e2749c..f02806e81d 100644 --- a/lib/rex/exploitation/jsobfu.rb +++ b/lib/rex/exploitation/jsobfu.rb @@ -1,513 +1,16 @@ # -*- coding: binary -*- -require 'rex/text' -require 'rex/random_identifier_generator' -require 'rkelly' +require 'jsobfu' module Rex module Exploitation - # -# Obfuscate JavaScript by randomizing as much as possible and removing -# easily-signaturable string constants. +# Simple wrapper class that makes the JSObfu functionality +# from the gem available under the Rex namespace. # -# Example: -# js = ::Rex::Exploitation::JSObfu.new %Q| -# var a = "0\\612\\063\\x34\\x35\\x36\\x37\\x38\\u0039"; -# var b = { foo : "foo", bar : "bar" } -# alert(a); -# alert(b.foo); -# | -# js.obfuscate -# puts js -# Example Output: -# var VwxvESbCgv = String.fromCharCode(0x30,0x31,062,063,064,53,0x36,067,070,0x39); -# var ToWZPn = { -# "\146\157\x6f": (function () { var yDyv="o",YnCL="o",Qcsa="f"; return Qcsa+YnCL+yDyv })(), -# "\142ar": String.fromCharCode(0142,97,0162) -# }; -# alert(VwxvESbCgv); -# alert(ToWZPn.foo); -# -# NOTE: Variables MUST be declared with a 'var' statement BEFORE first use (or -# not at all) for this to generate correct code! If variables are not declared -# they will not be randomized but the generated code will be correct. -# -# Bad Example Javascript: -# a = "asdf"; // this variable hasn't been declared and will not be randomized -# var a; -# alert(a); // real js engines will alert "asdf" here -# Bad Example Obfuscated: -# a = (function () { var hpHu="f",oyTm="asd"; return oyTm+hpHu })(); -# var zSrnHpEfJZtg; -# alert(zSrnHpEfJZtg); -# Notice that the first usage of +a+ (before it was declared) is not -# randomized. Thus, the obfuscated version will alert 'undefined' instead of -# "asdf". -# -class JSObfu - - # these keywords should never be used as a random var name - # 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 - ) - - # - # Abstract Syntax Tree generated by RKelly::Parser#parse - # - attr_reader :ast - - # - # Saves +code+ for later obfuscation with #obfuscate - # - def initialize(code) - @code = code - @funcs = {} - @vars = {} - @debug = false - @rand_gen = Rex::RandomIdentifierGenerator.new( - :max_length => 15, - :first_char_set => Rex::Text::Alpha+"_$", - :char_set => Rex::Text::AlphaNumeric+"_$" - ) - end - - # - # Add +str+ to the un-obfuscated code. - # - # Calling this method after #obfuscate is undefined - # - def <<(str) - @code << str - end - - # - # Return the (possibly obfuscated) code as a string. - # - # If #obfuscate has not been called before this, returns the parsed, - # unobfuscated code. This can be useful for example to remove comments and - # standardize spacing. - # - def to_s - parse if not @ast - @ast.to_ecma - end - - # - # Return the obfuscated name of a symbol - # - # You MUST call #obfuscate before this method! - # - def sym(lookup) - if @vars[lookup] - ret = @vars[lookup] - elsif @funcs[lookup] - ret = @funcs[lookup] - else - ret = lookup - end - ret - end - - # - # Parse and obfuscate - # - def obfuscate - parse - obfuscate_r(@ast) - 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 - - # @return [String] a random string - def random_string - @rand_gen.generate - end - - # - # Recursive method to obfuscate the given +ast+. - # - # +ast+ should be the result of RKelly::Parser#parse - # - def obfuscate_r(ast) - ast.each do |node| - #if node.respond_to? :value and node.value.kind_of? String and node.value =~ /bodyOnLoad/i - # $stdout.puts("bodyOnLoad: #{node.class}: #{node.value}") - #end - - case node - when nil - nil - - when ::RKelly::Nodes::SourceElementsNode - # Recurse - obfuscate_r(node.value) - - #when ::RKelly::Nodes::ObjectLiteralNode - # TODO - #$stdout.puts(node.methods - Object.new.methods) - #$stdout.puts(node.value.inspect) - - when ::RKelly::Nodes::PropertyNode - # Property names must be bare words or string literals NOT - # expressions! Can't use transform_string() here - if node.name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/ - n = '"' - node.name.unpack("C*") { |c| - case rand(3) - when 0; n << "\\x%02x"%(c) - when 1; n << "\\#{c.to_s 8}" - when 2; n << [c].pack("C") - end - } - n << '"' - node.instance_variable_set(:@name, n) - end - - # Variables - when ::RKelly::Nodes::VarDeclNode - if @vars[node.name].nil? - @vars[node.name] = random_var_name - end - node.name = @vars[node.name] - when ::RKelly::Nodes::ParameterNode - if @vars[node.value].nil? - @vars[node.value] = random_var_name - end - node.value = @vars[node.value] - when ::RKelly::Nodes::ResolveNode - #$stdout.puts("Resolve bodyOnload: #{@vars[node.value]}") if "bodyOnLoad" == node.value - node.value = @vars[node.value] if @vars[node.value] - when ::RKelly::Nodes::DotAccessorNode - case node.value - when ::RKelly::Nodes::ResolveNode - if @vars[node.value.value] - node.value.value = @vars[node.value.value] - end - #else - # $stderr.puts("Non-resolve node as target of dotaccessor: #{node.value.class}") - end - - # Functions - when ::RKelly::Nodes::FunctionDeclNode - #$stdout.puts("FunctionDecl: #{node.value}") - # Functions can also act as objects, so store them in the vars - # and the functions list so we can replace them in both places - if @funcs[node.value].nil? and not @funcs.values.include?(node.value) - @funcs[node.value] = random_var_name - if @vars[node.value].nil? - @vars[node.value] = @funcs[node.value] - end - node.value = @funcs[node.value] - end - when ::RKelly::Nodes::FunctionCallNode - # The value of a FunctionCallNode is some sort of accessor node or a ResolveNode - # so this is basically useless - #$stdout.puts("Function call: #{node.name} => #{@funcs[node.name]}") - #node.value = @funcs[node.value] if @funcs[node.value] - - # Transformers - when ::RKelly::Nodes::NumberNode - node.value = transform_number(node.value) - when ::RKelly::Nodes::StringNode - node.value = transform_string(node.value) - else - #$stderr.puts "#{node.class}: #{node.value}" - #$stderr.puts "#{node.class}" - end - - #unless node.kind_of? ::RKelly::Nodes::SourceElementsNode - # $stderr.puts "#{node.class}: #{node.value}" - #end - end - - nil - end - - # - # Generate an Abstract Syntax Tree (#ast) for later obfuscation - # - def parse - parser = RKelly::Parser.new - @ast = parser.parse(@code) - end - - # - # Convert a number to a random base (decimal, octal, or hexedecimal). - # - # Given 10 as input, the possible return values are: - # "10" - # "0xa" - # "012" - # - def rand_base(num) - case rand(3) - when 0; num.to_s - when 1; "0%o" % num - when 2; "0x%x" % num - end - end - - # - # Return a mathematical expression that will evaluate to the given number - # +num+. - # - # +num+ can be a float or an int, but should never be negative. - # - def transform_number(num) - case num - when Fixnum - if num == 0 - r = rand(10) + 1 - transformed = "('#{Rex::Text.rand_text_alpha(r)}'.length - #{r})" - elsif num > 0 and num < 10 - # use a random string.length for small numbers - transformed = "'#{Rex::Text.rand_text_alpha(num)}'.length" - else - transformed = "(" - divisor = rand(num) + 1 - a = num / divisor.to_i - b = num - (a * divisor) - # recurse half the time for a - a = (rand(2) == 0) ? transform_number(a) : rand_base(a) - # recurse half the time for divisor - divisor = (rand(2) == 0) ? transform_number(divisor) : rand_base(divisor) - transformed << "#{a}*#{divisor}" - transformed << "+#{b}" - transformed << ")" - end - when Float - transformed = "(#{num - num.floor} + #{rand_base(num.floor)})" - end - - #puts("#{num} == #{transformed}") - - transformed - end - - # - # Convert a javascript string into something that will generate that string. - # - # Randomly calls one of the +transform_string_*+ methods - # - def transform_string(str) - quote = str[0,1] - # pull off the quotes - str = str[1,str.length - 2] - return quote*2 if str.length == 0 - - case rand(2) - when 0 - transformed = transform_string_split_concat(str, quote) - when 1 - transformed = transform_string_fromCharCode(str) - #when 2 - # # Currently no-op - # transformed = transform_string_unescape(str) - end - - #$stderr.puts "Obfuscating str: #{str.ljust 30} #{transformed}" - transformed - end - - # - # Split a javascript string, +str+, without breaking escape sequences. - # - # The maximum length of each piece of the string is half the total length - # of the string, ensuring we (almost) always split into at least two - # pieces. This won't always be true when given a string like "AA\x41", - # where escape sequences artificially increase the total length (escape - # sequences are considered a single character). - # - # Returns an array of two-element arrays. The zeroeth element is a - # randomly generated variable name, the first is a piece of the string - # contained in +quote+s. - # - # See #escape_length - # - def safe_split(str, quote) - parts = [] - max_len = str.length / 2 - while str.length > 0 - len = 0 - loop do - e_len = escape_length(str[len..-1]) - e_len = 1 if e_len.nil? - len += e_len - # if we've reached the end of the string, bail - break unless str[len] - break if len > max_len - # randomize the length of each part - break if (rand(4) == 0) - end - - part = str.slice!(0, len) - - var = Rex::Text.rand_text_alpha(4) - parts.push( [ var, "#{quote}#{part}#{quote}" ] ) - end - - parts - end - - # - # Stolen from obfuscatejs.rb - # - # Determines the length of an escape sequence - # - def escape_length(str) - esc_len = nil - if str[0,1] == "\\" - case str[1,1] - when "u"; esc_len = 6 # unicode \u1234 - when "x"; esc_len = 4 # hex, \x41 - when /[0-7]/ # octal, \123, \0 - str[1,3] =~ /([0-7]{1,3})/ - if $1.to_i(8) > 255 - str[1,3] =~ /([0-7]{1,2})/ - end - esc_len = 1 + $1.length - else; esc_len = 2 # \" \n, etc. - end - end - esc_len - end - - # - # Split a javascript string, +str+, into multiple randomly-ordered parts - # and return an anonymous javascript function that joins them in the - # correct order. This method can be called safely on strings containing - # escape sequences. See #safe_split. - # - def transform_string_split_concat(str, quote) - parts = safe_split(str, quote) - func = "(function () { var " - ret = "; return " - parts.sort { |a,b| rand }.each do |part| - func << "#{part[0]}=#{part[1]}," - end - func.chop! - - ret << parts.map{|part| part[0]}.join("+") - final = func + ret + " })()" - - final - end - - - # TODO - #def transform_string_unescape(str) - # str - #end - - # - # Return a call to String.fromCharCode() with each char of the input as arguments - # - # Example: - # input : "A\n" - # output: String.fromCharCode(0x41, 10) - # - def transform_string_fromCharCode(str) - buf = "String.fromCharCode(" - bytes = str.unpack("C*") - len = 0 - while str.length > 0 - if str[0,1] == "\\" - str.slice!(0,1) - # then this is an escape sequence and we need to deal with all - # the special cases - case str[0,1] - # For chars that contain their non-escaped selves, step past - # the backslash and let the rand_base() below decide how to - # represent the character. - when '"', "'", "\\", " " - char = str.slice!(0,1).unpack("C").first - # For symbolic escapes, use the known value - when "n"; char = 0x0a; str.slice!(0,1) - when "t"; char = 0x09; str.slice!(0,1) - # Lastly, if it's a hex, unicode, or octal escape, pull out the - # real value and use that - when "x" - # Strip the x - str.slice!(0,1) - char = str.slice!(0,2).to_i 16 - when "u" - # This can potentially lose information in the case of - # characters like \u0041, but since regular ascii is stored - # as unicode internally, String.fromCharCode(0x41) will be - # represented as 00 41 in memory anyway, so it shouldn't - # matter. - str.slice!(0,1) - char = str.slice!(0,4).to_i 16 - when /[0-7]/ - # Octals are a bit harder since they are variable width and - # don't necessarily mean what you might think. For example, - # "\61" == "1" and "\610" == "10". 610 is a valid octal - # number, but not a valid ascii character. Javascript will - # interpreter as much as it can as a char and use the rest - # as a literal. Boo. - str =~ /([0-7]{1,3})/ - char = $1.to_i 8 - if char > 255 - str =~ /([0-7]{1,2})/ - char = $1.to_i 8 - end - str.slice!(0,$1.length) - end - else - char = str.slice!(0,1).unpack("C").first - end - buf << "#{rand_base(char)}," - end - # Strip off the last comma - buf = buf[0,buf.length-1] + ")" - transformed = buf - - transformed - end - +class JSObfu < ::JSObfu +end end end -end - - -=begin -if __FILE__ == $0 - if ARGV[0] - code = File.read(ARGV[0]) - else - #require 'rex/exploitation/javascriptosdetect' - #code = Rex::Exploitation::JavascriptOSDetect.new.to_s - code = <<-EOS - // Should alert "0123456789" - var a = "0\\612\\063\\x34\\x35\\x36\\x37\\x38\\u0039"; - var a,b=2,c=3; - alert(a); - // should alert "asdfjkl;" - var d = (function() { var foo = "jkl;", blah = "asdf"; return blah + foo; })(); - alert(d); - EOS - end - js = Rex::Exploitation::JSObfu.new(code) - js.obfuscate - puts js.to_s - -end - -=end diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index 792efb8921..dc49a5b7cf 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -55,6 +55,8 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'actionpack', rails_version_constraint # Needed for some admin modules (cfme_manageiq_evm_pass_reset.rb) spec.add_runtime_dependency 'bcrypt' + # Needed for Javascript obfuscation + spec.add_runtime_dependency 'jsobfu', '~> 0.1.6' # Needed for some admin modules (scrutinizer_add_user.rb) spec.add_runtime_dependency 'json' # Things that would normally be part of the database model, but which @@ -70,8 +72,6 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'packetfu', '1.1.9' # Run initializers for metasploit-concern, metasploit-credential, metasploit_data_models Rails::Engines spec.add_runtime_dependency 'railties' - # Needed by JSObfu - spec.add_runtime_dependency 'rkelly-remix', '0.0.6' # Needed by anemone crawler spec.add_runtime_dependency 'robots' # Needed by some modules