lib/module/auditor.rb in arachni-0.2.4 vs lib/module/auditor.rb in arachni-0.3
- old
+ new
@@ -12,20 +12,46 @@
module Module
#
# Auditor module
#
-# Included by {Module::Base}.<br/>
-# Includes audit methods.
+# Included by {Module::Base} and provides abstract audit methods.
#
+# There are 3 main types of audit techniques available:
+# * Pattern matching -- {#audit}
+# * Timing attacks -- {#audit_timeout}
+# * Differential analysis attacks -- {#audit_rdiff}
+#
+#
# @author: Tasos "Zapotek" Laskos
# <tasos.laskos@gmail.com>
# <zapotek@segfault.gr>
-# @version: 0.2.3
+# @version: 0.3
#
module Auditor
+ def self.included( mod )
+ # @@__timeout_audited ||= Set.new
+
+ # holds timing-attack performing Procs to be run after all
+ # non-tming-attack modules have finished.
+ @@__timeout_audit_blocks ||= Queue.new
+
+ # populated by timing attack phase 1 with
+ # candidate elements to be verified by phase 2
+ @@__timeout_candidates ||= Queue.new
+
+ # modules which have called the timing attack audit mthod (audit_timeout)
+ # we're interested in the amount, not the names, and is used to
+ # determine scan progress
+ @@__timeout_loaded_modules ||= Set.new
+
+ # the rdiff attack performs it own redundancy checks so we need this to
+ # keep track audited elements
+ @@__rdiff_audited ||= Set.new
+ end
+
#
# Holds constant bitfields that describe the preferred formatting
# of injection strings.
#
module Format
@@ -125,18 +151,20 @@
#
:async => true
}
#
- # Matches the HTML in @page.html to an array of regular expressions
- # and logs the results.
+ # Matches the "string" (default string is the HTML code in @page.html) to
+ # an array of regular expressions and logs the results.
#
- # @param [Array<Regexp>] regexps
- # @param [String] string
- # @param [Block] block block to verify matches before logging
- # must return true/false
+ # For good measure, regexps will also be run against the page headers (@page.response_headers).
#
+ # @param [Array<Regexp>] regexps array of regular expressions to be tested
+ # @param [String] string string to
+ # @param [Block] block block to verify matches before logging,
+ # must return true/false
+ #
def match_and_log( regexps, string = @page.html, &block )
# make sure that we're working with an array
regexps = [regexps].flatten
@@ -182,14 +210,14 @@
}
end
#
- # Logs a vulnerability based on a regular expression and it's matched string
+ # Populates and logs an {Arachni::Issue} based on data from "opts" and "res".
#
- # @param [Regexp] regexp
- # @param [String] match
+ # @param [Hash] opts as passed to blocks by audit methods
+ # @param [Typhoeus::Response] res defaults to @page data
#
def log( opts, res = nil )
method = nil
@@ -244,26 +272,30 @@
}.merge( self.class.info ) )
register_results( [vuln] )
end
#
- # Provides easy access to element auditing.
+ # Provides easy access to element auditing using simple injection and pattern
+ # matching.
#
+ # If a block has been provided analysis and logging will be delegated to it,
+ # otherwise, if a match is found it will be automatically logged.
+ #
# If no elements have been specified in 'opts' it will
# use the elements from the module's "self.info()" hash. <br/>
# If no elements have been specified in 'opts' or "self.info()" it will
# use the elements in {OPTIONS}. <br/>
#
#
# @param [String] injection_str the string to be injected
# @param [Hash] opts options as described in {OPTIONS}
- # @param [Block] &block block to be passed the:
- # * HTTP response
- # * name of the input vector
- # * updated opts
- # The block will be called as soon as the
- # HTTP response is received.
+ # @param [Block] &block block to be used for custom analysis of responses; will be passed the following:
+ # * HTTP response
+ # * options
+ # * element
+ # The block will be called as soon as the
+ # HTTP response is received.
#
def audit( injection_str, opts = { }, &block )
if( !opts.include?( :elements) || !opts[:elements] || opts[:elements].empty? )
opts[:elements] = self.class.info[:elements]
@@ -298,93 +330,218 @@
}
end
#
- # ABSTRACT - OPTIONAL
- #
# This is called right before an [Arachni::Parser::Element]
# is submitted/auditted and is used to determine whether to skip it or not.
#
- # Implementation details are left up to the running module.
+ # Running modules can override this as they wish *but* at their own peril.
#
# @param [Arachni::Parser::Element] elem
#
def skip?( elem )
+ redundant.map {
+ |mod|
+
+ mod_name = @framework.modules[mod].info[:name]
+
+ set_id = @framework.modules.class.issue_set_id_from_elem( mod_name, elem )
+ return true if @framework.modules.issue_set.include?( set_id )
+ }
+
+
+ # if !@@__timeout_audited.empty?
+ # return @@__timeout_audited.include?( __rdiff_audit_id( elem ) )
+ # end
+
return false
end
#
- # Audits elements using a 2 phase timing attack and logs results.
+ # Audits elements using timing attacks and automatically logs results.
#
- # 'opts' needs to contain a :timeout value in milliseconds.</br>
- # Optionally, you can add a :timeout_divider.
+ # Here's how it works:
+ # * Loop 1 -- Populates the candidate queue. We're picking the low hanging
+ # fruit here so we can run this in larger concurrent bursts which cause *lots* of noise.
+ # - Initial probing for candidates -- Any element that times out is added to a queue.
+ # - Stabilization -- The candidate is submited with its default values in
+ # order to wait until the effects of the timing attack have worn off.
+ # * Loop 2 -- Verifies the candidates. This is much more delicate so the
+ # concurrent requests are lowered to pairs.
+ # - Liveness test -- Ensures that stabilization was successful before moving on.
+ # - Verification using an increased timeout -- Any elements that time out again are logged.
+ # - Stabilization
#
- # Phase 1 uses the timeout value passed in opts, phase 2 uses (timeout * 2). </br>
- # If phase 1 fails, phase 2 is aborted. </br>
- # If we have a result in phase 1, phase 2 verifies that result with the higher timeout.
+ # Ideally, all requests involved with timing attacks would be run in sync mode
+ # but the performance penalties are too high, thus we compromise and make the best of it
+ # by running as little an amount of concurrent requests as possible for any given phase.
#
+ # opts = {
+ # :format => [ Format::STRAIGHT ],
+ # :timeout => 4000,
+ # :timeout_divider => 1000
+ # }
+ #
+ # audit_timeout( [ 'sleep( __TIME__ );' ], opts )
+ #
+ #
# @param [Array] strings injection strings
- # '__TIME__' will be substituded with (timeout / timeout_divider)
- # @param [Hash] opts options as described in {OPTIONS}
+ # __TIME__ will be substituded with (timeout / timeout_divider)
+ # @param [Hash] opts options as described in {OPTIONS} with the following extra:
+ # * :timeout -- milliseconds to wait for the request to complete
+ # * :timeout_divider -- __TIME__ = timeout / timeout_divider
#
def audit_timeout( strings, opts )
- logged = Set.new
+ @@__timeout_loaded_modules << self.class.info[:name]
- delay = opts[:timeout]
+ @@__timeout_audit_blocks << Proc.new {
+ delay = opts[:timeout]
- audit_timeout_debug_msg( 1, delay )
- timing_attack( strings, opts ) {
- |res, opts, elem|
+ audit_timeout_debug_msg( 1, delay )
+ timing_attack( strings, opts ) {
+ |res, opts, elem|
- if !logged.include?( opts[:altered] )
- logged << opts[:altered]
- audit_timeout_phase_2( elem )
- end
+ # maybe this should be removed to take care of accidental timeouts
+ # and let phase 2 clean up the mess
+ # if !@@__timeout_audited.include?( __rdiff_audit_id( elem ) )
+
+ elem.auditor( self )
+ # @@__timeout_audited << __rdiff_audit_id( elem )
+
+ print_info( "Found a candidate -- #{elem.type.capitalize} input '#{elem.altered}' at #{elem.action}" )
+
+ Arachni::Module::Auditor.audit_timeout_stabilize( elem )
+
+ @@__timeout_candidates << elem
+ # end
+ }
}
end
#
+ # Returns the names of all loaded modules that use timing attacks.
+ #
+ # @return [Set]
+ #
+ def self.timeout_loaded_modules
+ @@__timeout_loaded_modules
+ end
+
+ #
+ # Holds timing-attack performing Procs to be run after all
+ # non-tming-attack modules have finished.
+ #
+ # @return [Queue]
+ #
+ def self.timeout_audit_blocks
+ @@__timeout_audit_blocks
+ end
+
+ #
+ # Runs all blocks in {timeout_audit_blocks} and verifies
+ # and logs the candidate elements.
+ #
+ def self.timeout_audit_run
+ while( !@@__timeout_audit_blocks.empty? )
+ @@__timeout_audit_blocks.pop.call
+ end
+
+ while( !@@__timeout_candidates.empty? )
+ self.audit_timeout_phase_2( @@__timeout_candidates.pop )
+ end
+ end
+
+ #
# Runs phase 2 of the timing attack auditng an individual element
# (which passed phase 1) with a higher delay and timeout
#
- def audit_timeout_phase_2( elem )
+ def self.audit_timeout_phase_2( elem )
+ # reset the audited list since we're going to re-audit the elements
+ # @@__timeout_audited = Set.new
+
opts = elem.opts
opts[:timeout] *= 2
+ # opts[:async] = false
+ # self.audit_timeout_debug_msg( 2, opts[:timeout] )
- audit_timeout_debug_msg( 2, opts[:timeout] )
-
str = opts[:timing_string].gsub( '__TIME__',
- ( (opts[:timeout] + 3000) / opts[:timeout_divider] ).to_s )
+ ( opts[:timeout] / opts[:timeout_divider] ).to_s )
- elem.auditor( self )
+ opts[:timeout] *= 0.7
- # this is the control; audit the element with an empty seed to make sure
+ elem.auditable = elem.orig
+
+ # this is the control; request the URL of the element to make sure
# that the web page is alive i.e won't time-out by default
- elem.audit( '' , opts ) {
- |res, opts|
+ elem.get_auditor.http.get( elem.action ).on_complete {
+ |res|
+
if !res.timed_out?
+ elem.get_auditor.print_info( 'Liveness check was successful, progressing to verification...' )
+
elem.audit( str, opts ) {
|res, opts|
if res.timed_out?
# all issues logged by timing attacks need manual verification.
# end of story.
opts[:verification] = true
- log( opts, res)
+ elem.get_auditor.log( opts, res)
+
+ self.audit_timeout_stabilize( elem )
+
+ else
+ elem.get_auditor.print_info( 'Verification failed.' )
end
}
-
+ else
+ elem.get_auditor.print_info( 'Liveness check failed, bailing out...' )
end
}
+ elem.get_auditor.http.run
end
+ #
+ # Submits an element which has just been audited using a timing attack
+ # with a high timeout in order to determine when the effects of a timing
+ # attack has worn off in order to safely continue the audit.
+ #
+ # @param [Arachni::Element::Auditable] elem
+ #
+ def self.audit_timeout_stabilize( elem )
+
+ d_opts = {
+ :skip_orig => true,
+ :redundant => true,
+ :timeout => 120000,
+ :silent => true,
+ :async => false
+ }
+
+ orig_opts = elem.opts
+
+ elem.get_auditor.print_info( 'Waiting for the effects of the timing attack to wear off.' )
+ elem.get_auditor.print_info( 'Max waiting time: ' + ( d_opts[:timeout] /1000 ).to_s + ' seconds.' )
+
+ elem.auditable = elem.orig
+ res = elem.submit( d_opts ).response
+
+ if !res.timed_out?
+ elem.get_auditor.print_info( 'Server seems responsive again.' )
+ else
+ elem.get_auditor.print_error( 'Max waiting time exceeded, the server may be dead.' )
+ end
+
+ elem.opts.merge!( orig_opts )
+ end
+
def audit_timeout_debug_msg( phase, delay )
print_debug( '---------------------------------------------' )
print_debug( "Running phase #{phase.to_s} of timing attack." )
print_debug( "Delay set to: #{delay.to_s} milliseconds" )
print_debug( '---------------------------------------------' )
@@ -403,23 +560,294 @@
# it will be passed the response and opts
#
def timing_attack( strings, opts, &block )
opts[:timeout_divider] ||= 1
+ # opts[:async] = false
+
[strings].flatten.each {
|str|
opts[:timing_string] = str
- str = str.gsub( '__TIME__', ( (opts[:timeout] + 3000) / opts[:timeout_divider] ).to_s )
+ str = str.gsub( '__TIME__', ( (opts[:timeout] + 3 * opts[:timeout_divider]) / opts[:timeout_divider] ).to_s )
+ opts[:skip_orig] = true
+
audit( str, opts ) {
|res, opts, elem|
block.call( res, opts, elem ) if block && res.timed_out?
}
}
+
+ @http.run
end
#
+ # Audits all elements types in opts[:elements] (or self.class.info[:elements]
+ # if there are none in opts) using differential analysis attacks.
+ #
+ # opts = {
+ # :precision => 3,
+ # :faults => [ 'fault injections' ],
+ # :bools => [ 'boolean injections' ]
+ # }
+ #
+ # audit_rdiff( opts )
+ #
+ # Here's how it goes:
+ # let default be the default/original response
+ # let fault be the response of the fault injection
+ # let bool be the response of the boolean injection
+ #
+ # a vulnerability is logged if default == bool AND bool.code == 200 AND fault != bool
+ #
+ # The "bool" response is also checked in order to determine if it's a custom 404, if it is it'll be skipped.
+ #
+ # If a block has been provided analysis and logging will be delegated to it.
+ #
+ # @param [Hash] opts available options:
+ # * :format -- as seen in {OPTIONS}
+ # * :elements -- as seen in {OPTIONS}
+ # * :train -- as seen in {OPTIONS}
+ # * :precision -- amount of rdiff iterations
+ # * :faults -- array of fault injection strings (these are supposed to force erroneous conditions when interpreted)
+ # * :bools -- array of boolean injection strings (these are supposed to not alter the webapp behavior when interpreted)
+ # @param [Block] &block block to be used for custom analysis of responses; will be passed the following:
+ # * injected string
+ # * audited element
+ # * default response body
+ # * boolean response
+ # * fault injection response body
+ #
+ def audit_rdiff( opts = {}, &block )
+
+ if( !opts.include?( :elements) || !opts[:elements] || opts[:elements].empty? )
+ opts[:elements] = self.class.info[:elements]
+ end
+
+ if( !opts.include?( :elements) || !opts[:elements] || opts[:elements].empty? )
+ opts[:elements] = OPTIONS[:elements]
+ end
+
+ opts = {
+ # append our seeds to the default values
+ :format => [ Format::APPEND ],
+ # allow duplicate requests
+ :redundant => true,
+ # amound of rdiff iterations
+ :precision => 2
+ }.merge( opts )
+
+ opts[:elements].each {
+ |elem|
+
+ case elem
+
+ when Element::LINK
+ next if !Options.instance.audit_links
+ @page.links.each {
+ |elem|
+ audit_rdiff_elem( elem, opts, &block )
+ }
+
+ when Element::FORM
+ next if !Options.instance.audit_forms
+ @page.forms.each {
+ |elem|
+ audit_rdiff_elem( elem, opts, &block )
+ }
+
+ when Element::COOKIE
+ next if !Options.instance.audit_cookies
+ @page.cookies.each {
+ |elem|
+ audit_rdiff_elem( elem, opts, &block )
+ }
+
+ when Element::HEADER
+ next if !Options.instance.audit_headers
+ @page.headers.each {
+ |elem|
+ audit_rdiff_elem( elem, opts, &block )
+ }
+ else
+ raise( 'Unknown element to audit: ' + elem.to_s )
+
+ end
+
+ }
+ end
+
+ #
+ # Audits a single element using an rdiff attack.
+ #
+ # @param [Arachni::Element::Auditable] elem the element to audit
+ # @param [Hash] opts same as for {#audit_rdiff}
+ # @param [Block] &block same as for {#audit_rdiff}
+ #
+ def audit_rdiff_elem( elem, opts = {}, &block )
+
+ # don't continue if there's a missing value
+ elem.auditable.values.each {
+ |val|
+ return if !val || val.empty?
+ }
+
+ return if __rdiff_audited?( elem )
+ __rdiff_audited!( elem )
+
+ responses = {
+ :orig => nil,
+ :good => {},
+ :bad => {},
+ :bad_total => 0,
+ :good_total => 0
+ }
+
+ elem.auditor( self )
+ opts[:precision].times {
+ # get the default responses
+ elem.audit( '', opts ) {
+ |res|
+ responses[:orig] ||= res.body
+ # remove context-irrelevant dynamic content like banners and such
+ # from the error page
+ responses[:orig] = responses[:orig].rdiff( res.body )
+ }
+ }
+
+ opts[:precision].times {
+ opts[:faults].each {
+ |str|
+
+ # get injection variations that will hopefully cause an internal/silent
+ # SQL error
+ variations = elem.injection_sets( str, opts )
+
+ responses[:bad_total] = variations.size
+
+ variations.each {
+ |c_elem|
+
+ print_status( c_elem.get_status_str( c_elem.altered ) )
+
+ # register us as the auditor
+ c_elem.auditor( self )
+ # submit the link and get the response
+ c_elem.submit( opts ).on_complete {
+ |res|
+
+ responses[:bad][c_elem.altered] ||= res.body.clone
+
+ # remove context-irrelevant dynamic content like banners and such
+ # from the error page
+ responses[:bad][c_elem.altered] =
+ responses[:bad][c_elem.altered].rdiff( res.body.clone )
+ }
+ }
+ }
+ }
+
+ opts[:bools].each {
+ |str|
+
+ # get injection variations that will not affect the outcome of the query
+ variations = elem.injection_sets( str, opts )
+
+ responses[:good_total] = variations.size
+
+ variations.each {
+ |c_elem|
+
+ print_status( c_elem.get_status_str( c_elem.altered ) )
+
+ # register us as the auditor
+ c_elem.auditor( self )
+ # submit the link and get the response
+ c_elem.submit( opts ).on_complete {
+ |res|
+
+ responses[:good][c_elem.altered] ||= []
+
+ # save the response for later analysis
+ responses[:good][c_elem.altered] << {
+ 'str' => str,
+ 'res' => res,
+ 'elem' => c_elem
+ }
+ }
+ }
+ }
+
+ # when this runs the 'responses' hash will have been populated
+ @http.after_run {
+
+ responses[:good].keys.each {
+ |key|
+
+ responses[:good][key].each {
+ |res|
+
+ if block
+ block.call( res['str'], res['elem'], responses[:orig], res['res'], responses[:bad][key] )
+ elsif( responses[:orig] == res['res'].body &&
+ responses[:bad][key] != res['res'].body &&
+ !@http.custom_404?( res['res'] ) && res['res'].code == 200 )
+
+ url = res['res'].effective_url
+
+ # since we bypassed the auditor completely we need to create
+ # our own opts hash and pass it to the Vulnerability class.
+ #
+ # this is only required for Metasploitable vulnerabilities
+ opts = {
+ :injected_orig => res['str'],
+ :combo => res['elem'].auditable
+ }
+
+ issue = Issue.new( {
+ :var => key,
+ :url => url,
+ :method => res['res'].request.method.to_s,
+ :opts => opts,
+ :injected => res['str'],
+ :id => res['str'],
+ :regexp => 'n/a',
+ :regexp_match => 'n/a',
+ :elem => res['elem'].type,
+ :response => res['res'].body,
+ :verification => true,
+ :headers => {
+ :request => res['res'].request.headers,
+ :response => res['res'].headers,
+ }
+ }.merge( self.class.info )
+ )
+
+ print_ok( "In #{res['elem'].type} var '#{key}' ( #{url} )" )
+
+ # register our results with the system
+ register_results( [ issue ] )
+ end
+
+ }
+ }
+ }
+ end
+
+ def __rdiff_audited!( elem )
+ @@__rdiff_audited << __rdiff_audit_id( elem )
+ end
+
+ def __rdiff_audited?( elem )
+ @@__rdiff_audited.include?( __rdiff_audit_id( elem ) )
+ end
+
+ def __rdiff_audit_id( elem )
+ elem.action + elem.auditable.keys.to_s
+ end
+
+ #
# Provides the following methods:
# * audit_links()
# * audit_forms()
# * audit_cookies()
# * audit_headers()
@@ -446,18 +874,13 @@
end
#
# Audits Auditalble HTML/HTTP elements
#
- # @param [Array<Arachni::Element::Auditable>] elements auditable elements to audit
- # @param [String] injection_str the string to be injected
- # @param [Hash] opts options as described in {OPTIONS}
- # @param [Block] &block block to be passed the:
- # * HTTP response
- # * name of the input vector
- # * updated opts
- # The block will be called as soon as the
- # HTTP response is received.
+ # @param [Array<Arachni::Element::Auditable>] elements elements to audit
+ # @param [String] injection_str same as for {#audit}
+ # @param [Hash] opts same as for {#audit}
+ # @param [Block] &block same as for {#audit}
#
# @see #method_missing
#
def audit_elems( elements, injection_str, opts = { }, &block )