Module: Arachni::Module::Auditor

Included in:
Base
Defined in:
lib/module/auditor.rb

Overview

Auditor module

Included by Base.<br/> Includes audit methods.

@author: Tasos “Zapotek” Laskos

<[email protected]>
<[email protected]>

@version: 0.2.2

Defined Under Namespace

Modules: Element, Format

Constant Summary collapse

OPTIONS =

Default audit options.

{

    #
    # Elements to audit.
    #
    # Only required when calling {#audit}.<br/>
    # If no elements have been passed to audit it will
    # use the elements in {#self.info}.
    #
    :elements => [ Element::LINK, Element::FORM,
                   Element::COOKIE, Element::HEADER,
                   Issue::Element::BODY ],

    #
    # The regular expression to match against the response body.
    #
    :regexp   => nil,

    #
    # Verify the matched string with this value.
    #
    :match    => nil,

    #
    # Formatting of the injection strings.
    #
    # A new set of audit inputs will be generated
    # for each value in the array.
    #
    # Values can be OR'ed bitfields of all available constants
    # of {Auditor::Format}.
    #
    # @see  Auditor::Format
    #
    :format   => [ Format::STRAIGHT, Format::APPEND,
                   Format::NULL, Format::APPEND | Format::NULL ],

    #
    # If 'train' is set to true the HTTP response will be
    # analyzed for new elements. <br/>
    # Be carefull when enabling it, there'll be a performance penalty.
    #
    # When the Auditor submits a form with original or sample values
    # this option will be overriden to true.
    #
    :train     => false,

    #
    # Enable skipping of already audited inputs
    #
    :redundant => false,

    #
    # Make requests asynchronously
    #
    :async     => true
}

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args, &block) ⇒ Object

Provides the following methods:

  • audit_links()

  • audit_forms()

  • audit_cookies()

  • audit_headers()

Metaprogrammed to avoid redundant code while maintaining compatibility and method shortcuts.

Raises:

  • (NoMethodError)

See Also:



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/module/auditor.rb', line 421

def method_missing( sym, *args, &block )

    elem = sym.to_s.gsub!( 'audit_', '@' )
    raise NoMethodError.new( "Undefined method '#{sym.to_s}'.", sym, args ) if !elem

    elems = @page.instance_variable_get( elem )

    if( elems && elem )
        raise ArgumentError.new( "Missing required argument 'injection_str'" +
            " for audit_#{elem.gsub( '@', '' )}()." ) if( !args[0] )
        audit_elems( elems, args[0], args[1] ? args[1]: {}, &block )
    else
        raise NoMethodError.new( "Undefined method '#{sym.to_s}'.", sym, args )
    end
end

Instance Method Details

#audit(injection_str, opts = { }, &block) ⇒ Object

Provides easy access to element auditing.

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/>

Parameters:

  • injection_str (String)

    the string to be injected

  • opts (Hash) (defaults to: { })

    options as described in OPTIONS

  • &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.
    


266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/module/auditor.rb', line 266

def audit( injection_str, 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  = OPTIONS.merge( opts )

    opts[:elements].each {
        |elem|

        case elem

            when  Element::LINK
                audit_links( injection_str, opts, &block )

            when  Element::FORM
                audit_forms( injection_str, opts, &block )

            when  Element::COOKIE
                audit_cookies( injection_str, opts, &block )

            when  Element::HEADER
                audit_headers( injection_str, opts, &block )
            else
                raise( 'Unknown element to audit:  ' + elem.to_s )

        end

    }
end

#audit_elems(elements, injection_str, opts = { }, &block) ⇒ Object

Audits Auditalble HTML/HTTP elements

Parameters:

  • elements (Array<Arachni::Element::Auditable>)

    auditable elements to audit

  • injection_str (String)

    the string to be injected

  • opts (Hash) (defaults to: { })

    options as described in OPTIONS

  • &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.
    

See Also:



452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/module/auditor.rb', line 452

def audit_elems( elements, injection_str, opts = { }, &block )

    opts            = OPTIONS.merge( opts )
    url             = @page.url

    opts[:injected_orig] = injection_str

    elements.each{
        |elem|
        elem.auditor( self )
        elem.audit( injection_str, opts, &block )
    }
end

#audit_timeout(strings, opts) ⇒ Object

Audits elements using a 2 phase timing attack and logs results.

‘opts’ needs to contain a :timeout value in milliseconds.</br> Optionally, you can add a :timeout_divider.

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.

Parameters:

  • strings (Array)

    injection strings ‘__TIME__’ will be substituded with (timeout / timeout_divider)

  • opts (Hash)

    options as described in OPTIONS



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/module/auditor.rb', line 331

def audit_timeout( strings, opts )
    logged = Set.new

    delay = opts[:timeout]

    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
    }
end

#audit_timeout_debug_msg(phase, delay) ⇒ Object



375
376
377
378
379
380
# File 'lib/module/auditor.rb', line 375

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( '---------------------------------------------' )
end

#audit_timeout_phase_2(elem) ⇒ Object

Runs phase 2 of the timing attack auditng an individual element (which passed phase 1) with a higher delay and timeout



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/module/auditor.rb', line 351

def audit_timeout_phase_2( elem )

    opts = elem.opts
    opts[:timeout] *= 2

    audit_timeout_debug_msg( 2, opts[:timeout] )

    str = opts[:timing_string].gsub( '__TIME__',
        ( (opts[:timeout] + 3000) / opts[:timeout_divider] ).to_s )

    elem.auditor( self )
    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)
        end
    }
end

#log(opts, res = nil) ⇒ Object

Logs a vulnerability based on a regular expression and it’s matched string

Parameters:

  • regexp (Regexp)
  • match (String)


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/module/auditor.rb', line 192

def log( opts, res = nil )

    method = nil

    request_headers  = nil
    response_headers = @page.response_headers
    response         = @page.html
    url              = @page.url
    method           = @page.method.to_s.upcase if @page.method

    if( res )
        request_headers  = res.request.headers
        response_headers = res.headers
        response         = res.body
        url              = res.effective_url
        method           = res.request.method.to_s.upcase
    end

    if response_headers['content-type'] &&
       !response_headers['content-type'].substring?( 'text' )
        response = nil
    end

    begin
        print_ok( "In #{opts[:element]} var '#{opts[:altered]}' ( #{url} )" )
    rescue
    end

    print_verbose( "Injected string:\t" + opts[:injected] ) if opts[:injected]
    print_verbose( "Verified string:\t" + opts[:match].to_s ) if opts[:match]
    print_verbose( "Matched regular expression: " + opts[:regexp].to_s )
    print_debug( 'Request ID: ' + res.request.id.to_s ) if res
    print_verbose( '---------' ) if only_positives?

    # Instantiate a new Vulnerability class and
    # append it to the results array
    vuln = Issue.new( {
        :var          => opts[:altered],
        :url          => url,
        :injected     => opts[:injected],
        :id           => opts[:id],
        :regexp       => opts[:regexp],
        :regexp_match => opts[:match],
        :elem         => opts[:element],
        :verification => opts[:verification] || false,
        :method       => method,
        :response     => response,
        :opts         => opts,
        :headers      => {
            :request    => request_headers,
            :response   => response_headers,
        }
    }.merge( self.class.info ) )
    register_results( [vuln] )
end

#match_and_log(regexps, string = @page.html, &block) ⇒ Object

Matches the HTML in @page.html to an array of regular expressions and logs the results.

Parameters:

  • regexps (Array<Regexp>)
  • string (String) (defaults to: @page.html)
  • block (Block)

    block to verify matches before logging must return true/false



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/module/auditor.rb', line 138

def match_and_log( regexps, string = @page.html, &block )

    # make sure that we're working with an array
    regexps = [regexps].flatten

    elems = self.class.info[:elements]
    elems = OPTIONS[:elements] if !elems || elems.empty?

    regexps.each {
        |regexp|

        string.scan( regexp ).flatten.uniq.each {
            |match|

            next if !match
            next if block && !block.call( match )

            log(
                :regexp  => regexp,
                :match   => match,
                :element => Issue::Element::BODY
            )
        } if elems.include? Issue::Element::BODY

        next if string == @page.html

        @page.response_headers.each {
            |k,v|
            next if !v

            v.to_s.scan( regexp ).flatten.uniq.each {
                |match|

                next if !match
                next if block && !block.call( match )

                log(
                    :var => k,
                    :regexp  => regexp,
                    :match   => match,
                    :element => Issue::Element::HEADER
                )
            }
        } if elems.include? Issue::Element::HEADER

    }
end

#skip?(elem) ⇒ Boolean

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.

Parameters:

Returns:

  • (Boolean)


312
313
314
# File 'lib/module/auditor.rb', line 312

def skip?( elem )
    return false
end

#timing_attack(strings, opts, &block) ⇒ Object

Audits elements using a timing attack.

‘opts’ needs to contain a :timeout value in milliseconds.</br> Optionally, you can add a :timeout_divider.

Parameters:

  • strings (Array)

    injection strings ‘__TIME__’ will be substituded with (timeout / timeout_divider)

  • opts (Hash)

    options as described in OPTIONS

  • &block (Block)

    block to call if a timeout occurs, it will be passed the response and opts



394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/module/auditor.rb', line 394

def timing_attack( strings, opts, &block )

    opts[:timeout_divider] ||= 1
    [strings].flatten.each {
        |str|

        opts[:timing_string] = str
        str = str.gsub( '__TIME__', ( (opts[:timeout] + 3000) / opts[:timeout_divider] ).to_s )
        audit( str, opts ) {
            |res, opts, elem|
            block.call( res, opts, elem ) if block && res.timed_out?
        }
    }
end