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 )