arachni 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +8 -4
- data/bin/arachni_console +1 -1
- data/components/checks/active/no_sql_injection.rb +4 -4
- data/components/checks/passive/common_directories/directories.txt +1 -0
- data/components/checks/passive/common_files/filenames.txt +1 -0
- data/components/plugins/login_script.rb +156 -0
- data/components/reporters/plugin_formatters/html/login_script.rb +48 -0
- data/components/reporters/plugin_formatters/stdout/login_script.rb +23 -0
- data/components/reporters/plugin_formatters/xml/login_script.rb +26 -0
- data/components/reporters/xml/schema.xsd +17 -0
- data/lib/arachni/browser.rb +7 -4
- data/lib/arachni/browser/javascript.rb +40 -4
- data/lib/arachni/browser/javascript/proxy.rb +1 -1
- data/lib/arachni/browser_cluster/worker.rb +14 -4
- data/lib/arachni/check/auditor.rb +24 -7
- data/lib/arachni/check/manager.rb +6 -0
- data/lib/arachni/framework.rb +54 -6
- data/lib/arachni/http/client.rb +41 -23
- data/lib/arachni/http/headers.rb +5 -1
- data/lib/arachni/http/message.rb +0 -7
- data/lib/arachni/http/request.rb +40 -32
- data/lib/arachni/http/response.rb +8 -1
- data/lib/arachni/platform/manager.rb +7 -0
- data/lib/arachni/rpc/server/framework/multi_instance.rb +1 -1
- data/lib/arachni/session.rb +88 -58
- data/lib/arachni/state/framework.rb +34 -5
- data/lib/arachni/support/profiler.rb +2 -0
- data/lib/arachni/uri.rb +2 -1
- data/lib/version +1 -1
- data/spec/arachni/browser/javascript_spec.rb +15 -0
- data/spec/arachni/check/manager_spec.rb +17 -0
- data/spec/arachni/framework_spec.rb +4 -2
- data/spec/arachni/http/client_spec.rb +1 -1
- data/spec/arachni/session_spec.rb +80 -37
- data/spec/arachni/state/framework_spec.rb +34 -1
- data/spec/arachni/uri_spec.rb +7 -0
- data/spec/components/plugins/login_script_spec.rb +157 -0
- data/spec/support/servers/plugins/login_script.rb +13 -0
- data/ui/cli/output.rb +26 -9
- metadata +11 -3
@@ -29,6 +29,10 @@ class Javascript
|
|
29
29
|
# Filesystem directory containing the JS scripts.
|
30
30
|
SCRIPT_LIBRARY = "#{File.dirname( __FILE__ )}/javascript/scripts/"
|
31
31
|
|
32
|
+
SCRIPT_SOURCES = Dir.glob("#{SCRIPT_LIBRARY}*.js").inject({}) do |h, path|
|
33
|
+
h.merge!( path => IO.read(path) )
|
34
|
+
end
|
35
|
+
|
32
36
|
NO_EVENTS_FOR_ELEMENTS = Set.new([
|
33
37
|
:base, :bdo, :br, :head, :html, :iframe, :meta, :param, :script, :style,
|
34
38
|
:title, :link
|
@@ -198,6 +202,17 @@ class Javascript
|
|
198
202
|
@browser.watir.execute_script script
|
199
203
|
end
|
200
204
|
|
205
|
+
# Executes the given code but unwraps Watir elements.
|
206
|
+
#
|
207
|
+
# @param [String] script
|
208
|
+
# JS code to execute.
|
209
|
+
#
|
210
|
+
# @return [Object]
|
211
|
+
# Result of `script`.
|
212
|
+
def run_without_elements( script )
|
213
|
+
unwrap_elements run( script )
|
214
|
+
end
|
215
|
+
|
201
216
|
# @return (see TaintTracer#debug)
|
202
217
|
def debugging_data
|
203
218
|
return [] if !supported?
|
@@ -361,18 +376,18 @@ class Javascript
|
|
361
376
|
def read_script( filename )
|
362
377
|
@scripts ||= {}
|
363
378
|
@scripts[filename] ||=
|
364
|
-
|
365
|
-
gsub( '_token', "_#{token}" )
|
379
|
+
SCRIPT_SOURCES[filesystem_path_for_script(filename)].
|
380
|
+
gsub( '_token', "_#{token}" )
|
366
381
|
end
|
367
382
|
|
368
383
|
def script_exists?( filename )
|
369
|
-
(
|
384
|
+
SCRIPT_SOURCES.include? filesystem_path_for_script( filename )
|
370
385
|
end
|
371
386
|
|
372
387
|
def filesystem_path_for_script( filename )
|
373
388
|
name = "#{SCRIPT_LIBRARY}#{filename}"
|
374
389
|
name << '.js' if !name.end_with?( '.js')
|
375
|
-
name
|
390
|
+
File.expand_path( name )
|
376
391
|
end
|
377
392
|
|
378
393
|
def script_url_for( filename )
|
@@ -384,6 +399,27 @@ class Javascript
|
|
384
399
|
"#{SCRIPT_BASE_URL}#{filename}.js"
|
385
400
|
end
|
386
401
|
|
402
|
+
def unwrap_elements( obj )
|
403
|
+
case obj
|
404
|
+
when Watir::Element
|
405
|
+
unwrap_element( obj )
|
406
|
+
|
407
|
+
when Array
|
408
|
+
obj.map { |e| unwrap_elements( e ) }
|
409
|
+
|
410
|
+
when Hash
|
411
|
+
obj.each { |k, v| obj[k] = unwrap_elements( v ) }
|
412
|
+
obj
|
413
|
+
|
414
|
+
else
|
415
|
+
obj
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
def unwrap_element( element )
|
420
|
+
element.html
|
421
|
+
end
|
422
|
+
|
387
423
|
end
|
388
424
|
end
|
389
425
|
end
|
@@ -70,7 +70,7 @@ class Proxy < BasicObject
|
|
70
70
|
# Javascript property/function.
|
71
71
|
# @param [Array] arguments
|
72
72
|
def call( function, *arguments )
|
73
|
-
@javascript.
|
73
|
+
@javascript.run_without_elements "return #{stub.write( function, *arguments )}"
|
74
74
|
end
|
75
75
|
alias :method_missing :call
|
76
76
|
|
@@ -82,7 +82,8 @@ class Worker < Arachni::Browser
|
|
82
82
|
|
83
83
|
# PhantomJS may have crashed (it happens sometimes) so make sure that
|
84
84
|
# we've got a live one before running the job.
|
85
|
-
|
85
|
+
# If we can't respawn, then bail out.
|
86
|
+
return if browser_respawn_if_necessary.nil?
|
86
87
|
|
87
88
|
begin
|
88
89
|
with_timeout @job_timeout do
|
@@ -234,7 +235,7 @@ class Worker < Arachni::Browser
|
|
234
235
|
end
|
235
236
|
|
236
237
|
def browser_respawn_if_necessary
|
237
|
-
return if !time_to_die? && browser_alive? &&
|
238
|
+
return false if !time_to_die? && browser_alive? &&
|
238
239
|
watir.windows.size < RESPAWN_WHEN_WINDOW_COUNT_REACHES
|
239
240
|
|
240
241
|
browser_respawn
|
@@ -257,9 +258,18 @@ class Worker < Arachni::Browser
|
|
257
258
|
@watir = nil
|
258
259
|
@selenium = nil
|
259
260
|
|
260
|
-
|
261
|
+
# Browser may fail to respawn but there's nothing we can do about
|
262
|
+
# that, just leave it dead and try again at the next job.
|
263
|
+
begin
|
264
|
+
@watir = ::Watir::Browser.new( selenium )
|
265
|
+
|
266
|
+
ensure_open_window
|
261
267
|
|
262
|
-
|
268
|
+
true
|
269
|
+
rescue Browser::Error::Spawn => e
|
270
|
+
print_error 'Could not respawn the browser, will try again at the next job.'
|
271
|
+
nil
|
272
|
+
end
|
263
273
|
end
|
264
274
|
|
265
275
|
def time_to_die?
|
@@ -380,7 +380,8 @@ module Auditor
|
|
380
380
|
#
|
381
381
|
# @see Page#audit?
|
382
382
|
def skip?( element )
|
383
|
-
return true if
|
383
|
+
return true if audited?( element.coverage_id ) ||
|
384
|
+
!page.audit_element?( element )
|
384
385
|
|
385
386
|
# Don't audit elements which have been already logged as vulnerable
|
386
387
|
# either by us or preferred checks.
|
@@ -390,6 +391,10 @@ module Auditor
|
|
390
391
|
klass = framework.checks[check]
|
391
392
|
next if !klass.info.include?(:issue)
|
392
393
|
|
394
|
+
# No point in doing the following heavy deduplication check if there
|
395
|
+
# are no issues logged to begin with.
|
396
|
+
next if klass.issue_counter == 0
|
397
|
+
|
393
398
|
if Data.issues.include?( klass.create_issue( vector: element ) )
|
394
399
|
return true
|
395
400
|
end
|
@@ -499,7 +504,10 @@ module Auditor
|
|
499
504
|
if !block_given?
|
500
505
|
audit_taint( payloads, opts )
|
501
506
|
else
|
502
|
-
each_candidate_element( opts[:elements] )
|
507
|
+
each_candidate_element( opts[:elements] ) do |e|
|
508
|
+
e.audit( payloads, opts, &block )
|
509
|
+
audited( e.coverage_id )
|
510
|
+
end
|
503
511
|
end
|
504
512
|
end
|
505
513
|
|
@@ -512,7 +520,10 @@ module Auditor
|
|
512
520
|
# @see Arachni::Element::Capabilities::Analyzable::Taint
|
513
521
|
def audit_taint( payloads, opts = {} )
|
514
522
|
opts = OPTIONS.merge( opts )
|
515
|
-
each_candidate_element( opts[:elements] )
|
523
|
+
each_candidate_element( opts[:elements] )do |e|
|
524
|
+
e.taint_analysis( payloads, opts )
|
525
|
+
audited( e.coverage_id )
|
526
|
+
end
|
516
527
|
end
|
517
528
|
|
518
529
|
# Audits elements using differential analysis and automatically logs results.
|
@@ -523,7 +534,10 @@ module Auditor
|
|
523
534
|
# @see Arachni::Element::Capabilities::Analyzable::Differential
|
524
535
|
def audit_differential( opts = {}, &block )
|
525
536
|
opts = OPTIONS.merge( opts )
|
526
|
-
each_candidate_element( opts[:elements] )
|
537
|
+
each_candidate_element( opts[:elements] ) do |e|
|
538
|
+
e.differential_analysis( opts, &block )
|
539
|
+
audited( e.coverage_id )
|
540
|
+
end
|
527
541
|
end
|
528
542
|
|
529
543
|
# Audits elements using timing attacks and automatically logs results.
|
@@ -534,7 +548,10 @@ module Auditor
|
|
534
548
|
# @see Arachni::Element::Capabilities::Analyzable::Timeout
|
535
549
|
def audit_timeout( payloads, opts = {} )
|
536
550
|
opts = OPTIONS.merge( opts )
|
537
|
-
each_candidate_element( opts[:elements] )
|
551
|
+
each_candidate_element( opts[:elements] ) do |e|
|
552
|
+
e.timeout_analysis( payloads, opts )
|
553
|
+
audited( e.coverage_id )
|
554
|
+
end
|
538
555
|
end
|
539
556
|
|
540
557
|
# Traces the taint in the given `resource` and passes each page to the
|
@@ -581,7 +598,7 @@ module Auditor
|
|
581
598
|
|
582
599
|
def prepare_each_element( elements, &block )
|
583
600
|
elements.each do |e|
|
584
|
-
next if e.inputs.empty?
|
601
|
+
next if skip?( e ) || e.inputs.empty?
|
585
602
|
|
586
603
|
d = e.dup
|
587
604
|
d.auditor = self
|
@@ -591,7 +608,7 @@ module Auditor
|
|
591
608
|
|
592
609
|
def prepare_each_dom_element( elements, &block )
|
593
610
|
elements.each do |e|
|
594
|
-
next if !e.dom || e.dom.inputs.empty?
|
611
|
+
next if skip?( e ) || !e.dom || e.dom.inputs.empty?
|
595
612
|
|
596
613
|
d = e.dup
|
597
614
|
d.dom.auditor = self
|
@@ -116,6 +116,10 @@ class Manager < Arachni::Component::Manager
|
|
116
116
|
# Check to run as a class.
|
117
117
|
# @param [Page] page
|
118
118
|
# Page to audit.
|
119
|
+
#
|
120
|
+
# @return [Bool]
|
121
|
+
# `true` if the check was ran (based on {Check::Auditor.check?}),
|
122
|
+
# `false` otherwise.
|
119
123
|
def run_one( check, page )
|
120
124
|
return false if !check.check?( page )
|
121
125
|
|
@@ -123,6 +127,8 @@ class Manager < Arachni::Component::Manager
|
|
123
127
|
check_new.prepare
|
124
128
|
check_new.run
|
125
129
|
check_new.clean_up
|
130
|
+
|
131
|
+
true
|
126
132
|
end
|
127
133
|
|
128
134
|
def self.reset
|
data/lib/arachni/framework.rb
CHANGED
@@ -272,8 +272,6 @@ class Framework
|
|
272
272
|
|
273
273
|
print_line
|
274
274
|
print_status "[HTTP: #{page.code}] #{page.dom.url}"
|
275
|
-
# print_object_space
|
276
|
-
# print_with_statistics
|
277
275
|
|
278
276
|
if page.platforms.any?
|
279
277
|
print_info "Identified as: #{page.platforms.to_a.join( ', ' )}"
|
@@ -293,6 +291,9 @@ class Framework
|
|
293
291
|
end
|
294
292
|
end
|
295
293
|
|
294
|
+
# Aside from plugins and whatnot, the Trainer hooks here to update the
|
295
|
+
# ElementFilter so that it'll know if new elements appear during the
|
296
|
+
# audit, so it's a big deal.
|
296
297
|
notify_on_page_audit( page )
|
297
298
|
|
298
299
|
@current_url = page.dom.url.to_s
|
@@ -300,22 +301,23 @@ class Framework
|
|
300
301
|
http.update_cookies( page.cookie_jar )
|
301
302
|
perform_browser_analysis( page )
|
302
303
|
|
304
|
+
# Remove elements which have already passed through here.
|
305
|
+
pre_audit_element_filter( page )
|
306
|
+
|
303
307
|
# Run checks which **don't** benefit from fingerprinting first, so that
|
304
308
|
# we can use the responses of their HTTP requests to fingerprint the
|
305
309
|
# webapp platforms, so that the checks which **do** benefit from knowing
|
306
310
|
# the remote platforms can run more efficiently.
|
307
311
|
ran = false
|
308
312
|
@checks.without_platforms.values.each do |check|
|
309
|
-
ran = true
|
310
|
-
check_page( check, page )
|
313
|
+
ran = true if check_page( check, page )
|
311
314
|
end
|
312
315
|
harvest_http_responses if ran
|
313
316
|
run_http = ran
|
314
317
|
|
315
318
|
ran = false
|
316
319
|
@checks.with_platforms.values.each do |check|
|
317
|
-
ran = true
|
318
|
-
check_page( check, page )
|
320
|
+
ran = true if check_page( check, page )
|
319
321
|
end
|
320
322
|
harvest_http_responses if ran
|
321
323
|
run_http ||= ran
|
@@ -1201,7 +1203,53 @@ class Framework
|
|
1201
1203
|
rescue => e
|
1202
1204
|
print_error "Error in #{check.to_s}: #{e.to_s}"
|
1203
1205
|
print_error_backtrace e
|
1206
|
+
false
|
1207
|
+
end
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
# Small but (sometimes) important optimization:
|
1211
|
+
#
|
1212
|
+
# Keep track of page elements which have already been passed to checks,
|
1213
|
+
# in order to filter them out and hopefully even avoid running checks
|
1214
|
+
# against pages with no new elements.
|
1215
|
+
#
|
1216
|
+
# It's not like there were going to be redundant audits anyways, because
|
1217
|
+
# each layer of the audit performs its own redundancy checks, but those
|
1218
|
+
# redundancy checks can introduce significant latencies when dealing
|
1219
|
+
# with pages with lots of elements.
|
1220
|
+
def pre_audit_element_filter( page )
|
1221
|
+
redundant_elements = {}
|
1222
|
+
page.elements.each do |e|
|
1223
|
+
next if !Options.audit.element?( e.type )
|
1224
|
+
next if e.is_a?( Cookie ) || e.is_a?( Header )
|
1225
|
+
|
1226
|
+
new_element = false
|
1227
|
+
redundant_elements[e.type] ||= []
|
1228
|
+
|
1229
|
+
if !state.element_checked?( e )
|
1230
|
+
state.element_checked e
|
1231
|
+
new_element = true
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
if e.respond_to?( :dom ) && e.dom
|
1235
|
+
if !state.element_checked?( e.dom )
|
1236
|
+
state.element_checked e.dom
|
1237
|
+
new_element = true
|
1238
|
+
end
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
next if new_element
|
1242
|
+
|
1243
|
+
redundant_elements[e.type] << e
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
# Remove redundant elements from the page cache, if there are thousands
|
1247
|
+
# of them then just skipping them during the audit will introduce latency.
|
1248
|
+
redundant_elements.each do |type, elements|
|
1249
|
+
page.send( "#{type}s=", page.send( "#{type}s" ) - elements )
|
1204
1250
|
end
|
1251
|
+
|
1252
|
+
page
|
1205
1253
|
end
|
1206
1254
|
|
1207
1255
|
def add_to_sitemap( page )
|
data/lib/arachni/http/client.rb
CHANGED
@@ -131,23 +131,21 @@ class Client
|
|
131
131
|
clear_observers if hooks_too
|
132
132
|
State.http.clear
|
133
133
|
|
134
|
-
|
135
|
-
|
136
|
-
@url = opts.url.to_s
|
134
|
+
@url = Options.url.to_s
|
137
135
|
@url = nil if @url.empty?
|
138
136
|
|
139
|
-
|
137
|
+
client_initialize
|
140
138
|
|
141
139
|
headers.merge!(
|
142
140
|
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
143
|
-
'User-Agent' =>
|
141
|
+
'User-Agent' => Options.http.user_agent
|
144
142
|
)
|
145
|
-
headers['From'] =
|
146
|
-
headers.merge!(
|
143
|
+
headers['From'] = Options.authorized_by if Options.authorized_by
|
144
|
+
headers.merge!( Options.http.request_headers )
|
147
145
|
|
148
|
-
cookie_jar.load(
|
149
|
-
update_cookies(
|
150
|
-
update_cookies(
|
146
|
+
cookie_jar.load( Options.http.cookie_jar_filepath ) if Options.http.cookie_jar_filepath
|
147
|
+
update_cookies( Options.http.cookies )
|
148
|
+
update_cookies( Options.http.cookie_string ) if Options.http.cookie_string
|
151
149
|
|
152
150
|
reset_burst_info
|
153
151
|
|
@@ -203,7 +201,7 @@ class Client
|
|
203
201
|
@burst_runtime = nil
|
204
202
|
|
205
203
|
begin
|
206
|
-
|
204
|
+
run_and_update_statistics
|
207
205
|
|
208
206
|
duped_after_run = observers_for( :after_run ).dup
|
209
207
|
observers_for( :after_run ).clear
|
@@ -258,7 +256,7 @@ class Client
|
|
258
256
|
|
259
257
|
# Aborts the running requests on a best effort basis.
|
260
258
|
def abort
|
261
|
-
exception_jail {
|
259
|
+
exception_jail { client_abort }
|
262
260
|
end
|
263
261
|
|
264
262
|
# @return [Integer]
|
@@ -688,11 +686,11 @@ class Client
|
|
688
686
|
false
|
689
687
|
end
|
690
688
|
|
691
|
-
def
|
689
|
+
def run_and_update_statistics
|
692
690
|
@running = true
|
693
691
|
|
694
692
|
reset_burst_info
|
695
|
-
|
693
|
+
client_run
|
696
694
|
|
697
695
|
@queue_size = 0
|
698
696
|
@running = false
|
@@ -700,7 +698,7 @@ class Client
|
|
700
698
|
@burst_runtime += Time.now - @burst_runtime_start
|
701
699
|
@total_runtime += @burst_runtime
|
702
700
|
end
|
703
|
-
|
701
|
+
|
704
702
|
def reset_burst_info
|
705
703
|
@burst_response_time_sum = 0
|
706
704
|
@burst_response_count = 0
|
@@ -771,20 +769,40 @@ class Client
|
|
771
769
|
|
772
770
|
return if request.blocking?
|
773
771
|
|
772
|
+
if client_queue( request )
|
773
|
+
@queue_size += 1
|
774
|
+
|
775
|
+
if emergency_run?
|
776
|
+
print_info 'Request queue reached its maximum size, performing an emergency run.'
|
777
|
+
run_and_update_statistics
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
781
|
+
request
|
782
|
+
end
|
783
|
+
|
784
|
+
def client_initialize
|
785
|
+
@hydra = Typhoeus::Hydra.new(
|
786
|
+
max_concurrency: Options.http.request_concurrency || MAX_CONCURRENCY
|
787
|
+
)
|
788
|
+
end
|
789
|
+
|
790
|
+
def client_run
|
791
|
+
@hydra.run
|
792
|
+
end
|
793
|
+
|
794
|
+
def client_abort
|
795
|
+
@hydra.abort
|
796
|
+
end
|
797
|
+
|
798
|
+
def client_queue( request )
|
774
799
|
if request.high_priority?
|
775
800
|
@hydra.queue_front( request.to_typhoeus )
|
776
801
|
else
|
777
802
|
@hydra.queue( request.to_typhoeus )
|
778
803
|
end
|
779
804
|
|
780
|
-
|
781
|
-
|
782
|
-
if emergency_run?
|
783
|
-
print_info 'Request queue reached its maximum size, performing an emergency run.'
|
784
|
-
hydra_run
|
785
|
-
end
|
786
|
-
|
787
|
-
request
|
805
|
+
true
|
788
806
|
end
|
789
807
|
|
790
808
|
def emergency_run?
|