Class: Arachni::Browser::Javascript

Inherits:
Object
  • Object
show all
Includes:
UI::Output, Utilities
Defined in:
lib/arachni/browser/javascript.rb,
lib/arachni/browser/javascript/proxy.rb,
lib/arachni/browser/javascript/dom_monitor.rb,
lib/arachni/browser/javascript/taint_tracer.rb,
lib/arachni/browser/javascript/taint_tracer/frame.rb,
lib/arachni/browser/javascript/taint_tracer/sink/base.rb,
lib/arachni/browser/javascript/taint_tracer/sink/data_flow.rb,
lib/arachni/browser/javascript/taint_tracer/sink/execution_flow.rb,
lib/arachni/browser/javascript/taint_tracer/frame/called_function.rb

Overview

Provides access to the Arachni::Browser's JavaScript environment, mainly helps group and organize functionality related to our custom Javascript interfaces.

Author:

Defined Under Namespace

Classes: DOMMonitor, Proxy, TaintTracer

Constant Summary collapse

TOKEN =
'arachni_js_namespace'
SCRIPT_BASE_URL =

Returns URL to use when requesting our custom JS scripts.

Returns:

  • (String)

    URL to use when requesting our custom JS scripts.

'http://javascript.browser.arachni/'
SCRIPT_LIBRARY =

Returns Filesystem directory containing the JS scripts.

Returns:

  • (String)

    Filesystem directory containing the JS scripts.

"#{File.dirname( __FILE__ )}/javascript/scripts/"
SCRIPT_SOURCES =
Dir.glob("#{SCRIPT_LIBRARY}*.js").inject({}) do |h, path|
    h.merge!( path => IO.read(path) )
end
NO_EVENTS_FOR_ELEMENTS =
Set.new(%w(
    base bdo br head html iframe meta param script style title link hr
))
EACH_DOM_ELEMENT_WITH_EVENTS_BATCH_SIZE =
300
EVENTS =
Set.new([
    :onclick,
    :ondblclick,
    :onmousedown,
    :onmousemove,
    :onmouseout,
    :onmouseover,
    :onmouseup,
    :onload,
    :onsubmit,
    :onselect,
    :onchange,
    :onfocus,
    :onblur,
    :onkeydown,
    :onkeypress,
    :onkeyup,
    :oninput,
    :onselect,
    :onchange,
    :onfocus,
    :onblur,
    :onkeydown,
    :onkeypress,
    :onkeyup,
    :oninput,
    :onchange,
    :onfocus,
    :onblur,
    :onfocus,
    :onblur,
    :onfocus,
    :onblur
])

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utilities

#available_port, available_port_mutex, #bytes_to_kilobytes, #bytes_to_megabytes, #caller_name, #caller_path, #cookie_decode, #cookie_encode, #cookies_from_file, #cookies_from_parser, #cookies_from_response, #exception_jail, #exclude_path?, #follow_protocol?, #form_decode, #form_encode, #forms_from_parser, #forms_from_response, #full_and_absolute_url?, #generate_token, #get_path, #hms_to_seconds, #html_decode, #html_encode, #include_path?, #links_from_parser, #links_from_response, #normalize_url, #page_from_response, #page_from_url, #parse_set_cookie, #path_in_domain?, #path_too_deep?, #port_available?, #rand_port, #random_seed, #redundant_path?, #regexp_array_match, #remove_constants, #request_parse_body, #seconds_to_hms, #skip_page?, #skip_path?, #skip_resource?, #skip_response?, #to_absolute, #uri_decode, #uri_encode, #uri_parse, #uri_parse_query, #uri_parser, #uri_rewrite

Methods included from UI::Output

#caller_location, #debug?, #debug_level, #debug_level_1?, #debug_level_2?, #debug_level_3?, #debug_level_4?, #debug_off, #debug_on, #disable_only_positives, #error_buffer, #error_log_fd, #error_logfile, #has_error_log?, #included, #log_error, #mute, #muted?, #only_positives, #only_positives?, #print_bad, #print_debug, #print_debug_backtrace, #print_debug_exception, #print_debug_level_1, #print_debug_level_2, #print_debug_level_3, #print_debug_level_4, #print_error, #print_error_backtrace, #print_exception, #print_info, #print_line, #print_ok, #print_status, #print_verbose, #reroute_to_file, #reroute_to_file?, reset_output_options, #set_error_logfile, #unmute, #verbose?, #verbose_off, #verbose_on

Constructor Details

#initialize(browser) ⇒ Javascript

Returns a new instance of Javascript.

Parameters:


105
106
107
108
109
# File 'lib/arachni/browser/javascript.rb', line 105

def initialize( browser )
    @browser      = browser
    @taint_tracer = TaintTracer.new( self )
    @dom_monitor  = DOMMonitor.new( self )
end

Instance Attribute Details

#custom_codeString

Returns Inject custom JS code right after the initialization of the custom JS interfaces.

Returns:

  • (String)

    Inject custom JS code right after the initialization of the custom JS interfaces.


90
91
92
# File 'lib/arachni/browser/javascript.rb', line 90

def custom_code
  @custom_code
end

#dom_monitorDOMMonitor (readonly)

Returns Proxy for the DOMMonitor JS interface.

Returns:


94
95
96
# File 'lib/arachni/browser/javascript.rb', line 94

def dom_monitor
  @dom_monitor
end

#taintString

Returns Taints to look for and trace in the JS data flow.

Returns:

  • (String)

    Taints to look for and trace in the JS data flow.


85
86
87
# File 'lib/arachni/browser/javascript.rb', line 85

def taint
  @taint
end

#taint_tracerTaintTracer (readonly)

Returns Proxy for the TaintTracer JS interface.

Returns:


98
99
100
# File 'lib/arachni/browser/javascript.rb', line 98

def taint_tracer
  @taint_tracer
end

#tokenString

Returns Token used to namespace the injected JS code and avoid clashes.

Returns:

  • (String)

    Token used to namespace the injected JS code and avoid clashes.


81
82
83
# File 'lib/arachni/browser/javascript.rb', line 81

def token
  @token
end

Class Method Details

.eventsObject


100
101
102
# File 'lib/arachni/browser/javascript.rb', line 100

def self.events
    EVENTS
end

Instance Method Details

#data_flow_sinksArray<Sink::DataFlow>

Returns JS data flow sink data.

Returns:

  • (Array<Sink::DataFlow>)

    JS data flow sink data.


229
230
231
232
# File 'lib/arachni/browser/javascript.rb', line 229

def data_flow_sinks
    return [] if !supported?
    taint_tracer.data_flow_sinks[@taint] || []
end

#debug_stub(*args) ⇒ String

Returns JS code which will call the TaintTracer.debug, browser-side JS function.

Returns:

  • (String)

    JS code which will call the TaintTracer.debug, browser-side JS function.


155
156
157
# File 'lib/arachni/browser/javascript.rb', line 155

def debug_stub( *args )
    taint_tracer.stub.function( :debug, *args )
end

#debugging_dataObject


217
218
219
220
# File 'lib/arachni/browser/javascript.rb', line 217

def debugging_data
    return [] if !supported?
    taint_tracer.debugging_data
end

#dom_digestString

Returns Digest of the current DOM tree (i.e. node names and their attributes without text-nodes).

Returns:

  • (String)

    Digest of the current DOM tree (i.e. node names and their attributes without text-nodes).


255
256
257
258
# File 'lib/arachni/browser/javascript.rb', line 255

def dom_digest
    return '' if !supported?
    dom_monitor.digest
end

#dom_event_digestString

Returns Digest of the available DOM events.

Returns:

  • (String)

    Digest of the available DOM events.


262
263
264
265
# File 'lib/arachni/browser/javascript.rb', line 262

def dom_event_digest
    return '' if !supported?
    dom_monitor.event_digest
end

#each_dom_element_with_events(whitelist = []) ⇒ Array<Hash>

Note:

Will not include custom events.

Returns Information about all DOM elements, including any registered event listeners.

Returns:

  • (Array<Hash>)

    Information about all DOM elements, including any registered event listeners.


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
# File 'lib/arachni/browser/javascript.rb', line 271

def each_dom_element_with_events( whitelist = [] )
    return if !supported?

    start      = 0
    batch_size = EACH_DOM_ELEMENT_WITH_EVENTS_BATCH_SIZE

    loop do
        elements = dom_monitor.elements_with_events( start, batch_size, whitelist )
        return if elements.empty?

        elements.each do |element|
            next if NO_EVENTS_FOR_ELEMENTS.include? element['tag_name']

            events = {}
            element['events'].each do |event, handlers|
                events[event.to_sym] = handlers
            end
            element['events'] = events

            yield element
        end

        return if elements.size < batch_size

        start += elements.size
    end
end

#execution_flow_sinksArray<Sink::ExecutionFlow>

Returns JS execution flow sink data.

Returns:

  • (Array<Sink::ExecutionFlow>)

    JS execution flow sink data.


223
224
225
226
# File 'lib/arachni/browser/javascript.rb', line 223

def execution_flow_sinks
    return [] if !supported?
    taint_tracer.execution_flow_sinks
end

#flush_data_flow_sinksArray<Sink::DataFlow>

Returns and clears #data_flow_sinks.

Returns:


241
242
243
244
# File 'lib/arachni/browser/javascript.rb', line 241

def flush_data_flow_sinks
    return [] if !supported?
    taint_tracer.flush_data_flow_sinks[@taint] || []
end

#flush_execution_flow_sinksArray<Sink::ExecutionFlow>

Returns and clears #execution_flow_sinks.

Returns:


235
236
237
238
# File 'lib/arachni/browser/javascript.rb', line 235

def flush_execution_flow_sinks
    return [] if !supported?
    taint_tracer.flush_execution_flow_sinks
end

#has_js_initializer?(response) ⇒ Bool

Returns true if the response HTTP::Message#body contains the code for the JS environment.

Parameters:

Returns:


129
130
131
# File 'lib/arachni/browser/javascript.rb', line 129

def has_js_initializer?( response )
    response.body.include? js_initialization_signal
end

#has_sinks?Boolean

Returns:

  • (Boolean)

211
212
213
214
# File 'lib/arachni/browser/javascript.rb', line 211

def has_sinks?
    return false if !supported?
    taint_tracer.has_sinks( @taint )
end

#html?(response) ⇒ Boolean

Returns:

  • (Boolean)

401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/arachni/browser/javascript.rb', line 401

def html?( response )
    return false if response.body.empty?

    # We only care about HTML responses.
    return false if !response.html?

    # The last check isn't fool-proof, so don't do it when loading the page
    # for the first time, but only when the page loads stuff via AJAX and whatnot.
    #
    # Well, we can be pretty sure that the root page will be HTML anyways.
    return true if @browser.last_url == response.url

    # Finally, verify that we're really working with markup (hopefully HTML)
    # and that the previous checks weren't just flukes matching some other
    # kind of document.
    #
    # For example, it may have been JSON with the wrong content-type that
    # includes HTML -- it happens.
    #
    # Beware, if there's a doctype in the beginning this will get fooled.
    if !Parser.markup?( response.body )
        print_debug "Does not look like HTML: #{response.url}"
        print_debug "\n#{response.body}"
        return false
    end

    true
end

#inject(response) ⇒ Object

Note:

Will update the Content-Length header field.

Parameters:

  • response (HTTP::Response)

    Installs our custom JS interfaces in the given response.

See Also:


335
336
337
338
339
340
341
342
343
344
345
346
347
348
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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/arachni/browser/javascript.rb', line 335

def inject( response )
    # Don't intercept our own stuff!
    return if response.url.start_with?( SCRIPT_BASE_URL )

    # If it's a JS file, update our JS interfaces in case it has stuff that
    # can be tracked.
    #
    # This is necessary because new files can be required dynamically.
    if javascript?( response )

        response.body.insert 0, <<-EOCODE
            #{js_comment}
            #{taint_tracer.stub.function( :update_tracers )};
            #{dom_monitor.stub.function( :update_trackers )};
        EOCODE
        response.body << ";\n"

    # Already has the JS initializer, so it's an HTML response; just update
    # taints and custom code.
    elsif has_js_initializer?( response )

        update_taints( response.body, response )
        update_custom_code( response.body )

    elsif html?( response )

        # Perform an update before each script.
        response.body.gsub!(
            /<script.*?>/i,
            "\\0\n
            #{js_comment}
            #{@taint_tracer.stub.function( :update_tracers )};
            #{@dom_monitor.stub.function( :update_trackers )};\n\n"
        )

        # Perform an update after each script.
        response.body.gsub!(
            /<\/script>/i,
            "\\0\n<script type=\"text/javascript\">" <<
                "#{@taint_tracer.stub.function( :update_tracers )};" <<
                "#{@dom_monitor.stub.function( :update_trackers )};" <<
                "</script> #{html_comment}\n"
        )

        # Include and initialize our JS interfaces.
        response.body.insert 0, <<-EOHTML
<script src="#{script_url_for( :polyfills )}"></script> #{html_comment}
<script src="#{script_url_for( :taint_tracer )}"></script> #{html_comment}
<script src="#{script_url_for( :dom_monitor )}"></script> #{html_comment}
<script>
#{wrapped_taint_tracer_initializer( response )}
#{js_initialization_signal};

#{wrapped_custom_code}
</script> #{html_comment}
        EOHTML

    end

    true
end

#javascript?(response) ⇒ Boolean

Returns:

  • (Boolean)

397
398
399
# File 'lib/arachni/browser/javascript.rb', line 397

def javascript?( response )
    response.headers.content_type.to_s.downcase.include?( 'javascript' )
end

#log_data_flow_sink_stub(*args) ⇒ String

Returns JS code which will call the TaintTracer.log_data_flow_sink, browser-side, JS function.

Returns:

  • (String)

    JS code which will call the TaintTracer.log_data_flow_sink, browser-side, JS function.


149
150
151
# File 'lib/arachni/browser/javascript.rb', line 149

def log_data_flow_sink_stub( *args )
    taint_tracer.stub.function( :log_data_flow_sink, *args )
end

#log_execution_flow_sink_stub(*args) ⇒ String

Returns JS code which will call the TaintTracer.log_execution_flow_sink, browser-side, JS function.

Returns:

  • (String)

    JS code which will call the TaintTracer.log_execution_flow_sink, browser-side, JS function.


142
143
144
# File 'lib/arachni/browser/javascript.rb', line 142

def log_execution_flow_sink_stub( *args )
    taint_tracer.stub.function( :log_execution_flow_sink, *args )
end

#ready?Bool

Returns true if our custom JS environment has been initialized.

Returns:

  • (Bool)

    true if our custom JS environment has been initialized.


184
185
186
187
188
189
# File 'lib/arachni/browser/javascript.rb', line 184

def ready?
    !!run( "return window._#{token}" )
rescue => e
    print_debug_exception e, 2
    false
end

#run(*args) ⇒ Object

Returns Result of script.

Parameters:

  • script (String)

    JS code to execute.

Returns:

  • (Object)

    Result of script.


196
197
198
# File 'lib/arachni/browser/javascript.rb', line 196

def run( *args )
    @browser.selenium.execute_script *args
end

#run_without_elements(*args) ⇒ Object

Executes the given code but unwraps Watir elements.

Parameters:

  • script (String)

    JS code to execute.

Returns:

  • (Object)

    Result of script.


207
208
209
# File 'lib/arachni/browser/javascript.rb', line 207

def run_without_elements( *args )
    unwrap_elements run( *args )
end

#serve(request, response) ⇒ Bool

Returns true if the request corresponded to a JS file and was served, false otherwise.

Parameters:

Returns:

  • (Bool)

    true if the request corresponded to a JS file and was served, false otherwise.

See Also:


317
318
319
320
321
322
323
324
325
326
# File 'lib/arachni/browser/javascript.rb', line 317

def serve( request, response )
    return false if !request.url.start_with?( SCRIPT_BASE_URL ) ||
        !(script = read_script( request.parsed_url.path ))

    response.code = 200
    response.body = script
    response.headers['content-type']   = 'text/javascript'
    response.headers['content-length'] = script.bytesize
    true
end

#set_element_idsObject

Sets a custom ID attribute to elements with events but without a proper ID.


247
248
249
250
# File 'lib/arachni/browser/javascript.rb', line 247

def set_element_ids
    return '' if !supported?
    dom_monitor.setElementIds
end

#supported?Bool

Returns true if there is support for our JS environment in the current page, false otherwise.

Returns:

  • (Bool)

    true if there is support for our JS environment in the current page, false otherwise.

See Also:


116
117
118
119
120
121
# File 'lib/arachni/browser/javascript.rb', line 116

def supported?
    # We won't have a response if the browser was steered towards an
    # out-of-scope resource.
    response = @browser.response
    response && has_js_initializer?( response )
end

#timeoutsArray<Array>

Returns Arguments for JS setTimeout calls.

Returns:

  • (Array<Array>)

    Arguments for JS setTimeout calls.


301
302
303
304
# File 'lib/arachni/browser/javascript.rb', line 301

def timeouts
    return [] if !supported?
    dom_monitor.timeouts
end

#wait_till_readyObject

Blocks until the browser page is ready.


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/arachni/browser/javascript.rb', line 160

def wait_till_ready
    print_debug_level_2 'Waiting for custom JS...'

    if !supported?
        print_debug_level_2 '...unsupported.'
        return
    end

    t = Time.now
    while !ready?
        sleep 0.1

        if Time.now - t > Options.browser_cluster.job_timeout
            print_debug_level_2 '...timed out.'
            return
        end
    end

    print_debug_level_2 '...done.'
    true
end