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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -0
  3. data/README.md +1 -9
  4. data/bin/arachni_script +1 -1
  5. data/components/checks/active/xss_dom_script_context.rb +1 -1
  6. data/components/checks/active/xss_event.rb +1 -1
  7. data/components/checks/active/xss_script_context.rb +1 -1
  8. data/components/plugins/autologin.rb +2 -2
  9. data/components/plugins/content_types.rb +4 -5
  10. data/components/plugins/cookie_collector.rb +6 -3
  11. data/components/plugins/uncommon_headers.rb +6 -2
  12. data/lib/arachni/browser.rb +26 -2
  13. data/lib/arachni/browser/element_locator.rb +9 -2
  14. data/lib/arachni/browser/javascript.rb +6 -0
  15. data/lib/arachni/browser/javascript/scripts/dom_monitor.js +39 -0
  16. data/lib/arachni/browser_cluster.rb +11 -25
  17. data/lib/arachni/element/capabilities/analyzable/differential.rb +4 -0
  18. data/lib/arachni/element/capabilities/analyzable/timeout.rb +4 -0
  19. data/lib/arachni/element/capabilities/auditable/dom.rb +1 -0
  20. data/lib/arachni/element/capabilities/mutable.rb +0 -9
  21. data/lib/arachni/element/capabilities/with_auditor/output.rb +2 -0
  22. data/lib/arachni/element/cookie.rb +9 -4
  23. data/lib/arachni/element/form.rb +6 -6
  24. data/lib/arachni/element/header.rb +1 -1
  25. data/lib/arachni/framework.rb +1 -0
  26. data/lib/arachni/http/client.rb +1 -0
  27. data/lib/arachni/option_groups.rb +3 -0
  28. data/lib/arachni/option_groups/paths.rb +63 -6
  29. data/lib/arachni/option_groups/snapshot.rb +4 -0
  30. data/lib/arachni/session.rb +73 -17
  31. data/lib/arachni/state/audit.rb +2 -0
  32. data/lib/version +1 -1
  33. data/spec/arachni/browser/javascript_spec.rb +20 -0
  34. data/spec/arachni/browser_spec.rb +51 -0
  35. data/spec/arachni/element/cookie_spec.rb +22 -1
  36. data/spec/arachni/element/form_spec.rb +19 -9
  37. data/spec/arachni/framework_spec.rb +17 -0
  38. data/spec/arachni/option_groups/paths_spec.rb +109 -8
  39. data/spec/arachni/option_groups/snapshot_spec.rb +17 -0
  40. data/spec/arachni/session_spec.rb +54 -26
  41. data/spec/components/plugins/autologin_spec.rb +59 -0
  42. data/spec/spec_helper.rb +1 -3
  43. data/spec/support/factories/element/body.rb +3 -0
  44. data/spec/support/factories/element/generic_dom.rb +6 -0
  45. data/spec/support/factories/element/path.rb +3 -0
  46. data/spec/support/factories/element/server.rb +3 -0
  47. data/spec/support/factories/page/dom/transition.rb +21 -0
  48. data/spec/support/helpers/resets.rb +1 -0
  49. data/spec/support/servers/arachni/browser/javascript/dom_monitor.rb +15 -0
  50. data/ui/cli/framework.rb +5 -0
  51. data/ui/cli/framework/option_parser.rb +1 -1
  52. data/ui/cli/option_parser.rb +3 -0
  53. data/ui/cli/output.rb +45 -19
  54. 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|
@@ -6,6 +6,7 @@
6
6
  web site for more information on licensing and terms of use.
7
7
  =end
8
8
 
9
+ require 'forwardable'
9
10
  require_relative '../with_node'
10
11
 
11
12
  module Arachni
@@ -202,15 +202,6 @@ module Mutable
202
202
  end
203
203
  end
204
204
 
205
- if !opts[:respect_method]
206
- elem = switch_method
207
- if !generated.include?( elem )
208
- print_debug_mutation elem
209
- yield elem
210
- end
211
- generated << elem
212
- end
213
-
214
205
  nil
215
206
  end
216
207
 
@@ -6,6 +6,8 @@
6
6
  web site for more information on licensing and terms of use.
7
7
  =end
8
8
 
9
+ require 'forwardable'
10
+
9
11
  module Arachni
10
12
  module Element::Capabilities
11
13
  module WithAuditor
@@ -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::Options#audit_cookies_extensively} option.
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
- flip = opts.delete( :param_flip )
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 !Arachni::Options.audit.cookies_extensively?
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 value by' <<
204
+ print_debug_level_2 'Payload not supported as input name by' <<
200
205
  " #{audit_id}: #{payload.inspect}"
201
206
  return
202
207
  end
@@ -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 value by' <<
49
+ print_debug_level_2 'Payload not supported as input name by' <<
50
50
  " #{audit_id}: #{payload.inspect}"
51
51
  return
52
52
  end
@@ -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.
@@ -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
@@ -8,6 +8,9 @@
8
8
 
9
9
  require_relative 'option_group'
10
10
 
11
+ # We need this to be available prior to loading the rest of the groups.
12
+ require_relative 'option_groups/paths'
13
+
11
14
  Dir.glob( "#{File.dirname(__FILE__)}/option_groups/*.rb" ).each do |group|
12
15
  require group
13
16
  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
- @logs = ENV['ARACHNI_FRAMEWORK_LOGDIR'] ?
40
- "#{ENV['ARACHNI_FRAMEWORK_LOGDIR']}/" : @root + 'logs/'
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 = @components + 'services/'
57
+ @services = @components + 'services/'
46
58
  @path_extractors = @components + 'path_extractors/'
47
59
  @fingerprinters = @components + 'fingerprinters/'
48
60
 
49
- @lib = @root + 'lib/arachni/'
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
@@ -17,6 +17,10 @@ class Snapshot < Arachni::OptionGroup
17
17
  # @see Framework#suspend
18
18
  attr_accessor :save_path
19
19
 
20
+ def initialize
21
+ @save_path = Paths.config['framework']['snapshots']
22
+ end
23
+
20
24
  end
21
25
  end
22
26
 
@@ -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
- precision: false,
155
- http: {
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
- page_from_url( url, http_opts ) { |p| block.call find.call( p.forms ) }
157
+ http.get( url, http_opts ) do |r|
158
+ block.call find.call( forms_from_response( r, true ) )
159
+ end
163
160
  else
164
- page_from_url( url, http_opts ).forms
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
- refresh_browser
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
- pages: browser.load( configuration[:url] ).to_page,
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.dom.update configuration[:inputs]
228
- form.dom.auditor = self
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
- form.dom.submit { |p| page = p }
260
+ if has_browser?
261
+ print_debug 'Submitting form.'
262
+ form.submit { |p| page = p }
263
+ print_debug 'Form submitted.'
232
264
 
233
- http.update_cookies browser.cookies
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: block_given? ? :async : :sync
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
- @browser = Browser.new
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
@@ -6,6 +6,8 @@
6
6
  web site for more information on licensing and terms of use.
7
7
  =end
8
8
 
9
+ require 'forwardable'
10
+
9
11
  module Arachni
10
12
  class State
11
13