Module: Arachni::Element::Capabilities::Auditable::Timeout

Included in:
Arachni::Element::Capabilities::Auditable
Defined in:
lib/arachni/element/capabilities/auditable/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 times-out it is added to the Phase 2 queue.

    • Stabilization (#responsive?) – The element is submitted with its default values in order to wait until the effects of the timing attack have worn off.

  • Phase 2 (Timeout.timeout_analysis_phase_2) – Verifies the candidates. This is much more delicate so the concurrent requests are lowered to pairs.

    • Liveness test – 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 (#responsive?)

  • Phase 3 (Timeout.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 concurrent requests as possible for any given phase.

# Usage

  • Call #timeout_analysis to schedule requests for Phase 1.

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

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

Be sure to call Timeout.timeout_audit_run as soon as possible after Phase 1 as the candidate elements keep a reference to their auditor which will prevent it from being reaped by the garbage collector.

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.

If you want to be notified every time a timeout audit is performed you can pass a callback block to Timeout.on_timing_attacks.

Author:

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.add_timeout_candidate(elem) ⇒ Object



90
91
92
93
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 90

def @@parent.add_timeout_candidate( elem )
    @@timeout_audit_operations_cnt += 1
    @@timeout_candidates << elem
end

.add_timeout_phase3_candidate(elem) ⇒ Object



95
96
97
98
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 95

def @@parent.add_timeout_phase3_candidate( elem )
    @@timeout_audit_operations_cnt += 1
    @@timeout_candidates_phase3 << elem
end

.call_on_timing_blocks(res, elem) ⇒ Object



117
118
119
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 117

def @@parent.call_on_timing_blocks( res, elem )
    @@on_timing_attacks.each { |block| block.call( res, elem ) }
end

.current_timeout_audit_operations_cntInteger

Returns Amount of timeout-audit related operations (‘audit blocks + candidate elements`).

Returns:

  • (Integer)

    Amount of timeout-audit related operations (‘audit blocks + candidate elements`).



86
87
88
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 86

def @@parent.current_timeout_audit_operations_cnt
    @@timeout_candidates.size + @@timeout_candidates_phase3.size
end

.deduplicate?Boolean

Returns:

  • (Boolean)


217
218
219
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 217

def @@parent.deduplicate?
    @@deduplicate == 't'
end

.disable_deduplicationObject



209
210
211
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 209

def @@parent.disable_deduplication
    @@deduplicate = 'f'
end

.enable_deduplicationObject



213
214
215
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 213

def @@parent.enable_deduplication
    @@deduplicate = 't'
end

.included(mod) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
185
186
187
188
189
190
191
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
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
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 70

def self.included( mod )
    @@parent = mod

    # @return   [Set]
    #   Names of all loaded modules that use timing attacks.
    def @@parent.timeout_loaded_modules
        @@timeout_loaded_modules
    end

    def @@parent.timeout_candidates
        @@timeout_candidates
    end

    # @return   [Integer]
    #   Amount of timeout-audit related operations
    #   (`audit blocks + candidate elements`).
    def @@parent.current_timeout_audit_operations_cnt
        @@timeout_candidates.size + @@timeout_candidates_phase3.size
    end

    def @@parent.add_timeout_candidate( elem )
        @@timeout_audit_operations_cnt += 1
        @@timeout_candidates << elem
    end

    def @@parent.add_timeout_phase3_candidate( elem )
        @@timeout_audit_operations_cnt += 1
        @@timeout_candidates_phase3 << elem
    end

    # @return   [Bool]
    #   `true` if timeout attacks are currently running, `false` otherwise.
    def @@parent.running_timeout_attacks?
        @@running_timeout_attacks
    end

    # @param    [Block] block
    #   Block to be executed every time a timing attack is performed.
    def @@parent.on_timing_attacks( &block )
        @@on_timing_attacks << block
    end

    # @return   [Integer]    Amount of timeout-audit operations.
    def @@parent.timeout_audit_operations_cnt
        @@timeout_audit_operations_cnt
    end

    def @@parent.call_on_timing_blocks( res, elem )
        @@on_timing_attacks.each { |block| block.call( res, elem ) }
    end

    # Verifies and logs candidate elements.
    def @@parent.timeout_audit_run
        @@running_timeout_attacks = true

        while !@@timeout_candidates.empty?
            self.timeout_analysis_phase_2( @@timeout_candidates.pop )
        end

        while !@@timeout_candidates_phase3.empty?
            self.timeout_analysis_phase_3( @@timeout_candidates_phase3.pop )
        end
    end

    # (Called by {timeout_audit_run}, do *NOT* call manually.)
    #
    # Runs phase 2 of the timing attack auditing an individual element
    # (which passed phase 1) with a higher delay and timeout.
    #
    # * Liveness check: Element is submitted as is to make sure that the page is alive and responsive
    #   * If liveness check fails then phase 2 is aborted
    #   * If liveness check succeeds it progresses to verification
    # * Verification: Element is submitted with an increased delay to verify the vulnerability
    #   * If verification fails it aborts
    #   * If verification succeeds the issue is logged
    # * Stabilize responsiveness: Wait for the effects of the timing attack to wear off
    def @@parent.timeout_analysis_phase_2( elem )
        opts          = elem.opts
        opts[:delay] *= 2

        str = opts[:timing_string].dup
        str.gsub!( '__TIME__', (opts[:delay] / opts[:timeout_divider]).to_s )

        elem.auditable = elem.orig

        elem.print_status "Phase 2 for #{elem.type} input '#{elem.altered}'" <<
                              " with action #{elem.action}"

        elem.print_info '* Performing liveness check.'

        # This is the control; request the URL of the element to make sure
        # that the web page is responsive i.e won't time-out by default.
        elem.submit( timeout: opts[:delay] ) do |res|
            self.call_on_timing_blocks( res, elem )

            # Remove the timeout option set by the liveness check in order
            # to now affect later requests.
            elem.opts.delete( :timeout )

            if res.timed_out?
                elem.print_info '* Liveness check failed, aborting.'
                next
            end

            elem.print_info '* Liveness check was successful, progressing' <<
                                ' to verification.'

            opts[:skip_like] = proc { |m| m.altered != elem.altered }
            opts[:format]    = [Mutable::Format::STRAIGHT]
            opts[:silent]    = true

            elem.audit( str, opts ) do |c_res|
                if c_res.app_time <= (opts[:delay] + opts[:add]) / 1000.0
                    elem.print_info '* Verification failed.'
                    next
                end

                if deduplicate?
                    if @@timeout_candidate_phase3_ids.include?( elem.audit_id )
                        elem.print_info '* Duplicate, skipping.'
                        next
                    end

                    @@timeout_candidate_phase3_ids << elem.audit_id
                end

                elem.opts[:delay] = opts[:delay]

                elem.print_info '* Verification was successful, ' <<
                                'candidate can progress to Phase 3.'

                @@parent.add_timeout_phase3_candidate( elem )
                elem.responsive?
            end
        end

        elem.http.run
    end

    def @@parent.disable_deduplication
        @@deduplicate = 'f'
    end

    def @@parent.enable_deduplication
        @@deduplicate = 't'
    end

    def @@parent.deduplicate?
        @@deduplicate == 't'
    end

    def @@parent.timeout_analysis_phase_3( elem )
        opts          = elem.opts
        opts[:delay] *= 2

        str = opts[:timing_string].dup
        str.gsub!( '__TIME__', (opts[:delay] / opts[:timeout_divider]).to_s )

        elem.auditable = elem.orig

        elem.print_status "Phase 3 for #{elem.type} input '#{elem.altered}'" <<
                              " with action #{elem.action}"

        elem.print_info '* Performing liveness check.'

        # 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.submit( timeout: opts[:delay] ) do |res|
            self.call_on_timing_blocks( res, elem )

            if res.timed_out?
                elem.print_info '* Liveness check failed.'
                next
            end

            elem.print_info '* Liveness check was successful, progressing' <<
                            ' to verification.'

            opts[:skip_like] = proc { |m| m.altered != elem.altered }
            opts[:format]    = [Mutable::Format::STRAIGHT]
            opts[:silent]    = true

            elem.audit( str, opts ) do |c_res, c_opts|
                if c_res.app_time <= (opts[:delay] + opts[:add]) / 1000.0
                    elem.print_info '* Verification failed.'
                    next
                end

                elem.print_info '* Verification was successful.'
                elem.auditor.log( c_opts, c_res )
                elem.responsive?
            end
        end

        elem.http.run
    end

    def call_on_timing_blocks( res, elem )
        @@parent.call_on_timing_blocks( res, elem )
    end

    @@timeout_audit_operations_cnt ||= 0

    # Populated by timing attack phase 1 with candidate elements to be
    # verified by phase 2.
    @@timeout_candidates     ||= []
    @@timeout_candidate_ids  ||= ::Arachni::Support::LookUp::HashSet.new

    @@timeout_candidates_phase3    ||= []
    @@timeout_candidate_phase3_ids ||= ::Arachni::Support::LookUp::HashSet.new

    # Modules which have called the timing attack audit method
    # ({Arachni::Module::Auditor#audit_timeout}) we're interested in the
    # amount, not the names, and is used to determine scan progress.
    @@timeout_loaded_modules ||= Set.new

    @@on_timing_attacks      ||= []

    @@running_timeout_attacks ||= false

    @@deduplicate ||= 't'
end

.on_timing_attacks(&block) ⇒ Object

Parameters:

  • block (Block)

    Block to be executed every time a timing attack is performed.



108
109
110
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 108

def @@parent.on_timing_attacks( &block )
    @@on_timing_attacks << block
end

.resetObject



293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 293

def Timeout.reset
    @@timeout_audit_operations_cnt = 0

    @@timeout_candidates.clear
    @@timeout_candidate_ids.clear

    @@timeout_candidates_phase3.clear
    @@timeout_candidate_phase3_ids.clear

    @@timeout_loaded_modules.clear

    @@deduplicate = true
end

.running_timeout_attacks?Bool

Returns ‘true` if timeout attacks are currently running, `false` otherwise.

Returns:

  • (Bool)

    ‘true` if timeout attacks are currently running, `false` otherwise.



102
103
104
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 102

def @@parent.running_timeout_attacks?
    @@running_timeout_attacks
end

.timeout_analysis_phase_2(elem) ⇒ Object

(Called by timeout_audit_run, do NOT call manually.)

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

  • Liveness check: Element is submitted as is to make sure that the page is alive and responsive

    • If liveness check fails then phase 2 is aborted

    • If liveness check succeeds it progresses to verification

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

    • If verification fails it aborts

    • If verification succeeds the issue is logged

  • Stabilize responsiveness: Wait for the effects of the timing attack to wear off



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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 146

def @@parent.timeout_analysis_phase_2( elem )
    opts          = elem.opts
    opts[:delay] *= 2

    str = opts[:timing_string].dup
    str.gsub!( '__TIME__', (opts[:delay] / opts[:timeout_divider]).to_s )

    elem.auditable = elem.orig

    elem.print_status "Phase 2 for #{elem.type} input '#{elem.altered}'" <<
                          " with action #{elem.action}"

    elem.print_info '* Performing liveness check.'

    # This is the control; request the URL of the element to make sure
    # that the web page is responsive i.e won't time-out by default.
    elem.submit( timeout: opts[:delay] ) do |res|
        self.call_on_timing_blocks( res, elem )

        # Remove the timeout option set by the liveness check in order
        # to now affect later requests.
        elem.opts.delete( :timeout )

        if res.timed_out?
            elem.print_info '* Liveness check failed, aborting.'
            next
        end

        elem.print_info '* Liveness check was successful, progressing' <<
                            ' to verification.'

        opts[:skip_like] = proc { |m| m.altered != elem.altered }
        opts[:format]    = [Mutable::Format::STRAIGHT]
        opts[:silent]    = true

        elem.audit( str, opts ) do |c_res|
            if c_res.app_time <= (opts[:delay] + opts[:add]) / 1000.0
                elem.print_info '* Verification failed.'
                next
            end

            if deduplicate?
                if @@timeout_candidate_phase3_ids.include?( elem.audit_id )
                    elem.print_info '* Duplicate, skipping.'
                    next
                end

                @@timeout_candidate_phase3_ids << elem.audit_id
            end

            elem.opts[:delay] = opts[:delay]

            elem.print_info '* Verification was successful, ' <<
                            'candidate can progress to Phase 3.'

            @@parent.add_timeout_phase3_candidate( elem )
            elem.responsive?
        end
    end

    elem.http.run
end

.timeout_analysis_phase_3(elem) ⇒ Object



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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 221

def @@parent.timeout_analysis_phase_3( elem )
    opts          = elem.opts
    opts[:delay] *= 2

    str = opts[:timing_string].dup
    str.gsub!( '__TIME__', (opts[:delay] / opts[:timeout_divider]).to_s )

    elem.auditable = elem.orig

    elem.print_status "Phase 3 for #{elem.type} input '#{elem.altered}'" <<
                          " with action #{elem.action}"

    elem.print_info '* Performing liveness check.'

    # 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.submit( timeout: opts[:delay] ) do |res|
        self.call_on_timing_blocks( res, elem )

        if res.timed_out?
            elem.print_info '* Liveness check failed.'
            next
        end

        elem.print_info '* Liveness check was successful, progressing' <<
                        ' to verification.'

        opts[:skip_like] = proc { |m| m.altered != elem.altered }
        opts[:format]    = [Mutable::Format::STRAIGHT]
        opts[:silent]    = true

        elem.audit( str, opts ) do |c_res, c_opts|
            if c_res.app_time <= (opts[:delay] + opts[:add]) / 1000.0
                elem.print_info '* Verification failed.'
                next
            end

            elem.print_info '* Verification was successful.'
            elem.auditor.log( c_opts, c_res )
            elem.responsive?
        end
    end

    elem.http.run
end

.timeout_audit_operations_cntInteger

Returns Amount of timeout-audit operations.

Returns:

  • (Integer)

    Amount of timeout-audit operations.



113
114
115
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 113

def @@parent.timeout_audit_operations_cnt
    @@timeout_audit_operations_cnt
end

.timeout_audit_runObject

Verifies and logs candidate elements.



122
123
124
125
126
127
128
129
130
131
132
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 122

def @@parent.timeout_audit_run
    @@running_timeout_attacks = true

    while !@@timeout_candidates.empty?
        self.timeout_analysis_phase_2( @@timeout_candidates.pop )
    end

    while !@@timeout_candidates_phase3.empty?
        self.timeout_analysis_phase_3( @@timeout_candidates_phase3.pop )
    end
end

.timeout_candidatesObject



79
80
81
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 79

def @@parent.timeout_candidates
    @@timeout_candidates
end

.timeout_loaded_modulesSet

Returns Names of all loaded modules that use timing attacks.

Returns:

  • (Set)

    Names of all loaded modules that use timing attacks.



75
76
77
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 75

def @@parent.timeout_loaded_modules
    @@timeout_loaded_modules
end

Instance Method Details

#call_on_timing_blocks(res, elem) ⇒ Object



267
268
269
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 267

def call_on_timing_blocks( res, elem )
    @@parent.call_on_timing_blocks( res, elem )
end

#deduplicate?Boolean

Returns:

  • (Boolean)


315
316
317
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 315

def deduplicate?
    @@parent.deduplicate?
end

#disable_deduplicationObject



307
308
309
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 307

def disable_deduplication
    @@parent.disable_deduplication
end

#enable_deduplicationObject



311
312
313
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 311

def enable_deduplication
    @@parent.enable_deduplication
end

#responsive?(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.

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.



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 386

def responsive?( limit = 120_000, prepend = '* ' )
    d_opts = {
        skip_orig: true,
        redundant: true,
        timeout:   limit,
        silent:    true,
        async:     false
    }

    orig_opts = opts

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

    @auditable = @orig
    res = submit( d_opts ).response

    @opts.merge!( orig_opts )

    if res.timed_out?
        print_bad 'Max waiting time exceeded.'
        false
    else
        true
    end
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#pick picked} from the hash based on
      {Element::Base#platforms applicable platforms} for the
      {Base#action resource} to be audited.
      

    Delay placeholder ‘__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).



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/arachni/element/capabilities/auditable/timeout.rb', line 349

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

    if skip_path? self.action
        print_debug "Element's action matches skip rule, bailing out."
        return false
    end

    @@timeout_loaded_modules << @auditor.fancy_name

    delay = opts[:timeout]
    audit_timeout_debug_msg( 1, delay )
    timing_attack( payloads, opts ) do |elem|
        elem.auditor = @auditor

        if deduplicate?
            next if @@timeout_candidate_ids.include?( elem.audit_id )
            @@timeout_candidate_ids << elem.audit_id
        end

        print_info 'Found a candidate for Phase 2 -- ' <<
            "#{elem.type.capitalize} input '#{elem.altered}' at #{elem.action}"
        @@parent.add_timeout_candidate( elem )
    end

    true
end