=begin Copyright 2010-2013 Tasos Laskos Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. =end module Arachni module Element::Capabilities # # Performs boolean, fault injection and behavioral analysis (using the rDiff algorithm) # in order to determine whether the web application is responding to the injected data and how. # # If the behavior can be manipulated by the injected data in ways that it's not supposed to # (like when evaluating injected code) then the element is deemed vulnerable. # # @author Tasos "Zapotek" Laskos # module Auditable::RDiff def self.included( mod ) # the rdiff attack performs it own redundancy checks so we need this to # keep track of audited elements @@rdiff_audited ||= Support::LookUp::HashSet.new end RDIFF_OPTIONS = { # append our seeds to the default values format: [Mutable::Format::APPEND], # allow duplicate requests redundant: true, # amount of rdiff iterations precision: 2, respect_method: true } # # Performs differential analysis and logs an issue should there be one. # # opts = { # pairs: [ # { 'true expression' => 'false expression' } # ] # } # # element.rdiff_analysis( opts ) # # Here's how it goes: # # * let `control` be the control/control response # * let `true_response` be the response of the injection of 'true expression' # * let `false_response` be the response of the injection of 'false expression' # # A vulnerability is logged if: # # control == true_response AND true_response.code == 200 AND false_response != true_response # # The `bool` response is also checked in order to determine if it's a custom # 404, if it is then it'll be skipped. # # If a block has been provided analysis and logging will be delegated to it. # # @param [Hash] opts # @option opts [Integer] :format # As seen in {Arachni::Element::Capabilities::Mutable::Format}. # @option opts [Integer] :precision # Amount of {String#rdiff refinement} iterations to perform. # @option opts [Array] :pairs # Pair of strings that should yield different results when interpreted. # Keys should be the `true` expressions. # @param [Block] block # To be used for custom analysis of gathered data. # # @return [Bool] # `true` if the audit was scheduled successfully, `false` otherwise (like # if the resource is out of scope or already audited). # def rdiff_analysis( opts = {}, &block ) if skip_path? self.action print_debug "Element's action matches skip rule, bailing out." return false end opts = self.class::MUTATION_OPTIONS.merge( RDIFF_OPTIONS.merge( opts ) ) return false if auditable.empty? # Don't continue if there's a missing value. auditable.values.each { |val| return if val.to_s.empty? } return false if rdiff_audited? rdiff_audited responses = {} control = nil opts[:precision].times do # Get the default response. submit do |res| if control print_status 'Got default/control response.' end # Remove context-irrelevant dynamic content like banners and such. control = (control ? control.rdiff( res.body ) : res.body) end end opts[:pairs].each do |pair| responses[pair] ||= {} true_expr, false_expr = pair.to_a.first opts[:precision].times do mutations( true_expr, opts ).each do |elem| print_status elem.status_string # Submit the mutation and store the response. elem.submit( opts ) do |res| if responses[pair][elem.altered][:true] elem.print_status "Gathering data for '#{elem.altered}' " << "#{type} input -- Got true response:" << " #{true_expr}" end responses[pair][elem.altered] ||= {} responses[pair][elem.altered][:mutation] = elem # Keep the latest response for the {Arachni::Issue}. responses[pair][elem.altered][:response] = res responses[pair][elem.altered][:injected_string] = true_expr responses[pair][elem.altered][:true] ||= res.body.clone # Remove context-irrelevant dynamic content like banners # and such from the error page. responses[pair][elem.altered][:true] = responses[pair][elem.altered][:true].rdiff( res.body.clone ) end end mutations( false_expr, opts ).each do |elem| responses[pair][elem.altered] ||= {} # Submit the mutation and store the response. elem.submit( opts ) do |res| if responses[pair][elem.altered][:false] elem.print_status "Gathering data for '#{elem.altered}'" << " #{type} input -- Got false " << "response: #{false_expr}" end responses[pair][elem.altered][:false] ||= res.body.clone # Remove context-irrelevant dynamic content like banners # and such from the error page. responses[pair][elem.altered][:false] = responses[pair][elem.altered][:false].rdiff( res.body.clone ) end end end end # When this runs the "responses" hash will have been populated and we # can continue with analysis. http.after_run do responses.each do |pair, data| if block exception_jail( false ){ block.call( pair, data ) } next end data.each do |input_name, result| # if default_response_body == true_response_body AND # false_response_body != true_response_code AND # true_response_code == 200 if control == result[:true] && result[:false] != result[:true] && result[:response].code == 200 # Check to see if the `true` response we're analyzing # is a custom 404 page. http.custom_404?( result[:response] ) do |custom_404| # If this is a custom 404 page bail out. next if custom_404 @auditor.log({ var: input_name, opts: { injected_orig: result[:injected_string], combo: result[:mutation].auditable }, injected: result[:mutation].altered_value, elem: type }, result[:response] ) end end end end end true end private def rdiff_audited @@rdiff_audited << rdiff_audit_id end def rdiff_audited? @@rdiff_audited.include?( rdiff_audit_id ) end def rdiff_audit_id @action + @auditable.keys.to_s end end end end