arachni 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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