arachni 0.2.4 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +33 -0
- data/README.md +2 -4
- data/Rakefile +15 -4
- data/bin/arachni +0 -0
- data/bin/arachni_web +0 -0
- data/bin/arachni_web_autostart +0 -0
- data/bin/arachni_xmlrpc +0 -0
- data/bin/arachni_xmlrpcd +0 -0
- data/bin/arachni_xmlrpcd_monitor +0 -0
- data/lib/arachni.rb +1 -1
- data/lib/framework.rb +36 -6
- data/lib/http.rb +12 -5
- data/lib/module/auditor.rb +482 -59
- data/lib/module/base.rb +17 -0
- data/lib/module/manager.rb +26 -2
- data/lib/module/trainer.rb +1 -12
- data/lib/module/utilities.rb +12 -0
- data/lib/parser/auditable.rb +8 -3
- data/lib/parser/elements.rb +11 -0
- data/lib/parser/page.rb +3 -1
- data/lib/parser/parser.rb +130 -18
- data/lib/rpc/xml/server/dispatcher.rb +21 -0
- data/lib/spider.rb +141 -82
- data/lib/ui/cli/cli.rb +2 -3
- data/lib/ui/web/addon_manager.rb +273 -0
- data/lib/ui/web/addons/autodeploy.rb +172 -0
- data/lib/ui/web/addons/autodeploy/lib/manager.rb +291 -0
- data/lib/ui/web/addons/autodeploy/views/index.erb +124 -0
- data/lib/ui/web/addons/sample.rb +78 -0
- data/lib/ui/web/addons/sample/views/index.erb +4 -0
- data/lib/ui/web/addons/scheduler.rb +139 -0
- data/lib/ui/web/addons/scheduler/views/index.erb +131 -0
- data/lib/ui/web/addons/scheduler/views/options.erb +93 -0
- data/lib/ui/web/dispatcher_manager.rb +80 -13
- data/lib/ui/web/instance_manager.rb +87 -0
- data/lib/ui/web/scheduler.rb +166 -0
- data/lib/ui/web/server.rb +142 -202
- data/lib/ui/web/server/public/js/jquery-ui-timepicker.js +985 -0
- data/lib/ui/web/server/public/plugins/sample/style.css +0 -0
- data/lib/ui/web/server/public/style.css +42 -0
- data/lib/ui/web/server/views/addon.erb +15 -0
- data/lib/ui/web/server/views/addons.erb +46 -0
- data/lib/ui/web/server/views/dispatchers.erb +1 -1
- data/lib/ui/web/server/views/instance.erb +9 -11
- data/lib/ui/web/server/views/layout.erb +14 -1
- data/lib/ui/web/server/views/welcome.erb +7 -6
- data/lib/ui/web/utilities.rb +134 -0
- data/modules/audit/code_injection_timing.rb +6 -2
- data/modules/audit/code_injection_timing/payloads.txt +2 -2
- data/modules/audit/os_cmd_injection_timing.rb +7 -3
- data/modules/audit/os_cmd_injection_timing/payloads.txt +1 -1
- data/modules/audit/sqli_blind_rdiff.rb +18 -233
- data/modules/audit/sqli_blind_rdiff/payloads.txt +5 -0
- data/modules/audit/sqli_blind_timing.rb +9 -2
- data/path_extractors/anchors.rb +1 -1
- data/path_extractors/forms.rb +1 -1
- data/path_extractors/frames.rb +1 -1
- data/path_extractors/generic.rb +1 -1
- data/path_extractors/links.rb +1 -1
- data/path_extractors/meta_refresh.rb +1 -1
- data/path_extractors/scripts.rb +1 -1
- data/path_extractors/sitemap.rb +1 -1
- data/plugins/proxy/server.rb +3 -2
- data/plugins/waf_detector.rb +0 -3
- metadata +37 -34
- data/lib/anemone/cookie_store.rb +0 -35
- data/lib/anemone/core.rb +0 -371
- data/lib/anemone/exceptions.rb +0 -5
- data/lib/anemone/http.rb +0 -144
- data/lib/anemone/page.rb +0 -338
- data/lib/anemone/page_store.rb +0 -160
- data/lib/anemone/storage.rb +0 -34
- data/lib/anemone/storage/base.rb +0 -75
- data/lib/anemone/storage/exceptions.rb +0 -15
- data/lib/anemone/storage/mongodb.rb +0 -89
- data/lib/anemone/storage/pstore.rb +0 -50
- data/lib/anemone/storage/redis.rb +0 -90
- data/lib/anemone/storage/tokyo_cabinet.rb +0 -57
- 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.
|
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
|
data/bin/arachni_web_autostart
CHANGED
File without changes
|
data/bin/arachni_xmlrpc
CHANGED
File without changes
|
data/bin/arachni_xmlrpcd
CHANGED
File without changes
|
data/bin/arachni_xmlrpcd_monitor
CHANGED
File without changes
|
data/lib/arachni.rb
CHANGED
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.
|
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 =>
|
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
|
230
|
-
:current_page
|
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.
|
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.
|
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 =>
|
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
|
data/lib/module/auditor.rb
CHANGED
@@ -14,16 +14,42 @@ module Module
|
|
14
14
|
#
|
15
15
|
# Auditor module
|
16
16
|
#
|
17
|
-
# Included by {Module::Base}
|
18
|
-
#
|
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.
|
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
|
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
|
-
#
|
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
|
-
#
|
215
|
+
# Populates and logs an {Arachni::Issue} based on data from "opts" and "res".
|
188
216
|
#
|
189
|
-
# @param [
|
190
|
-
# @param [
|
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
|
-
#
|
261
|
-
#
|
262
|
-
#
|
263
|
-
#
|
264
|
-
#
|
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
|
-
#
|
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
|
362
|
+
# Audits elements using timing attacks and automatically logs results.
|
319
363
|
#
|
320
|
-
# '
|
321
|
-
#
|
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
|
-
#
|
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
|
-
|
396
|
+
@@__timeout_loaded_modules << self.class.info[:name]
|
333
397
|
|
334
|
-
|
398
|
+
@@__timeout_audit_blocks << Proc.new {
|
399
|
+
delay = opts[:timeout]
|
335
400
|
|
336
|
-
|
337
|
-
|
338
|
-
|
401
|
+
audit_timeout_debug_msg( 1, delay )
|
402
|
+
timing_attack( strings, opts ) {
|
403
|
+
|res, opts, elem|
|
339
404
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
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
|
-
(
|
470
|
+
( opts[:timeout] / opts[:timeout_divider] ).to_s )
|
360
471
|
|
361
|
-
|
472
|
+
opts[:timeout] *= 0.7
|
362
473
|
|
363
|
-
|
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.
|
366
|
-
|res
|
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] +
|
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
|
452
|
-
# @param [String] injection_str
|
453
|
-
# @param [Hash] opts
|
454
|
-
# @param [Block] &block
|
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
|
#
|