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.
- 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
|
#
|