arachni 0.2.4 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/CHANGELOG.md +33 -0
  2. data/README.md +2 -4
  3. data/Rakefile +15 -4
  4. data/bin/arachni +0 -0
  5. data/bin/arachni_web +0 -0
  6. data/bin/arachni_web_autostart +0 -0
  7. data/bin/arachni_xmlrpc +0 -0
  8. data/bin/arachni_xmlrpcd +0 -0
  9. data/bin/arachni_xmlrpcd_monitor +0 -0
  10. data/lib/arachni.rb +1 -1
  11. data/lib/framework.rb +36 -6
  12. data/lib/http.rb +12 -5
  13. data/lib/module/auditor.rb +482 -59
  14. data/lib/module/base.rb +17 -0
  15. data/lib/module/manager.rb +26 -2
  16. data/lib/module/trainer.rb +1 -12
  17. data/lib/module/utilities.rb +12 -0
  18. data/lib/parser/auditable.rb +8 -3
  19. data/lib/parser/elements.rb +11 -0
  20. data/lib/parser/page.rb +3 -1
  21. data/lib/parser/parser.rb +130 -18
  22. data/lib/rpc/xml/server/dispatcher.rb +21 -0
  23. data/lib/spider.rb +141 -82
  24. data/lib/ui/cli/cli.rb +2 -3
  25. data/lib/ui/web/addon_manager.rb +273 -0
  26. data/lib/ui/web/addons/autodeploy.rb +172 -0
  27. data/lib/ui/web/addons/autodeploy/lib/manager.rb +291 -0
  28. data/lib/ui/web/addons/autodeploy/views/index.erb +124 -0
  29. data/lib/ui/web/addons/sample.rb +78 -0
  30. data/lib/ui/web/addons/sample/views/index.erb +4 -0
  31. data/lib/ui/web/addons/scheduler.rb +139 -0
  32. data/lib/ui/web/addons/scheduler/views/index.erb +131 -0
  33. data/lib/ui/web/addons/scheduler/views/options.erb +93 -0
  34. data/lib/ui/web/dispatcher_manager.rb +80 -13
  35. data/lib/ui/web/instance_manager.rb +87 -0
  36. data/lib/ui/web/scheduler.rb +166 -0
  37. data/lib/ui/web/server.rb +142 -202
  38. data/lib/ui/web/server/public/js/jquery-ui-timepicker.js +985 -0
  39. data/lib/ui/web/server/public/plugins/sample/style.css +0 -0
  40. data/lib/ui/web/server/public/style.css +42 -0
  41. data/lib/ui/web/server/views/addon.erb +15 -0
  42. data/lib/ui/web/server/views/addons.erb +46 -0
  43. data/lib/ui/web/server/views/dispatchers.erb +1 -1
  44. data/lib/ui/web/server/views/instance.erb +9 -11
  45. data/lib/ui/web/server/views/layout.erb +14 -1
  46. data/lib/ui/web/server/views/welcome.erb +7 -6
  47. data/lib/ui/web/utilities.rb +134 -0
  48. data/modules/audit/code_injection_timing.rb +6 -2
  49. data/modules/audit/code_injection_timing/payloads.txt +2 -2
  50. data/modules/audit/os_cmd_injection_timing.rb +7 -3
  51. data/modules/audit/os_cmd_injection_timing/payloads.txt +1 -1
  52. data/modules/audit/sqli_blind_rdiff.rb +18 -233
  53. data/modules/audit/sqli_blind_rdiff/payloads.txt +5 -0
  54. data/modules/audit/sqli_blind_timing.rb +9 -2
  55. data/path_extractors/anchors.rb +1 -1
  56. data/path_extractors/forms.rb +1 -1
  57. data/path_extractors/frames.rb +1 -1
  58. data/path_extractors/generic.rb +1 -1
  59. data/path_extractors/links.rb +1 -1
  60. data/path_extractors/meta_refresh.rb +1 -1
  61. data/path_extractors/scripts.rb +1 -1
  62. data/path_extractors/sitemap.rb +1 -1
  63. data/plugins/proxy/server.rb +3 -2
  64. data/plugins/waf_detector.rb +0 -3
  65. metadata +37 -34
  66. data/lib/anemone/cookie_store.rb +0 -35
  67. data/lib/anemone/core.rb +0 -371
  68. data/lib/anemone/exceptions.rb +0 -5
  69. data/lib/anemone/http.rb +0 -144
  70. data/lib/anemone/page.rb +0 -338
  71. data/lib/anemone/page_store.rb +0 -160
  72. data/lib/anemone/storage.rb +0 -34
  73. data/lib/anemone/storage/base.rb +0 -75
  74. data/lib/anemone/storage/exceptions.rb +0 -15
  75. data/lib/anemone/storage/mongodb.rb +0 -89
  76. data/lib/anemone/storage/pstore.rb +0 -50
  77. data/lib/anemone/storage/redis.rb +0 -90
  78. data/lib/anemone/storage/tokyo_cabinet.rb +0 -57
  79. data/lib/anemone/tentacle.rb +0 -40
data/CHANGELOG.md CHANGED
@@ -1,6 +1,39 @@
1
1
 
2
2
  # ChangeLog
3
3
 
4
+ ## Version 0.3 _(July 26, 2011)_
5
+ - HTTP client
6
+ - Fixed race condition in timeout options.
7
+ - Spider (**New**)
8
+ - Replaced Anemone with a lightweight custom-written spider.
9
+ - WebUI
10
+ - Major refactoring.
11
+ - Improved handling of connection errors during scan progress updates.
12
+ - Added support for add-ons. (**New**)
13
+ - Add-ons (**New**)
14
+ - Scan scheduler
15
+ - Auto-deploy -- Automatically converts any SSH enabled Linux box into an Arachni Dispatcher.
16
+ - Fixed bug when IP addresses are used, instead of hostnames, for the Dispatchers.
17
+ - Parser
18
+ - Form action attributes are now sanitized using iterative URI decoding.
19
+ - Link variables are extracted before URL sanitization takes place in order to keep values with URL-encoded characters intact.
20
+ - The link variables of any current page's URL are now pushed to 'page.links'.
21
+ - Auditor
22
+ - Abstracted the rDiff audit methods from the "Blind (rDiff) SQL Injection" module and moved them in the Auditor.
23
+ - Timing attack technique has been greatly improved and all timing attacks are now scheduled to run at the end of the scan.
24
+ - Modules
25
+ - API
26
+ - Added the "redundant()" method -- Allows a module to prevents itself from auditting elements that have been previously logged by other modules.
27
+ - Modules are now passed an instance of the framework.
28
+ - Audit
29
+ - Blind (rDiff) SQL Injection
30
+ - Updated to support all element types (Links, Forms, Cookies, Headers).
31
+ - Optimized using the new "redundant()" method -- It will no longer audit elements that have been previously logged by the 'sqli' or 'sqli_blind_rdiff' modules.
32
+ - OS command injection (timing)
33
+ - Optimized using the new "redundant()" method -- It will no longer audit elements that have been previously logged by the 'os_cmd_injection' module.
34
+ - Code injection (timing)
35
+ - Optimized using the new "redundant()" method -- It will no longer audit elements that have been previously logged by the 'code_injection' module.
36
+
4
37
  ## Version 0.2.4 _(July 1, 2011)_
5
38
  - HTTP
6
39
  - Implemented a 10s time-out [Issue #40]
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <table>
3
3
  <tr>
4
4
  <th>Version</th>
5
- <td>0.2.4</td>
5
+ <td>0.3</td>
6
6
  </tr>
7
7
  <tr>
8
8
  <th>Homepage</th>
@@ -107,13 +107,10 @@ From a user's or a component developer's point of view everything appears simple
107
107
 
108
108
  ### Website Crawler
109
109
 
110
- The crawler is provided by a modified version of [Anemone](http://anemone.rubyforge.org/).
111
-
112
110
  - Filters for redundant pages like galleries, catalogs, etc based on regular expressions and counters.
113
111
  - URL exclusion filter based on regular expressions.
114
112
  - URL inclusion filter based on regular expressions.
115
113
  - Can optionally follow subdomains.
116
- - Adjustable depth limit.
117
114
  - Adjustable link count limit.
118
115
  - Adjustable redirect limit.
119
116
  - Modular path extraction via "Path Extractor" components.
@@ -125,6 +122,7 @@ Can extract and analyze:
125
122
  - Forms
126
123
  - Links
127
124
  - Cookies
125
+ - Headers
128
126
 
129
127
  The analyzer can graciously handle badly written HTML code due to a combination of regular expression analysis and the [Nokogiri](http://nokogiri.org/) HTML parser.
130
128
 
data/Rakefile CHANGED
@@ -54,6 +54,7 @@ desc "Cleaning report and log files."
54
54
  task :clean do
55
55
 
56
56
  sh "rm *.afr || true"
57
+ sh "rm *.gem || true"
57
58
  sh "rm logs/XMLRPC* || true"
58
59
  sh "rm lib/ui/web/server/db/log.db || true"
59
60
  sh "rm lib/ui/web/server/db/default.db || true"
@@ -61,15 +62,26 @@ task :clean do
61
62
  end
62
63
 
63
64
 
65
+ #
66
+ # Building
67
+ #
68
+ desc "Build the arachni gem."
69
+ task :build => [ :clean ] do
70
+
71
+ require File.expand_path( File.dirname( __FILE__ ) ) + '/lib/arachni'
72
+
73
+ sh "gem build arachni.gemspec"
74
+ end
75
+
76
+
64
77
  #
65
78
  # Installing
66
79
  #
67
80
  desc "Build and install the arachni gem."
68
- task :install do
81
+ task :install => [ :build ] do
69
82
 
70
83
  require File.expand_path( File.dirname( __FILE__ ) ) + '/lib/arachni'
71
84
 
72
- sh "gem build arachni.gemspec"
73
85
  sh "gem install arachni-#{Arachni::VERSION}.gem"
74
86
  end
75
87
 
@@ -78,10 +90,9 @@ end
78
90
  # Publishing
79
91
  #
80
92
  desc "Push a new version to Gemcutter"
81
- task :publish do
93
+ task :publish => [ :build ] do
82
94
 
83
95
  require File.expand_path( File.dirname( __FILE__ ) ) + '/lib/arachni'
84
96
 
85
- sh "gem build arachni.gemspec"
86
97
  sh "gem push arachni-#{Arachni::VERSION}.gem"
87
98
  end
data/bin/arachni CHANGED
File without changes
data/bin/arachni_web CHANGED
File without changes
File without changes
data/bin/arachni_xmlrpc CHANGED
File without changes
data/bin/arachni_xmlrpcd CHANGED
File without changes
File without changes
data/lib/arachni.rb CHANGED
@@ -11,6 +11,6 @@
11
11
  module Arachni
12
12
 
13
13
  # the universal system version
14
- VERSION = '0.2.4'
14
+ VERSION = '0.3'
15
15
 
16
16
  end
data/lib/framework.rb CHANGED
@@ -71,7 +71,7 @@ class Framework
71
71
  include Arachni::Mixins::Observable
72
72
 
73
73
  # the version of *this* class
74
- REVISION = '0.2.1'
74
+ REVISION = '0.2.3'
75
75
 
76
76
  #
77
77
  # Instance options
@@ -210,24 +210,40 @@ class Framework
210
210
  end
211
211
 
212
212
  curr_avg = 0
213
- if http.curr_res_cnt > 0
213
+ if http.curr_res_cnt > 0 && http.curr_res_time > 0
214
214
  curr_avg = (http.curr_res_cnt / http.curr_res_time).to_i.to_s
215
215
  end
216
216
 
217
+ avg = 0
218
+ if res_cnt > 0
219
+ avg = ( res_cnt / @opts.delta_time ).to_i.to_s
220
+ end
221
+
222
+ progress = (Float( @auditmap.size ) / @sitemap.size) * 100
223
+
224
+ if Arachni::Module::Auditor.timeout_loaded_modules.size > 0 &&
225
+ Arachni::Module::Auditor.timeout_audit_blocks.size > 0
226
+
227
+ progress /= 2
228
+ progress += ( Float( Arachni::Module::Auditor.timeout_loaded_modules.size ) /
229
+ Arachni::Module::Auditor.timeout_audit_blocks.size ) * 50
230
+ end
231
+
217
232
  return {
218
233
  :requests => req_cnt,
219
234
  :responses => res_cnt,
220
235
  :time_out_count => http.time_out_count,
221
236
  :time => audit_store.delta_time,
222
- :avg => ( res_cnt / @opts.delta_time ).to_i.to_s,
237
+ :avg => avg,
223
238
  :sitemap_size => @sitemap.size,
224
239
  :auditmap_size => @auditmap.size,
240
+ :progress => progress.to_s[0...5],
225
241
  :curr_res_time => http.curr_res_time,
226
242
  :curr_res_cnt => http.curr_res_cnt,
227
243
  :curr_avg => curr_avg,
228
244
  :average_res_time => http.average_res_time,
229
- :max_concurrency => http.max_concurrency,
230
- :current_page => @current_url
245
+ :max_concurrency => http.max_concurrency,
246
+ :current_page => @current_url
231
247
  }
232
248
  end
233
249
 
@@ -250,12 +266,21 @@ class Framework
250
266
  @spider.run {
251
267
  |page|
252
268
 
253
- @sitemap |= @spider.pages
269
+ @sitemap |= @spider.sitemap
254
270
 
255
271
  @page_queue << page
256
272
  audit_queue if !@opts.spider_first
257
273
  }
258
274
 
275
+ exception_jail {
276
+ if !Arachni::Module::Auditor.timeout_audit_blocks.empty?
277
+ print_line
278
+ print_status( 'Running timing attacks.' )
279
+ print_info( '---------------------------------------' )
280
+ Arachni::Module::Auditor.timeout_audit_run
281
+ end
282
+ }
283
+
259
284
  audit_queue
260
285
 
261
286
  if( @opts.http_harvest_last )
@@ -428,12 +453,14 @@ class Framework
428
453
  end
429
454
 
430
455
  def pause!
456
+ @spider.pause! if @spider
431
457
  @paused << caller
432
458
  return true
433
459
  end
434
460
 
435
461
  def resume!
436
462
  @paused.delete( caller )
463
+ @spider.resume! if @spider
437
464
  return true
438
465
  end
439
466
 
@@ -591,10 +618,13 @@ class Framework
591
618
 
592
619
  # instantiate the module
593
620
  mod_new = mod.new( page )
621
+ mod_new.set_framework( self )
594
622
 
595
623
  mod_new.prepare
596
624
  mod_new.run
597
625
  mod_new.clean_up
626
+ rescue SystemExit
627
+ raise
598
628
  rescue Exception => e
599
629
  print_error( 'Error in ' + mod.to_s + ': ' + e.to_s )
600
630
  print_debug_backtrace( e )
data/lib/http.rb CHANGED
@@ -34,7 +34,7 @@ require Options.instance.dir['lib'] + 'mixins/observable'
34
34
  # @author: Tasos "Zapotek" Laskos
35
35
  # <tasos.laskos@gmail.com>
36
36
  # <zapotek@segfault.gr>
37
- # @version: 0.2.5
37
+ # @version: 0.2.6
38
38
  #
39
39
  class HTTP
40
40
 
@@ -129,7 +129,7 @@ class HTTP
129
129
  :user_agent => opts.user_agent,
130
130
  :follow_location => false,
131
131
  :disable_ssl_peer_verification => true,
132
- :timeout => 10000
132
+ :timeout => 50000
133
133
  }.merge( proxy_opts )
134
134
 
135
135
  @request_count = 0
@@ -246,7 +246,7 @@ class HTTP
246
246
  print_debug( '------------' )
247
247
 
248
248
  if res.timed_out?
249
- print_error( 'Request timed-out! -- ID# ' + res.request.id.to_s )
249
+ # print_error( 'Request timed-out! -- ID# ' + res.request.id.to_s )
250
250
  @time_out_count += 1
251
251
  end
252
252
 
@@ -320,6 +320,7 @@ class HTTP
320
320
  params = opts[:params] || {}
321
321
  remove_id = opts[:remove_id]
322
322
  train = opts[:train]
323
+ timeout = opts[:timeout]
323
324
 
324
325
  follow_location = opts[:follow_location] || false
325
326
 
@@ -363,9 +364,10 @@ class HTTP
363
364
  :headers => headers,
364
365
  :params => cparams.empty? ? nil : cparams,
365
366
  :follow_location => follow_location,
366
- :timeout => opts[:timeout]
367
367
  }.merge( @opts )
368
368
 
369
+ opts[:timeout] = timeout if timeout
370
+
369
371
  req = Typhoeus::Request.new( curl, opts )
370
372
  req.train! if train
371
373
 
@@ -391,6 +393,7 @@ class HTTP
391
393
 
392
394
  params = opts[:params]
393
395
  train = opts[:train]
396
+ timeout = opts[:timeout]
394
397
 
395
398
  async = opts[:async]
396
399
  async = true if async == nil
@@ -405,8 +408,8 @@ class HTTP
405
408
  :headers => headers,
406
409
  :params => params,
407
410
  :follow_location => false,
408
- :timeout => opts[:timeout]
409
411
  }.merge( @opts )
412
+ opts[:timeout] = timeout if timeout
410
413
 
411
414
  req = Typhoeus::Request.new( normalize_url( url ), opts )
412
415
  req.train! if train
@@ -474,6 +477,8 @@ class HTTP
474
477
  cookies = opts[:params] || {}
475
478
  # params = opts[:params]
476
479
  train = opts[:train]
480
+ timeout = opts[:timeout]
481
+
477
482
 
478
483
  async = opts[:async]
479
484
  async = true if async == nil
@@ -492,6 +497,8 @@ class HTTP
492
497
  # :params => params
493
498
  :timeout => opts[:timeout]
494
499
  }.merge( @opts )
500
+ opts[:timeout] = timeout if timeout
501
+
495
502
 
496
503
  req = Typhoeus::Request.new( normalize_url( url ), opts )
497
504
  req.train! if train
@@ -14,16 +14,42 @@ module Module
14
14
  #
15
15
  # Auditor module
16
16
  #
17
- # Included by {Module::Base}.<br/>
18
- # Includes audit methods.
17
+ # Included by {Module::Base} and provides abstract audit methods.
18
+ #
19
+ # There are 3 main types of audit techniques available:
20
+ # * Pattern matching -- {#audit}
21
+ # * Timing attacks -- {#audit_timeout}
22
+ # * Differential analysis attacks -- {#audit_rdiff}
23
+ #
19
24
  #
20
25
  # @author: Tasos "Zapotek" Laskos
21
26
  # <tasos.laskos@gmail.com>
22
27
  # <zapotek@segfault.gr>
23
- # @version: 0.2.3
28
+ # @version: 0.3
24
29
  #
25
30
  module Auditor
26
31
 
32
+ def self.included( mod )
33
+ # @@__timeout_audited ||= Set.new
34
+
35
+ # holds timing-attack performing Procs to be run after all
36
+ # non-tming-attack modules have finished.
37
+ @@__timeout_audit_blocks ||= Queue.new
38
+
39
+ # populated by timing attack phase 1 with
40
+ # candidate elements to be verified by phase 2
41
+ @@__timeout_candidates ||= Queue.new
42
+
43
+ # modules which have called the timing attack audit mthod (audit_timeout)
44
+ # we're interested in the amount, not the names, and is used to
45
+ # determine scan progress
46
+ @@__timeout_loaded_modules ||= Set.new
47
+
48
+ # the rdiff attack performs it own redundancy checks so we need this to
49
+ # keep track audited elements
50
+ @@__rdiff_audited ||= Set.new
51
+ end
52
+
27
53
  #
28
54
  # Holds constant bitfields that describe the preferred formatting
29
55
  # of injection strings.
@@ -127,13 +153,15 @@ module Auditor
127
153
  }
128
154
 
129
155
  #
130
- # Matches the HTML in @page.html to an array of regular expressions
131
- # and logs the results.
156
+ # Matches the "string" (default string is the HTML code in @page.html) to
157
+ # an array of regular expressions and logs the results.
158
+ #
159
+ # For good measure, regexps will also be run against the page headers (@page.response_headers).
132
160
  #
133
- # @param [Array<Regexp>] regexps
134
- # @param [String] string
135
- # @param [Block] block block to verify matches before logging
136
- # must return true/false
161
+ # @param [Array<Regexp>] regexps array of regular expressions to be tested
162
+ # @param [String] string string to
163
+ # @param [Block] block block to verify matches before logging,
164
+ # must return true/false
137
165
  #
138
166
  def match_and_log( regexps, string = @page.html, &block )
139
167
 
@@ -184,10 +212,10 @@ module Auditor
184
212
  end
185
213
 
186
214
  #
187
- # Logs a vulnerability based on a regular expression and it's matched string
215
+ # Populates and logs an {Arachni::Issue} based on data from "opts" and "res".
188
216
  #
189
- # @param [Regexp] regexp
190
- # @param [String] match
217
+ # @param [Hash] opts as passed to blocks by audit methods
218
+ # @param [Typhoeus::Response] res defaults to @page data
191
219
  #
192
220
  def log( opts, res = nil )
193
221
 
@@ -246,7 +274,11 @@ module Auditor
246
274
  end
247
275
 
248
276
  #
249
- # Provides easy access to element auditing.
277
+ # Provides easy access to element auditing using simple injection and pattern
278
+ # matching.
279
+ #
280
+ # If a block has been provided analysis and logging will be delegated to it,
281
+ # otherwise, if a match is found it will be automatically logged.
250
282
  #
251
283
  # If no elements have been specified in 'opts' it will
252
284
  # use the elements from the module's "self.info()" hash. <br/>
@@ -256,12 +288,12 @@ module Auditor
256
288
  #
257
289
  # @param [String] injection_str the string to be injected
258
290
  # @param [Hash] opts options as described in {OPTIONS}
259
- # @param [Block] &block block to be passed the:
260
- # * HTTP response
261
- # * name of the input vector
262
- # * updated opts
263
- # The block will be called as soon as the
264
- # HTTP response is received.
291
+ # @param [Block] &block block to be used for custom analysis of responses; will be passed the following:
292
+ # * HTTP response
293
+ # * options
294
+ # * element
295
+ # The block will be called as soon as the
296
+ # HTTP response is received.
265
297
  #
266
298
  def audit( injection_str, opts = { }, &block )
267
299
 
@@ -299,73 +331,157 @@ module Auditor
299
331
  }
300
332
  end
301
333
 
302
- #
303
- # ABSTRACT - OPTIONAL
304
334
  #
305
335
  # This is called right before an [Arachni::Parser::Element]
306
336
  # is submitted/auditted and is used to determine whether to skip it or not.
307
337
  #
308
- # Implementation details are left up to the running module.
338
+ # Running modules can override this as they wish *but* at their own peril.
309
339
  #
310
340
  # @param [Arachni::Parser::Element] elem
311
341
  #
312
342
  def skip?( elem )
343
+ redundant.map {
344
+ |mod|
345
+
346
+ mod_name = @framework.modules[mod].info[:name]
347
+
348
+ set_id = @framework.modules.class.issue_set_id_from_elem( mod_name, elem )
349
+ return true if @framework.modules.issue_set.include?( set_id )
350
+ }
351
+
352
+
353
+ # if !@@__timeout_audited.empty?
354
+ # return @@__timeout_audited.include?( __rdiff_audit_id( elem ) )
355
+ # end
356
+
313
357
  return false
314
358
  end
315
359
 
316
360
 
317
361
  #
318
- # Audits elements using a 2 phase timing attack and logs results.
362
+ # Audits elements using timing attacks and automatically logs results.
319
363
  #
320
- # 'opts' needs to contain a :timeout value in milliseconds.</br>
321
- # Optionally, you can add a :timeout_divider.
364
+ # Here's how it works:
365
+ # * Loop 1 -- Populates the candidate queue. We're picking the low hanging
366
+ # fruit here so we can run this in larger concurrent bursts which cause *lots* of noise.
367
+ # - Initial probing for candidates -- Any element that times out is added to a queue.
368
+ # - Stabilization -- The candidate is submited with its default values in
369
+ # order to wait until the effects of the timing attack have worn off.
370
+ # * Loop 2 -- Verifies the candidates. This is much more delicate so the
371
+ # concurrent requests are lowered to pairs.
372
+ # - Liveness test -- Ensures that stabilization was successful before moving on.
373
+ # - Verification using an increased timeout -- Any elements that time out again are logged.
374
+ # - Stabilization
375
+ #
376
+ # Ideally, all requests involved with timing attacks would be run in sync mode
377
+ # but the performance penalties are too high, thus we compromise and make the best of it
378
+ # by running as little an amount of concurrent requests as possible for any given phase.
379
+ #
380
+ # opts = {
381
+ # :format => [ Format::STRAIGHT ],
382
+ # :timeout => 4000,
383
+ # :timeout_divider => 1000
384
+ # }
385
+ #
386
+ # audit_timeout( [ 'sleep( __TIME__ );' ], opts )
322
387
  #
323
- # Phase 1 uses the timeout value passed in opts, phase 2 uses (timeout * 2). </br>
324
- # If phase 1 fails, phase 2 is aborted. </br>
325
- # If we have a result in phase 1, phase 2 verifies that result with the higher timeout.
326
388
  #
327
389
  # @param [Array] strings injection strings
328
- # '__TIME__' will be substituded with (timeout / timeout_divider)
329
- # @param [Hash] opts options as described in {OPTIONS}
390
+ # __TIME__ will be substituded with (timeout / timeout_divider)
391
+ # @param [Hash] opts options as described in {OPTIONS} with the following extra:
392
+ # * :timeout -- milliseconds to wait for the request to complete
393
+ # * :timeout_divider -- __TIME__ = timeout / timeout_divider
330
394
  #
331
395
  def audit_timeout( strings, opts )
332
- logged = Set.new
396
+ @@__timeout_loaded_modules << self.class.info[:name]
333
397
 
334
- delay = opts[:timeout]
398
+ @@__timeout_audit_blocks << Proc.new {
399
+ delay = opts[:timeout]
335
400
 
336
- audit_timeout_debug_msg( 1, delay )
337
- timing_attack( strings, opts ) {
338
- |res, opts, elem|
401
+ audit_timeout_debug_msg( 1, delay )
402
+ timing_attack( strings, opts ) {
403
+ |res, opts, elem|
339
404
 
340
- if !logged.include?( opts[:altered] )
341
- logged << opts[:altered]
342
- audit_timeout_phase_2( elem )
343
- end
405
+ # maybe this should be removed to take care of accidental timeouts
406
+ # and let phase 2 clean up the mess
407
+ # if !@@__timeout_audited.include?( __rdiff_audit_id( elem ) )
408
+
409
+ elem.auditor( self )
410
+ # @@__timeout_audited << __rdiff_audit_id( elem )
411
+
412
+ print_info( "Found a candidate -- #{elem.type.capitalize} input '#{elem.altered}' at #{elem.action}" )
413
+
414
+ Arachni::Module::Auditor.audit_timeout_stabilize( elem )
415
+
416
+ @@__timeout_candidates << elem
417
+ # end
418
+ }
344
419
  }
345
420
  end
346
421
 
422
+ #
423
+ # Returns the names of all loaded modules that use timing attacks.
424
+ #
425
+ # @return [Set]
426
+ #
427
+ def self.timeout_loaded_modules
428
+ @@__timeout_loaded_modules
429
+ end
430
+
431
+ #
432
+ # Holds timing-attack performing Procs to be run after all
433
+ # non-tming-attack modules have finished.
434
+ #
435
+ # @return [Queue]
436
+ #
437
+ def self.timeout_audit_blocks
438
+ @@__timeout_audit_blocks
439
+ end
440
+
441
+ #
442
+ # Runs all blocks in {timeout_audit_blocks} and verifies
443
+ # and logs the candidate elements.
444
+ #
445
+ def self.timeout_audit_run
446
+ while( !@@__timeout_audit_blocks.empty? )
447
+ @@__timeout_audit_blocks.pop.call
448
+ end
449
+
450
+ while( !@@__timeout_candidates.empty? )
451
+ self.audit_timeout_phase_2( @@__timeout_candidates.pop )
452
+ end
453
+ end
454
+
347
455
  #
348
456
  # Runs phase 2 of the timing attack auditng an individual element
349
457
  # (which passed phase 1) with a higher delay and timeout
350
458
  #
351
- def audit_timeout_phase_2( elem )
459
+ def self.audit_timeout_phase_2( elem )
460
+
461
+ # reset the audited list since we're going to re-audit the elements
462
+ # @@__timeout_audited = Set.new
352
463
 
353
464
  opts = elem.opts
354
465
  opts[:timeout] *= 2
355
-
356
- audit_timeout_debug_msg( 2, opts[:timeout] )
466
+ # opts[:async] = false
467
+ # self.audit_timeout_debug_msg( 2, opts[:timeout] )
357
468
 
358
469
  str = opts[:timing_string].gsub( '__TIME__',
359
- ( (opts[:timeout] + 3000) / opts[:timeout_divider] ).to_s )
470
+ ( opts[:timeout] / opts[:timeout_divider] ).to_s )
360
471
 
361
- elem.auditor( self )
472
+ opts[:timeout] *= 0.7
362
473
 
363
- # this is the control; audit the element with an empty seed to make sure
474
+ elem.auditable = elem.orig
475
+
476
+ # this is the control; request the URL of the element to make sure
364
477
  # that the web page is alive i.e won't time-out by default
365
- elem.audit( '' , opts ) {
366
- |res, opts|
478
+ elem.get_auditor.http.get( elem.action ).on_complete {
479
+ |res|
480
+
367
481
  if !res.timed_out?
368
482
 
483
+ elem.get_auditor.print_info( 'Liveness check was successful, progressing to verification...' )
484
+
369
485
  elem.audit( str, opts ) {
370
486
  |res, opts|
371
487
 
@@ -374,13 +490,54 @@ module Auditor
374
490
  # all issues logged by timing attacks need manual verification.
375
491
  # end of story.
376
492
  opts[:verification] = true
377
- log( opts, res)
493
+ elem.get_auditor.log( opts, res)
494
+
495
+ self.audit_timeout_stabilize( elem )
496
+
497
+ else
498
+ elem.get_auditor.print_info( 'Verification failed.' )
378
499
  end
379
500
  }
380
-
501
+ else
502
+ elem.get_auditor.print_info( 'Liveness check failed, bailing out...' )
381
503
  end
382
504
  }
383
505
 
506
+ elem.get_auditor.http.run
507
+ end
508
+
509
+ #
510
+ # Submits an element which has just been audited using a timing attack
511
+ # with a high timeout in order to determine when the effects of a timing
512
+ # attack has worn off in order to safely continue the audit.
513
+ #
514
+ # @param [Arachni::Element::Auditable] elem
515
+ #
516
+ def self.audit_timeout_stabilize( elem )
517
+
518
+ d_opts = {
519
+ :skip_orig => true,
520
+ :redundant => true,
521
+ :timeout => 120000,
522
+ :silent => true,
523
+ :async => false
524
+ }
525
+
526
+ orig_opts = elem.opts
527
+
528
+ elem.get_auditor.print_info( 'Waiting for the effects of the timing attack to wear off.' )
529
+ elem.get_auditor.print_info( 'Max waiting time: ' + ( d_opts[:timeout] /1000 ).to_s + ' seconds.' )
530
+
531
+ elem.auditable = elem.orig
532
+ res = elem.submit( d_opts ).response
533
+
534
+ if !res.timed_out?
535
+ elem.get_auditor.print_info( 'Server seems responsive again.' )
536
+ else
537
+ elem.get_auditor.print_error( 'Max waiting time exceeded, the server may be dead.' )
538
+ end
539
+
540
+ elem.opts.merge!( orig_opts )
384
541
  end
385
542
 
386
543
  def audit_timeout_debug_msg( phase, delay )
@@ -405,16 +562,287 @@ module Auditor
405
562
  def timing_attack( strings, opts, &block )
406
563
 
407
564
  opts[:timeout_divider] ||= 1
565
+ # opts[:async] = false
566
+
408
567
  [strings].flatten.each {
409
568
  |str|
410
569
 
411
570
  opts[:timing_string] = str
412
- str = str.gsub( '__TIME__', ( (opts[:timeout] + 3000) / opts[:timeout_divider] ).to_s )
571
+ str = str.gsub( '__TIME__', ( (opts[:timeout] + 3 * opts[:timeout_divider]) / opts[:timeout_divider] ).to_s )
572
+ opts[:skip_orig] = true
573
+
413
574
  audit( str, opts ) {
414
575
  |res, opts, elem|
415
576
  block.call( res, opts, elem ) if block && res.timed_out?
416
577
  }
417
578
  }
579
+
580
+ @http.run
581
+ end
582
+
583
+ #
584
+ # Audits all elements types in opts[:elements] (or self.class.info[:elements]
585
+ # if there are none in opts) using differential analysis attacks.
586
+ #
587
+ # opts = {
588
+ # :precision => 3,
589
+ # :faults => [ 'fault injections' ],
590
+ # :bools => [ 'boolean injections' ]
591
+ # }
592
+ #
593
+ # audit_rdiff( opts )
594
+ #
595
+ # Here's how it goes:
596
+ # let default be the default/original response
597
+ # let fault be the response of the fault injection
598
+ # let bool be the response of the boolean injection
599
+ #
600
+ # a vulnerability is logged if default == bool AND bool.code == 200 AND fault != bool
601
+ #
602
+ # The "bool" response is also checked in order to determine if it's a custom 404, if it is it'll be skipped.
603
+ #
604
+ # If a block has been provided analysis and logging will be delegated to it.
605
+ #
606
+ # @param [Hash] opts available options:
607
+ # * :format -- as seen in {OPTIONS}
608
+ # * :elements -- as seen in {OPTIONS}
609
+ # * :train -- as seen in {OPTIONS}
610
+ # * :precision -- amount of rdiff iterations
611
+ # * :faults -- array of fault injection strings (these are supposed to force erroneous conditions when interpreted)
612
+ # * :bools -- array of boolean injection strings (these are supposed to not alter the webapp behavior when interpreted)
613
+ # @param [Block] &block block to be used for custom analysis of responses; will be passed the following:
614
+ # * injected string
615
+ # * audited element
616
+ # * default response body
617
+ # * boolean response
618
+ # * fault injection response body
619
+ #
620
+ def audit_rdiff( opts = {}, &block )
621
+
622
+ if( !opts.include?( :elements) || !opts[:elements] || opts[:elements].empty? )
623
+ opts[:elements] = self.class.info[:elements]
624
+ end
625
+
626
+ if( !opts.include?( :elements) || !opts[:elements] || opts[:elements].empty? )
627
+ opts[:elements] = OPTIONS[:elements]
628
+ end
629
+
630
+ opts = {
631
+ # append our seeds to the default values
632
+ :format => [ Format::APPEND ],
633
+ # allow duplicate requests
634
+ :redundant => true,
635
+ # amound of rdiff iterations
636
+ :precision => 2
637
+ }.merge( opts )
638
+
639
+ opts[:elements].each {
640
+ |elem|
641
+
642
+ case elem
643
+
644
+ when Element::LINK
645
+ next if !Options.instance.audit_links
646
+ @page.links.each {
647
+ |elem|
648
+ audit_rdiff_elem( elem, opts, &block )
649
+ }
650
+
651
+ when Element::FORM
652
+ next if !Options.instance.audit_forms
653
+ @page.forms.each {
654
+ |elem|
655
+ audit_rdiff_elem( elem, opts, &block )
656
+ }
657
+
658
+ when Element::COOKIE
659
+ next if !Options.instance.audit_cookies
660
+ @page.cookies.each {
661
+ |elem|
662
+ audit_rdiff_elem( elem, opts, &block )
663
+ }
664
+
665
+ when Element::HEADER
666
+ next if !Options.instance.audit_headers
667
+ @page.headers.each {
668
+ |elem|
669
+ audit_rdiff_elem( elem, opts, &block )
670
+ }
671
+ else
672
+ raise( 'Unknown element to audit: ' + elem.to_s )
673
+
674
+ end
675
+
676
+ }
677
+ end
678
+
679
+ #
680
+ # Audits a single element using an rdiff attack.
681
+ #
682
+ # @param [Arachni::Element::Auditable] elem the element to audit
683
+ # @param [Hash] opts same as for {#audit_rdiff}
684
+ # @param [Block] &block same as for {#audit_rdiff}
685
+ #
686
+ def audit_rdiff_elem( elem, opts = {}, &block )
687
+
688
+ # don't continue if there's a missing value
689
+ elem.auditable.values.each {
690
+ |val|
691
+ return if !val || val.empty?
692
+ }
693
+
694
+ return if __rdiff_audited?( elem )
695
+ __rdiff_audited!( elem )
696
+
697
+ responses = {
698
+ :orig => nil,
699
+ :good => {},
700
+ :bad => {},
701
+ :bad_total => 0,
702
+ :good_total => 0
703
+ }
704
+
705
+ elem.auditor( self )
706
+ opts[:precision].times {
707
+ # get the default responses
708
+ elem.audit( '', opts ) {
709
+ |res|
710
+ responses[:orig] ||= res.body
711
+ # remove context-irrelevant dynamic content like banners and such
712
+ # from the error page
713
+ responses[:orig] = responses[:orig].rdiff( res.body )
714
+ }
715
+ }
716
+
717
+ opts[:precision].times {
718
+ opts[:faults].each {
719
+ |str|
720
+
721
+ # get injection variations that will hopefully cause an internal/silent
722
+ # SQL error
723
+ variations = elem.injection_sets( str, opts )
724
+
725
+ responses[:bad_total] = variations.size
726
+
727
+ variations.each {
728
+ |c_elem|
729
+
730
+ print_status( c_elem.get_status_str( c_elem.altered ) )
731
+
732
+ # register us as the auditor
733
+ c_elem.auditor( self )
734
+ # submit the link and get the response
735
+ c_elem.submit( opts ).on_complete {
736
+ |res|
737
+
738
+ responses[:bad][c_elem.altered] ||= res.body.clone
739
+
740
+ # remove context-irrelevant dynamic content like banners and such
741
+ # from the error page
742
+ responses[:bad][c_elem.altered] =
743
+ responses[:bad][c_elem.altered].rdiff( res.body.clone )
744
+ }
745
+ }
746
+ }
747
+ }
748
+
749
+ opts[:bools].each {
750
+ |str|
751
+
752
+ # get injection variations that will not affect the outcome of the query
753
+ variations = elem.injection_sets( str, opts )
754
+
755
+ responses[:good_total] = variations.size
756
+
757
+ variations.each {
758
+ |c_elem|
759
+
760
+ print_status( c_elem.get_status_str( c_elem.altered ) )
761
+
762
+ # register us as the auditor
763
+ c_elem.auditor( self )
764
+ # submit the link and get the response
765
+ c_elem.submit( opts ).on_complete {
766
+ |res|
767
+
768
+ responses[:good][c_elem.altered] ||= []
769
+
770
+ # save the response for later analysis
771
+ responses[:good][c_elem.altered] << {
772
+ 'str' => str,
773
+ 'res' => res,
774
+ 'elem' => c_elem
775
+ }
776
+ }
777
+ }
778
+ }
779
+
780
+ # when this runs the 'responses' hash will have been populated
781
+ @http.after_run {
782
+
783
+ responses[:good].keys.each {
784
+ |key|
785
+
786
+ responses[:good][key].each {
787
+ |res|
788
+
789
+ if block
790
+ block.call( res['str'], res['elem'], responses[:orig], res['res'], responses[:bad][key] )
791
+ elsif( responses[:orig] == res['res'].body &&
792
+ responses[:bad][key] != res['res'].body &&
793
+ !@http.custom_404?( res['res'] ) && res['res'].code == 200 )
794
+
795
+ url = res['res'].effective_url
796
+
797
+ # since we bypassed the auditor completely we need to create
798
+ # our own opts hash and pass it to the Vulnerability class.
799
+ #
800
+ # this is only required for Metasploitable vulnerabilities
801
+ opts = {
802
+ :injected_orig => res['str'],
803
+ :combo => res['elem'].auditable
804
+ }
805
+
806
+ issue = Issue.new( {
807
+ :var => key,
808
+ :url => url,
809
+ :method => res['res'].request.method.to_s,
810
+ :opts => opts,
811
+ :injected => res['str'],
812
+ :id => res['str'],
813
+ :regexp => 'n/a',
814
+ :regexp_match => 'n/a',
815
+ :elem => res['elem'].type,
816
+ :response => res['res'].body,
817
+ :verification => true,
818
+ :headers => {
819
+ :request => res['res'].request.headers,
820
+ :response => res['res'].headers,
821
+ }
822
+ }.merge( self.class.info )
823
+ )
824
+
825
+ print_ok( "In #{res['elem'].type} var '#{key}' ( #{url} )" )
826
+
827
+ # register our results with the system
828
+ register_results( [ issue ] )
829
+ end
830
+
831
+ }
832
+ }
833
+ }
834
+ end
835
+
836
+ def __rdiff_audited!( elem )
837
+ @@__rdiff_audited << __rdiff_audit_id( elem )
838
+ end
839
+
840
+ def __rdiff_audited?( elem )
841
+ @@__rdiff_audited.include?( __rdiff_audit_id( elem ) )
842
+ end
843
+
844
+ def __rdiff_audit_id( elem )
845
+ elem.action + elem.auditable.keys.to_s
418
846
  end
419
847
 
420
848
  #
@@ -448,15 +876,10 @@ module Auditor
448
876
  #
449
877
  # Audits Auditalble HTML/HTTP elements
450
878
  #
451
- # @param [Array<Arachni::Element::Auditable>] elements auditable elements to audit
452
- # @param [String] injection_str the string to be injected
453
- # @param [Hash] opts options as described in {OPTIONS}
454
- # @param [Block] &block block to be passed the:
455
- # * HTTP response
456
- # * name of the input vector
457
- # * updated opts
458
- # The block will be called as soon as the
459
- # HTTP response is received.
879
+ # @param [Array<Arachni::Element::Auditable>] elements elements to audit
880
+ # @param [String] injection_str same as for {#audit}
881
+ # @param [Hash] opts same as for {#audit}
882
+ # @param [Block] &block same as for {#audit}
460
883
  #
461
884
  # @see #method_missing
462
885
  #