arachni 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +1 -9
- data/bin/arachni_script +1 -1
- data/components/checks/active/xss_dom_script_context.rb +1 -1
- data/components/checks/active/xss_event.rb +1 -1
- data/components/checks/active/xss_script_context.rb +1 -1
- data/components/plugins/autologin.rb +2 -2
- data/components/plugins/content_types.rb +4 -5
- data/components/plugins/cookie_collector.rb +6 -3
- data/components/plugins/uncommon_headers.rb +6 -2
- data/lib/arachni/browser.rb +26 -2
- data/lib/arachni/browser/element_locator.rb +9 -2
- data/lib/arachni/browser/javascript.rb +6 -0
- data/lib/arachni/browser/javascript/scripts/dom_monitor.js +39 -0
- data/lib/arachni/browser_cluster.rb +11 -25
- data/lib/arachni/element/capabilities/analyzable/differential.rb +4 -0
- data/lib/arachni/element/capabilities/analyzable/timeout.rb +4 -0
- data/lib/arachni/element/capabilities/auditable/dom.rb +1 -0
- data/lib/arachni/element/capabilities/mutable.rb +0 -9
- data/lib/arachni/element/capabilities/with_auditor/output.rb +2 -0
- data/lib/arachni/element/cookie.rb +9 -4
- data/lib/arachni/element/form.rb +6 -6
- data/lib/arachni/element/header.rb +1 -1
- data/lib/arachni/framework.rb +1 -0
- data/lib/arachni/http/client.rb +1 -0
- data/lib/arachni/option_groups.rb +3 -0
- data/lib/arachni/option_groups/paths.rb +63 -6
- data/lib/arachni/option_groups/snapshot.rb +4 -0
- data/lib/arachni/session.rb +73 -17
- data/lib/arachni/state/audit.rb +2 -0
- data/lib/version +1 -1
- data/spec/arachni/browser/javascript_spec.rb +20 -0
- data/spec/arachni/browser_spec.rb +51 -0
- data/spec/arachni/element/cookie_spec.rb +22 -1
- data/spec/arachni/element/form_spec.rb +19 -9
- data/spec/arachni/framework_spec.rb +17 -0
- data/spec/arachni/option_groups/paths_spec.rb +109 -8
- data/spec/arachni/option_groups/snapshot_spec.rb +17 -0
- data/spec/arachni/session_spec.rb +54 -26
- data/spec/components/plugins/autologin_spec.rb +59 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/support/factories/element/body.rb +3 -0
- data/spec/support/factories/element/generic_dom.rb +6 -0
- data/spec/support/factories/element/path.rb +3 -0
- data/spec/support/factories/element/server.rb +3 -0
- data/spec/support/factories/page/dom/transition.rb +21 -0
- data/spec/support/helpers/resets.rb +1 -0
- data/spec/support/servers/arachni/browser/javascript/dom_monitor.rb +15 -0
- data/ui/cli/framework.rb +5 -0
- data/ui/cli/framework/option_parser.rb +1 -1
- data/ui/cli/option_parser.rb +3 -0
- data/ui/cli/output.rb +45 -19
- metadata +10 -2
@@ -39,6 +39,10 @@ module Differential
|
|
39
39
|
# element under audit.
|
40
40
|
respect_method: true,
|
41
41
|
|
42
|
+
# Disable {Arachni::Options#audit_cookies_extensively}, there's little
|
43
|
+
# to be gained in this case and just causes interference.
|
44
|
+
extensively: false,
|
45
|
+
|
42
46
|
# Don't generate or submit any mutations with default or sample inputs.
|
43
47
|
skip_original: true,
|
44
48
|
|
@@ -299,6 +299,10 @@ module Timeout
|
|
299
299
|
# any interference during timing attacks.
|
300
300
|
skip_original: true,
|
301
301
|
|
302
|
+
# Disable {Arachni::OptionGroups::Audit#cookies_extensively}, there's little
|
303
|
+
# to be gained in this case and just causes interference.
|
304
|
+
extensively: false,
|
305
|
+
|
302
306
|
# Intercept each element mutation prior to it being submitted and
|
303
307
|
# replace the '__TIME__' stub with the actual delay value.
|
304
308
|
each_mutation: proc do |mutation|
|
@@ -40,6 +40,8 @@ class Cookie < Base
|
|
40
40
|
httponly: false
|
41
41
|
}
|
42
42
|
|
43
|
+
attr_reader :data
|
44
|
+
|
43
45
|
# @param [Hash] options
|
44
46
|
# For options see {DEFAULT}, with the following extras:
|
45
47
|
# @option options [String] :url
|
@@ -175,7 +177,7 @@ class Cookie < Base
|
|
175
177
|
end
|
176
178
|
|
177
179
|
# Overrides {Capabilities::Mutable#each_mutation} to handle cookie-specific
|
178
|
-
# limitations and the {Arachni::
|
180
|
+
# limitations and the {Arachni::OptionGroups::Audit#cookies_extensively} option.
|
179
181
|
#
|
180
182
|
# @param (see Capabilities::Mutable#each_mutation)
|
181
183
|
# @return (see Capabilities::Mutable#each_mutation)
|
@@ -184,19 +186,22 @@ class Cookie < Base
|
|
184
186
|
#
|
185
187
|
# @see Capabilities::Mutable#each_mutation
|
186
188
|
def each_mutation( payload, opts = {}, &block )
|
187
|
-
|
189
|
+
opts = opts.dup
|
190
|
+
flip = opts.delete( :param_flip )
|
191
|
+
extensively = opts[:extensively]
|
192
|
+
extensively = Arachni::Options.audit.cookies_extensively? if extensively.nil?
|
188
193
|
|
189
194
|
super( payload, opts ) do |elem|
|
190
195
|
yield elem
|
191
196
|
|
192
|
-
next if !
|
197
|
+
next if !extensively
|
193
198
|
elem.each_extensive_mutation( elem, &block )
|
194
199
|
end
|
195
200
|
|
196
201
|
return if !flip
|
197
202
|
|
198
203
|
if !valid_input_name_data?( payload )
|
199
|
-
print_debug_level_2 'Payload not supported as input
|
204
|
+
print_debug_level_2 'Payload not supported as input name by' <<
|
200
205
|
" #{audit_id}: #{payload.inspect}"
|
201
206
|
return
|
202
207
|
end
|
data/lib/arachni/element/form.rb
CHANGED
@@ -375,8 +375,8 @@ class Form < Base
|
|
375
375
|
# @param [Arachni::HTTP::Response] response
|
376
376
|
#
|
377
377
|
# @return [Array<Form>]
|
378
|
-
def from_response( response )
|
379
|
-
from_document( response.url, response.body )
|
378
|
+
def from_response( response, ignore_scope = false )
|
379
|
+
from_document( response.url, response.body, ignore_scope )
|
380
380
|
end
|
381
381
|
|
382
382
|
# Extracts forms from an HTML document.
|
@@ -386,18 +386,18 @@ class Form < Base
|
|
386
386
|
# @param [String, Nokogiri::HTML::Document] document
|
387
387
|
#
|
388
388
|
# @return [Array<Form>]
|
389
|
-
def from_document( url, document )
|
389
|
+
def from_document( url, document, ignore_scope = false )
|
390
390
|
document = Nokogiri::HTML( document.to_s ) if !document.is_a?( Nokogiri::HTML::Document )
|
391
391
|
base_url = (document.search( '//base[@href]' )[0]['href'] rescue url)
|
392
392
|
|
393
393
|
document.search( '//form' ).map do |node|
|
394
|
-
next if !(form = from_node( base_url, node ))
|
394
|
+
next if !(form = from_node( base_url, node, ignore_scope ))
|
395
395
|
form.url = url.freeze
|
396
396
|
form
|
397
397
|
end.compact
|
398
398
|
end
|
399
399
|
|
400
|
-
def from_node( url, node )
|
400
|
+
def from_node( url, node, ignore_scope = false )
|
401
401
|
options = attributes_to_hash( node.attributes )
|
402
402
|
options[:url] = url.freeze
|
403
403
|
options[:action] = to_absolute( options[:action], url ).freeze
|
@@ -405,7 +405,7 @@ class Form < Base
|
|
405
405
|
options[:html] = node.to_html.freeze
|
406
406
|
|
407
407
|
if (parsed_url = Arachni::URI( options[:action] ))
|
408
|
-
return if parsed_url.scope.out?
|
408
|
+
return if !ignore_scope && parsed_url.scope.out?
|
409
409
|
end
|
410
410
|
|
411
411
|
%w(textarea input select button).each do |attr|
|
@@ -46,7 +46,7 @@ class Header < Base
|
|
46
46
|
return if !flip
|
47
47
|
|
48
48
|
if !valid_input_name_data?( payload )
|
49
|
-
print_debug_level_2 'Payload not supported as input
|
49
|
+
print_debug_level_2 'Payload not supported as input name by' <<
|
50
50
|
" #{audit_id}: #{payload.inspect}"
|
51
51
|
return
|
52
52
|
end
|
data/lib/arachni/framework.rb
CHANGED
@@ -1017,6 +1017,7 @@ class Framework
|
|
1017
1017
|
state.status = :scanning if !pausing?
|
1018
1018
|
|
1019
1019
|
push_to_url_queue( options.url )
|
1020
|
+
options.scope.extend_paths.each { |url| push_to_url_queue( url ) }
|
1020
1021
|
options.scope.restrict_paths.each { |url| push_to_url_queue( url, true ) }
|
1021
1022
|
|
1022
1023
|
# Initialize the BrowserCluster.
|
data/lib/arachni/http/client.rb
CHANGED
@@ -725,6 +725,7 @@ class Client
|
|
725
725
|
print_debug_level_3 "Params: #{request.parameters}"
|
726
726
|
print_debug_level_3 "Body: #{request.body}"
|
727
727
|
print_debug_level_3 "Headers: #{request.headers}"
|
728
|
+
print_debug_level_3 "Cookies: #{request.cookies}"
|
728
729
|
print_debug_level_3 "Train?: #{request.train?}"
|
729
730
|
print_debug_level_3 '------------'
|
730
731
|
end
|
@@ -6,6 +6,8 @@
|
|
6
6
|
web site for more information on licensing and terms of use.
|
7
7
|
=end
|
8
8
|
|
9
|
+
require 'fileutils'
|
10
|
+
|
9
11
|
module Arachni::OptionGroups
|
10
12
|
|
11
13
|
# Holds paths to the directories of various system components.
|
@@ -34,19 +36,29 @@ class Paths < Arachni::OptionGroup
|
|
34
36
|
@root = root_path
|
35
37
|
@gfx = @root + 'gfx/'
|
36
38
|
@components = @root + 'components/'
|
37
|
-
@snapshots = @root + 'snapshots/'
|
38
39
|
|
39
|
-
|
40
|
-
|
40
|
+
if self.class.config['framework']['snapshots']
|
41
|
+
@snapshots = self.class.config['framework']['snapshots']
|
42
|
+
else
|
43
|
+
@snapshots = @root + 'snapshots/'
|
44
|
+
end
|
45
|
+
|
46
|
+
if ENV['ARACHNI_FRAMEWORK_LOGDIR']
|
47
|
+
@logs = "#{ENV['ARACHNI_FRAMEWORK_LOGDIR']}/"
|
48
|
+
elsif self.class.config['framework']['logs']
|
49
|
+
@logs = self.class.config['framework']['logs']
|
50
|
+
else
|
51
|
+
@logs = "#{@root}logs/"
|
52
|
+
end
|
41
53
|
|
42
54
|
@checks = @components + 'checks/'
|
43
55
|
@reporters = @components + 'reporters/'
|
44
56
|
@plugins = @components + 'plugins/'
|
45
|
-
@services
|
57
|
+
@services = @components + 'services/'
|
46
58
|
@path_extractors = @components + 'path_extractors/'
|
47
59
|
@fingerprinters = @components + 'fingerprinters/'
|
48
60
|
|
49
|
-
@lib
|
61
|
+
@lib = @root + 'lib/arachni/'
|
50
62
|
|
51
63
|
@executables = @lib + 'processes/executables/'
|
52
64
|
@support = @lib + 'support/'
|
@@ -54,10 +66,55 @@ class Paths < Arachni::OptionGroup
|
|
54
66
|
@arachni = @lib[0...-1]
|
55
67
|
end
|
56
68
|
|
57
|
-
# @return [String] Root path of the framework.
|
58
69
|
def root_path
|
70
|
+
self.class.root_path
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [String] Root path of the framework.
|
74
|
+
def self.root_path
|
59
75
|
File.expand_path( File.dirname( __FILE__ ) + '/../../..' ) + '/'
|
60
76
|
end
|
61
77
|
|
78
|
+
def config
|
79
|
+
self.class.config
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.paths_config_file
|
83
|
+
"#{root_path}config/write_paths.yml"
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.clear_config_cache
|
87
|
+
@config = nil
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.config
|
91
|
+
return @config if @config
|
92
|
+
|
93
|
+
if !File.exist?( paths_config_file )
|
94
|
+
@config = {}
|
95
|
+
else
|
96
|
+
@config = YAML.load( IO.read( paths_config_file ) )
|
97
|
+
end
|
98
|
+
|
99
|
+
@config['framework'] ||= {}
|
100
|
+
@config['cli'] ||= {}
|
101
|
+
|
102
|
+
@config.dup.each do |category, config|
|
103
|
+
config.dup.each do |subcat, dir|
|
104
|
+
if dir.to_s.empty?
|
105
|
+
@config[category].delete subcat
|
106
|
+
next
|
107
|
+
end
|
108
|
+
|
109
|
+
dir.gsub!( '~', ENV['HOME'] )
|
110
|
+
dir << '/' if !dir.end_with?( '/' )
|
111
|
+
|
112
|
+
FileUtils.mkdir_p dir
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
@config
|
117
|
+
end
|
118
|
+
|
62
119
|
end
|
63
120
|
end
|
data/lib/arachni/session.rb
CHANGED
@@ -116,8 +116,6 @@ class Session
|
|
116
116
|
# Pages to look through.
|
117
117
|
# @option opts [String] :url
|
118
118
|
# URL to fetch and look for forms.
|
119
|
-
# @option opts [Bool] :with_browser
|
120
|
-
# Does the login form require a {Browser} environment?
|
121
119
|
#
|
122
120
|
# @param [Block] block
|
123
121
|
# If a block and a :url are given, the request will run async and the
|
@@ -151,17 +149,19 @@ class Session
|
|
151
149
|
opts[:forms]
|
152
150
|
elsif (url = opts[:url])
|
153
151
|
http_opts = {
|
154
|
-
|
155
|
-
|
156
|
-
update_cookies: true,
|
157
|
-
follow_location: true
|
158
|
-
}
|
152
|
+
update_cookies: true,
|
153
|
+
follow_location: true
|
159
154
|
}
|
160
155
|
|
161
156
|
if async
|
162
|
-
|
157
|
+
http.get( url, http_opts ) do |r|
|
158
|
+
block.call find.call( forms_from_response( r, true ) )
|
159
|
+
end
|
163
160
|
else
|
164
|
-
|
161
|
+
forms_from_response(
|
162
|
+
http.get( url, http_opts.merge( mode: :sync ) ),
|
163
|
+
true
|
164
|
+
)
|
165
165
|
end
|
166
166
|
end
|
167
167
|
|
@@ -212,25 +212,71 @@ class Session
|
|
212
212
|
def login
|
213
213
|
fail Error::NotConfigured, 'Please #configure the session first.' if !configured?
|
214
214
|
|
215
|
-
|
215
|
+
if has_browser?
|
216
|
+
print_debug 'Logging in using browser.'
|
217
|
+
else
|
218
|
+
print_debug 'Logging in without browser.'
|
219
|
+
end
|
220
|
+
|
221
|
+
print_debug "Grabbing page at: #{configuration[:url]}"
|
222
|
+
|
223
|
+
# Revert to the Framework DOM Level 1 page handling if no browser
|
224
|
+
# is available.
|
225
|
+
page = refresh_browser ?
|
226
|
+
browser.load( configuration[:url], take_snapshot: false ).to_page :
|
227
|
+
Page.from_url( configuration[:url], precision: 1, http: {
|
228
|
+
update_cookies: true
|
229
|
+
})
|
230
|
+
|
231
|
+
print_debug "Got page with URL #{page.url}"
|
216
232
|
|
217
233
|
form = find_login_form(
|
218
|
-
|
234
|
+
# We need to reparse the body in order to override the scope
|
235
|
+
# and thus extract even out-of-scope forms in case we're dealing
|
236
|
+
# with a Single-Sign-On situation.
|
237
|
+
forms: forms_from_document( page.url, page.body, true ),
|
219
238
|
inputs: configuration[:inputs].keys
|
220
239
|
)
|
221
240
|
|
222
241
|
if !form
|
242
|
+
print_debug_level_2 page.body
|
223
243
|
fail Error::FormNotFound,
|
224
244
|
"Login form could not be found with: #{configuration}"
|
225
245
|
end
|
226
246
|
|
227
|
-
form.
|
228
|
-
|
247
|
+
print_debug "Found login form: #{form.id}"
|
248
|
+
|
249
|
+
form.page = page
|
250
|
+
|
251
|
+
# Use the form DOM to submit if a browser is available.
|
252
|
+
form = form.dom if has_browser?
|
253
|
+
|
254
|
+
form.update configuration[:inputs]
|
255
|
+
form.auditor = self
|
256
|
+
|
257
|
+
print_debug "Updated form inputs: #{form.inputs}"
|
229
258
|
|
230
259
|
page = nil
|
231
|
-
|
260
|
+
if has_browser?
|
261
|
+
print_debug 'Submitting form.'
|
262
|
+
form.submit { |p| page = p }
|
263
|
+
print_debug 'Form submitted.'
|
232
264
|
|
233
|
-
|
265
|
+
http.update_cookies browser.cookies
|
266
|
+
else
|
267
|
+
page = form.submit(
|
268
|
+
mode: :sync,
|
269
|
+
follow_location: false,
|
270
|
+
update_cookies: true
|
271
|
+
).to_page
|
272
|
+
|
273
|
+
if page.response.redirection?
|
274
|
+
url = to_absolute( page.response.headers.location, page.url )
|
275
|
+
print_debug "Redirected to: #{url}"
|
276
|
+
|
277
|
+
page = Page.from_url( url, precision: 1, http: { update_cookies: true } )
|
278
|
+
end
|
279
|
+
end
|
234
280
|
|
235
281
|
page
|
236
282
|
end
|
@@ -256,7 +302,8 @@ class Session
|
|
256
302
|
fail Error::NoLoginCheck if !has_login_check?
|
257
303
|
|
258
304
|
http_options = http_options.merge(
|
259
|
-
mode:
|
305
|
+
mode: block_given? ? :async : :sync,
|
306
|
+
follow_location: true
|
260
307
|
)
|
261
308
|
|
262
309
|
bool = nil
|
@@ -278,6 +325,10 @@ class Session
|
|
278
325
|
HTTP::Client
|
279
326
|
end
|
280
327
|
|
328
|
+
def has_browser?
|
329
|
+
Browser.has_executable? && Options.scope.dom_depth_limit > 0
|
330
|
+
end
|
331
|
+
|
281
332
|
private
|
282
333
|
|
283
334
|
def shutdown_browser
|
@@ -288,8 +339,13 @@ class Session
|
|
288
339
|
end
|
289
340
|
|
290
341
|
def refresh_browser
|
342
|
+
return if !has_browser?
|
343
|
+
|
291
344
|
shutdown_browser
|
292
|
-
|
345
|
+
|
346
|
+
# The session handling browser needs to be able to roam free in order
|
347
|
+
# to support SSO.
|
348
|
+
@browser = Browser.new( store_pages: false, ignore_scope: true )
|
293
349
|
end
|
294
350
|
|
295
351
|
end
|