arachni 1.0.2 → 1.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.
- 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
|