Module: Arachni::Element::Capabilities::Analyzable::Timeout

Included in:
Arachni::Element::Capabilities::Analyzable
Defined in:
lib/arachni/element/capabilities/analyzable/timeout.rb

Overview

Evaluates whether or not the injection of specific data affects the response time of the web application.

It takes into account unstable network conditions and server-side failures and verifies the results before logging.

# Methodology

Here's how it works:

  • Phase 1 (#timeout_analysis) – 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, if element submission

      times-out it is added to the Phase 2 queue.
      
  • Phase 2 (Timeout.analysis_phase_2) – Verifies the candidates. This is much more delicate so the concurrent requests are lowered to pairs.

    • Control check – Ensures that the webapp is alive and not just timing-out

      by default.
      
    • Verification using an increased timeout delay –

      Any elements that time out again are logged.
      
    • Stabilization (#ensure_responsiveness).

  • Phase 3 (Timeout.analysis_phase_3) – Same as phase 2 but with a higher delay to ensure that false-positives are truly weeded out.

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 blocking requests as possible for any given phase.

# Usage

  • Call #timeout_analysis to schedule requests for Phase 1.

  • Call HTTP::Client#run to run the Phase 1 requests which will populate the Phase 2 queue with candidates – if there are any.

  • Call Timeout.run to filter the candidates through Phases 2 and 3 to ensure that false-positives are weeded out.

Be sure to call Timeout.run as soon as possible after Phase 1, as the candidate elements keep a reference to their auditor which will prevent it from being garbage collected.

This deviates from the normal framework structure because it is preferable to run timeout audits separately in order to avoid interference by other audit operations.

Author:

  • Tasos “Zapotek” Laskos <tasos.laskos@arachni-scanner.com>

Constant Summary collapse

TIMEOUT_OPTIONS =

Override user audit options that don't play nice with this technique.

{
    skip_original:          true,
    with_both_http_methods: false,
    parameter_names:        false,
    with_extra_parameter:   false,
    extensively:            false
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#timing_attack_remark_dataObject

Returns the value of attribute timing_attack_remark_data.



253
254
255
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 253

def timing_attack_remark_data
  @timing_attack_remark_data
end

Class Method Details

.add_phase_2_candidate(elem) ⇒ Object



118
119
120
121
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 118

def add_phase_2_candidate( elem )
    @phase_2_candidate_ids << elem
    @candidates_phase_2    << elem
end

.candidates_include?(candidate) ⇒ Boolean

Returns:

  • (Boolean)


114
115
116
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 114

def candidates_include?( candidate )
    @phase_2_candidate_ids.include? candidate
end

.deduplicateObject



101
102
103
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 101

def deduplicate
    @deduplicate = true
end

.deduplicate?Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 97

def deduplicate?
    @deduplicate
end

.do_not_deduplicateObject

Used just for specs of timing-attack checks.



106
107
108
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 106

def do_not_deduplicate
    @deduplicate = false
end

.has_candidates?Boolean

Returns:

  • (Boolean)


110
111
112
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 110

def has_candidates?
    @candidates_phase_2.any?
end

.payload_delay_from_options(options) ⇒ Object



142
143
144
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 142

def payload_delay_from_options( options )
    (options[:delay] / options[:timeout_divider]).to_s
end

.resetObject



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 73

def reset

    # We can track out own candidate state here, without registering it
    # with the global system State, because everything that happens
    # here is green-lit by #timing_attack_probe, which does register
    # its state as it uses #audit.
    #
    # Also, candidates will be consumed prior to a suspension, so when
    # we suspend and restore scans there will be no issue.

    @candidates_phase_2    = []
    @phase_2_candidate_ids = Support::LookUp::HashSet.new( hasher: :timeout_id )

    @candidates_phase_3    = []
    @phase_3_candidate_ids = Support::LookUp::HashSet.new( hasher: :timeout_id )

    @candidates_phase_4    = []
    @phase_4_candidate_ids = Support::LookUp::HashSet.new( hasher: :timeout_id )

    @logged = Support::LookUp::HashSet.new( hasher: :timeout_id )

    deduplicate
end

.runObject

Verifies and logs candidate elements.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 124

def run
    while !@candidates_phase_2.empty?
        analysis_phase_2( @candidates_phase_2.pop )
    end

    while (candidate = @candidates_phase_3.pop)
        next if Timeout.deduplicate? && logged?( candidate )

        analysis_phase_3( candidate )
    end

    while (candidate = @candidates_phase_4.pop)
        next if Timeout.deduplicate? && logged?( candidate )

        analysis_phase_4( candidate )
    end
end

.timeout_from_options(options) ⇒ Object



146
147
148
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 146

def timeout_from_options( options )
    options[:delay] + options[:add]
end

Instance Method Details

#dupObject



466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 466

def dup
    e = super
    return e if !@timing_attack_remark_data

    dupped_remark_data = {}
    @timing_attack_remark_data.each do |k, v|
        dupped_remark_data[k] = v.dup
    end

    e.timing_attack_remark_data = dupped_remark_data
    e
end

#ensure_responsiveness(limit = 120_000, prepend = '* ') ⇒ Bool

Submits self with a high timeout value and blocks until it gets a response.

This is to make sure that responsiveness has been restored before progressing further in the timeout analysis.

Parameters:

  • limit (Integer) (defaults to: 120_000)

    How many milliseconds to afford the server to respond.

Returns:

  • (Bool)

    `true` if server responds within the given time limit, `false` otherwise.



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 330

def ensure_responsiveness( limit = 120_000, prepend = '* ' )
    options = {
        timeout:           limit,
        mode:              :sync,
        response_max_size: 0
    }

    print_info "#{prepend}Waiting for the effects of the timing attack to " <<
        'wear off, this may take a while (max waiting time is ' <<
         "#{options[:timeout] / 1000.0} seconds)."

    response = timeout_control.submit( options )

    if response.timed_out? || response.partial?
        print_bad "#{prepend}Max waiting time exceeded."
        false
    else
        @timing_attack_remark_data[:stabilization_times] << response.time

        print_info "#{prepend}OK, got a response after #{response.time} seconds."
        true
    end
end

#initializeObject



255
256
257
258
259
260
261
262
263
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 255

def initialize(*)
    super

    @timing_attack_remark_data = {
        control_times:       [],
        stabilization_times: [],
        delays:              []
    }
end

#timeout_analysis(payloads, opts) ⇒ Bool

Performs timeout/time-delay analysis and logs an issue should there be one.

Parameters:

  • payloads (String, Array<String>, Hash{Symbol => <String, Array<String>>})

    Payloads to inject, if given:

    • String – Will inject the single payload.

    • Array – Will iterate over all payloads and inject them.

    • Hash – Expects Platform (as `Symbol`s ) for keys and Array of

      `payloads` for values. The applicable `payloads` will be
      {Platform::Manager#pick picked} from the hash based on
      {Element::Capabilities::Submittable#platforms applicable platforms}
      for the {Element::Capabilities::Submittable#action resource} to be audited.
      

    Delay stub `__TIME__` will be substituted with `timeout / timeout_divider`.

  • opts (Hash)

    Options as described in Mutable::MUTATION_OPTIONS with the specified extras.

Options Hash (opts):

  • :timeout (Integer)

    Milliseconds to wait for the request to complete.

  • :timeout_divider (Integer) — default: 1

    `__TIME__ = timeout / timeout_divider`

  • :add (Integer) — default: 0

    Add this integer to the expected time the request is supposed to take, in milliseconds.

Returns:

  • (Bool)

    `true` if the audit was scheduled successfully, `false` otherwise (like if the resource is out of scope).



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 293

def timeout_analysis( payloads, opts )
    return false if self.inputs.empty?

    if scope.out?
        print_debug 'Timeout analysis: Element is out of scope,' <<
                        " skipping: #{audit_id}"
        return false
    end

    timing_attack_probe( payloads, opts ) do |elem|
        next if Timeout.deduplicate? && Timeout.candidates_include?( elem )

        print_info 'Found a candidate for Phase 2 -- ' <<
            "#{elem.type.capitalize} input '#{elem.affected_input_name}' " <<
            "pointing to: #{elem.action}"
        print_verbose "Using: #{elem.affected_input_value.inspect}"

        Timeout.add_phase_2_candidate( elem )
    end

    true
end

#timeout_idObject



316
317
318
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 316

def timeout_id
    "#{audit_id( self.affected_input_value )}:#{self.affected_input_name}"
end

#timing_attack_probe(payloads, options, &block) ⇒ Object

Performs a simple probe for elements whose submission results in a response time that matches the delay criteria in `options`.

Parameters:

  • payloads (String, Array<String>, Hash{Symbol => <String, Array<String>>})

    Payloads to inject, if given:

    • String – Will inject the single payload.

    • Array – Will iterate over all payloads and inject them.

    • Hash – Expects Platform (as `Symbol`s ) for keys and Array of

      `payloads` for values. The applicable `payloads` will be
      {Platform::Manager#pick picked} from the hash based on
      {Element::Capabilities::Submittable#platforms applicable platforms}
      for the {Element::Capabilities::Submittable#action resource} to be audited.
      

    Delay stub `__TIME__` will be substituted with `timeout / timeout_divider`.

  • opts (Hash)

    Options as described in Mutable::MUTATION_OPTIONS with the specified extras.



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 358

def timing_attack_probe( payloads, options, &block )
    fail ArgumentError, 'Missing block' if !block_given?

    options                     = options.merge( TIMEOUT_OPTIONS )
    options[:delay]             = options.delete(:timeout)
    options[:timeout_divider] ||= 1
    options[:add]             ||= 0

    # Intercept each element mutation prior to it being submitted and
    # replace the '__TIME__' stub with the actual delay value.
    options[:each_mutation] = proc do |mutation|
        injected = mutation.affected_input_value

        # Preserve the placeholder (__TIME__) payload because it's going to
        # be needed for the verification phases...
        mutation.audit_options[:timing_string] = injected

        # ...but update it to use a real payload for this audit.
        mutation.affected_input_value = injected.
            gsub( '__TIME__', payload_delay_from_options( options ) )
    end

    # Ignore response bodies to preserve bandwidth since we don't care
    # about them anyways.
    options[:submit] = {
        response_max_size: 0,
        timeout:           timeout_from_options( options ),
    }

    if debug_level_2?
        print_debug_level_2 "#{options}"
    end

    audit( payloads, options ) do |response, mutation|
        next if !response.timed_out? || response.partial?

        mutation.timing_attack_remark_data[:delays] << options[:delay]
        block.call( mutation, response )
    end
end

#timing_attack_verify(delay, &block) ⇒ Object

Verifies that response times are controllable for elements picked by #timing_attack_probe.

  • Liveness check: Element is submitted as is with a very high timeout value, to make sure that (or wait until) the server is alive to #ensure_responsiveness.

  • Control check: Element is, again, submitted as is, although this time with a timeout value of `delay` to ensure that the server is stable enough to be checked.

    • If this fails the check is aborted.

  • Verification: Element is submitted with an increased delay to verify the vulnerability.

    • If verification succeeds the `block` is called.

  • Stabilize responsiveness: Wait for the effects of the timing attack to wear off by calling #ensure_responsiveness.

Parameters:

  • delay (Integer)
  • block (Block)


417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 417

def timing_attack_verify( delay, &block )
    fail ArgumentError, 'Missing block' if !block_given?

    options         = self.audit_options
    options[:delay] = delay

    # Actual value to use for the server-side delay operation.
    payload_delay = payload_delay_from_options( options )

    # Prepared payload, which will hopefully introduce a server-side delay.
    payload = options[:timing_string].gsub( '__TIME__', payload_delay )

    # Timeout value (in milliseconds) for the HTTP request.
    timeout = timeout_from_options( options )

    # Make sure we're starting off with a clean slate.
    ensure_responsiveness

    # This is the control; submits the element with its default (or sample,
    # if its defaults are empty) values and ensures that element submission
    # doesn't time out by default.
    #
    # If it does, then there's no way for us to test it reliably.
    if_timeout_control_check_ok seed, timeout do

        # Update our candidate mutation's affected input with the new payload.
        self.affected_input_value = payload

        print_verbose "  * Payload delay:   #{payload_delay}"
        print_verbose "  * Request timeout: #{timeout}"
        print_verbose "  * Payload:         #{payload.inspect}"

        submit( response_max_size: 0, timeout: timeout ) do |response|
            if !response.timed_out? || response.partial?
                print_info '* Verification failed.'
                print_verbose "  * Server responded in #{response.time} seconds."
                next
            end

            @timing_attack_remark_data[:delays] << timeout
            block.call( response )

            ensure_responsiveness
        end
    end

    http.run
end

#to_rpc_dataObject



479
480
481
# File 'lib/arachni/element/capabilities/analyzable/timeout.rb', line 479

def to_rpc_data
    super.tap { |data| data.delete 'timing_attack_remark_data' }
end