arachni 0.2.4 → 0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  #