arachni 1.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +2 -2
  4. data/arachni.gemspec +1 -1
  5. data/components/checks/active/code_injection_php_input_wrapper.rb +8 -3
  6. data/components/checks/active/file_inclusion.rb +7 -3
  7. data/components/checks/active/path_traversal.rb +7 -3
  8. data/components/checks/passive/grep/cookie_set_for_parent_domain.rb +5 -3
  9. data/components/plugins/cookie_collector.rb +1 -1
  10. data/components/plugins/proxy.rb +4 -3
  11. data/components/plugins/vector_feed.rb +1 -1
  12. data/components/reporters/html/default/issue.erb +5 -0
  13. data/components/reporters/stdout.rb +4 -0
  14. data/lib/arachni/browser.rb +25 -6
  15. data/lib/arachni/browser/element_locator.rb +1 -1
  16. data/lib/arachni/browser/javascript/taint_tracer.rb +3 -3
  17. data/lib/arachni/browser/javascript/taint_tracer/frame.rb +1 -1
  18. data/lib/arachni/browser/javascript/taint_tracer/frame/called_function.rb +1 -1
  19. data/lib/arachni/browser/javascript/taint_tracer/sink/base.rb +1 -1
  20. data/lib/arachni/check/auditor.rb +2 -0
  21. data/lib/arachni/component/manager.rb +2 -2
  22. data/lib/arachni/component/options/base.rb +2 -2
  23. data/lib/arachni/element/base.rb +2 -2
  24. data/lib/arachni/element/cookie.rb +4 -4
  25. data/lib/arachni/element/form.rb +1 -1
  26. data/lib/arachni/element/generic_dom.rb +1 -1
  27. data/lib/arachni/framework.rb +9 -1
  28. data/lib/arachni/http/client.rb +2 -0
  29. data/lib/arachni/http/request.rb +2 -2
  30. data/lib/arachni/http/response.rb +1 -1
  31. data/lib/arachni/issue.rb +2 -2
  32. data/lib/arachni/option_group.rb +1 -1
  33. data/lib/arachni/option_groups/input.rb +1 -1
  34. data/lib/arachni/option_groups/scope.rb +1 -1
  35. data/lib/arachni/page.rb +1 -1
  36. data/lib/arachni/page/dom/transition.rb +3 -3
  37. data/lib/arachni/parser.rb +3 -1
  38. data/lib/arachni/platform/list.rb +1 -1
  39. data/lib/arachni/report.rb +1 -1
  40. data/lib/arachni/rpc/client/instance/framework.rb +6 -6
  41. data/lib/arachni/rpc/client/instance/service.rb +7 -7
  42. data/lib/arachni/rpc/server/dispatcher.rb +18 -5
  43. data/lib/arachni/rpc/server/dispatcher/node.rb +13 -6
  44. data/lib/arachni/rpc/server/framework/distributor.rb +1 -1
  45. data/lib/arachni/rpc/server/framework/master.rb +1 -1
  46. data/lib/arachni/rpc/server/framework/multi_instance.rb +2 -2
  47. data/lib/arachni/rpc/server/instance.rb +11 -3
  48. data/lib/arachni/ruby/hash.rb +7 -6
  49. data/lib/arachni/state/framework.rb +1 -0
  50. data/lib/version +1 -1
  51. data/spec/arachni/browser_spec.rb +25 -0
  52. data/spec/arachni/component/manager_spec.rb +1 -1
  53. data/spec/arachni/element/cookie_spec.rb +3 -3
  54. data/spec/arachni/http/request_spec.rb +3 -3
  55. data/spec/arachni/option_groups/scope_spec.rb +2 -2
  56. data/spec/arachni/parser_spec.rb +7 -0
  57. data/spec/arachni/reporter/manager_spec.rb +1 -1
  58. data/spec/arachni/rpc/server/dispatcher/node_spec.rb +2 -0
  59. data/spec/arachni/rpc/server/framework_spec.rb +1 -1
  60. data/spec/arachni/ruby/hash_spec.rb +8 -8
  61. data/spec/support/servers/checks/passive/grep/cookie_set_for_parent_domain.rb +1 -1
  62. data/spec/support/shared/element/capabilities/inputtable.rb +2 -2
  63. data/ui/cli/utilities.rb +3 -0
  64. metadata +4 -4
@@ -48,7 +48,7 @@ class Base
48
48
  attr_reader :initialization_options
49
49
 
50
50
  def initialize( options )
51
- options = options.symbolize_keys( false )
51
+ options = options.my_symbolize_keys( false )
52
52
 
53
53
  if !(options[:url] || options[:action])
54
54
  fail 'Needs :url or :action option.'
@@ -167,7 +167,7 @@ class Base
167
167
 
168
168
  when 'initialization_options'
169
169
  value.is_a?( Hash ) ?
170
- value.symbolize_keys( false ) : value
170
+ value.my_symbolize_keys( false ) : value
171
171
 
172
172
  when 'method'
173
173
  value.to_sym
@@ -77,7 +77,7 @@ class Cookie < Base
77
77
  @data[:expires] = Time.parse( @data[:expires] ) rescue nil
78
78
  end
79
79
 
80
- @data[:domain] ||= ".#{parsed_uri.host}"
80
+ @data[:domain] ||= parsed_uri.host
81
81
 
82
82
  @default_inputs = self.inputs.dup.freeze
83
83
  end
@@ -321,7 +321,7 @@ class Cookie < Base
321
321
  c['expires'] = nil
322
322
  end
323
323
  c['secure'] = (c['secure'] == 'TRUE') ? true : false
324
- new( { url: url }.merge( c.symbolize_keys ) )
324
+ new( { url: url }.merge( c.my_symbolize_keys ) )
325
325
  end.flatten.compact
326
326
  end
327
327
 
@@ -428,7 +428,7 @@ class Cookie < Base
428
428
  cookie_hash['name'] = decode( cookie.name )
429
429
  cookie_hash['value'] = decode( cookie.value )
430
430
 
431
- new( { url: url }.merge( cookie_hash.symbolize_keys ) )
431
+ new( { url: url }.merge( cookie_hash.my_symbolize_keys ) )
432
432
  end.flatten.compact
433
433
  end
434
434
  alias :parse_set_cookie :from_set_cookie
@@ -461,7 +461,7 @@ class Cookie < Base
461
461
  #
462
462
  # @return [String]
463
463
  def encode( str, type = :value )
464
- reserved = "+;%\0"
464
+ reserved = "+;%\0\'\""
465
465
  reserved << '=' if type == :name
466
466
 
467
467
  URI.encode( str, reserved ).recode.gsub( ' ', '+' )
@@ -78,7 +78,7 @@ class Form < Base
78
78
 
79
79
  cinputs = (options[:inputs] || {}).inject({}) do |h, (name, value_or_info)|
80
80
  if value_or_info.is_a? Hash
81
- value_or_info = value_or_info.symbolize_keys
81
+ value_or_info = value_or_info.my_symbolize_keys
82
82
  h[name] = value_or_info[:value]
83
83
  @input_details[name.to_s] = value_or_info
84
84
  else
@@ -113,7 +113,7 @@ class GenericDOM < Base
113
113
  Arachni::Page::DOM::Transition.from_rpc_data( value )
114
114
 
115
115
  when 'initialization_options'
116
- value = value.is_a?( Hash ) ? value.symbolize_keys(false) : value
116
+ value = value.is_a?( Hash ) ? value.my_symbolize_keys(false) : value
117
117
  value[:transition] =
118
118
  Arachni::Page::DOM::Transition.from_rpc_data( value[:transition] )
119
119
  value
@@ -198,7 +198,15 @@ class Framework
198
198
 
199
199
  # Initialization may take a while so since we lazy load this make sure
200
200
  # that only one thread gets to this code at a time.
201
- synchronize { @browser_cluster ||= BrowserCluster.new }
201
+ synchronize do
202
+ if !@browser_cluster
203
+ state.set_status_message :browser_cluster_startup
204
+ end
205
+
206
+ @browser_cluster ||= BrowserCluster.new
207
+ state.clear_status_messages
208
+ @browser_cluster
209
+ end
202
210
  end
203
211
 
204
212
  # Starts the scan.
@@ -719,6 +719,7 @@ class Client
719
719
  print_debug_level_3 '------------'
720
720
  print_debug_level_3 'Queued request.'
721
721
  print_debug_level_3 "ID#: #{request.id}"
722
+ print_debug_level_3 "Performer: #{request.performer}"
722
723
  print_debug_level_3 "URL: #{request.url}"
723
724
  print_debug_level_3 "Method: #{request.method}"
724
725
  print_debug_level_3 "Params: #{request.parameters}"
@@ -747,6 +748,7 @@ class Client
747
748
  if debug_level_3?
748
749
  print_debug_level_3 '------------'
749
750
  print_debug_level_3 "Got response for request ID#: #{response.request.id}"
751
+ print_debug_level_3 "Performer: #{response.request.performer}"
750
752
  print_debug_level_3 "Status: #{response.code}"
751
753
  print_debug_level_3 "Code: #{response.return_code}"
752
754
  print_debug_level_3 "Message: #{response.return_message}"
@@ -343,7 +343,7 @@ class Request < Message
343
343
  if proxy
344
344
  options.merge!(
345
345
  proxy: proxy,
346
- proxytype: proxy_type
346
+ proxytype: (proxy_type || :http).to_sym
347
347
  )
348
348
 
349
349
  if proxy_user_password
@@ -353,7 +353,7 @@ class Request < Message
353
353
  elsif Arachni::Options.http.proxy_host && Arachni::Options.http.proxy_port
354
354
  options.merge!(
355
355
  proxy: "#{Arachni::Options.http.proxy_host}:#{Arachni::Options.http.proxy_port}",
356
- proxytype: Arachni::Options.http.proxy_type
356
+ proxytype: (Arachni::Options.http.proxy_type || :http).to_sym
357
357
  )
358
358
 
359
359
  if Arachni::Options.http.proxy_username && Arachni::Options.http.proxy_password
@@ -176,7 +176,7 @@ class Response < Message
176
176
  def to_rpc_data
177
177
  data = to_h
178
178
  data[:request] = request.to_rpc_data
179
- data.stringify_keys(false)
179
+ data.my_stringify_keys(false)
180
180
  end
181
181
 
182
182
  # @param [Hash] data {#to_rpc_data}
@@ -462,13 +462,13 @@ class Issue
462
462
  end)
463
463
  end
464
464
 
465
- value.symbolize_keys(false)
465
+ value.my_symbolize_keys(false)
466
466
 
467
467
  when 'variations'
468
468
  value.map { |i| from_rpc_data i }
469
469
 
470
470
  when 'remarks'
471
- value.symbolize_keys
471
+ value.my_symbolize_keys
472
472
 
473
473
  when 'platform_name', 'platform_type'
474
474
  next if !value
@@ -54,7 +54,7 @@ class OptionGroup
54
54
  end
55
55
 
56
56
  def to_rpc_data
57
- to_h.stringify_keys(false)
57
+ to_h.my_stringify_keys(false)
58
58
  end
59
59
 
60
60
  # @return [Hash]
@@ -147,7 +147,7 @@ class Input < Arachni::OptionGroup
147
147
  h = super
148
148
  [:values, :default_values].each do |k|
149
149
  # We can't have blocks in there...
150
- h[k] = h[k].select{ |_, v| v.is_a? String }.stringify
150
+ h[k] = h[k].select{ |_, v| v.is_a? String }.my_stringify
151
151
  end
152
152
  h
153
153
  end
@@ -219,7 +219,7 @@ class Scope < Arachni::OptionGroup
219
219
  d = super
220
220
 
221
221
  %w(redundant_path_patterns url_rewrites).each do |k|
222
- d[k] = d[k].stringify
222
+ d[k] = d[k].my_stringify
223
223
  end
224
224
 
225
225
  %w(exclude_path_patterns exclude_content_patterns include_path_patterns).each do |k|
@@ -478,7 +478,7 @@ class Page
478
478
  # @return [Hash]
479
479
  # Data representing this instance that are suitable the RPC transmission.
480
480
  def to_rpc_data
481
- data = to_initialization_options.stringify_keys(false)
481
+ data = to_initialization_options.my_stringify_keys(false)
482
482
  data['dom'] = dom.to_rpc_data
483
483
  data['element_audit_whitelist'] = element_audit_whitelist.to_a
484
484
  data['response'] = data['response'].to_rpc_data
@@ -143,7 +143,7 @@ class Transition
143
143
  self.event = event
144
144
  @element = element
145
145
 
146
- @options = options.symbolize_keys(false)
146
+ @options = options.my_symbolize_keys(false)
147
147
  @clock = Time.now
148
148
 
149
149
  return self if !block_given?
@@ -253,7 +253,7 @@ class Transition
253
253
  # @return [Hash]
254
254
  # Data representing this instance that are suitable the RPC transmission.
255
255
  def to_rpc_data
256
- h = to_hash.stringify_keys(false)
256
+ h = to_hash.my_stringify_keys(false)
257
257
  h['element'] = element.to_rpc_data_or_self
258
258
  h
259
259
  end
@@ -276,7 +276,7 @@ class Transition
276
276
  end
277
277
 
278
278
  when 'options'
279
- value.symbolize_keys
279
+ value.my_symbolize_keys
280
280
 
281
281
  else
282
282
  value
@@ -217,7 +217,9 @@ class Parser
217
217
  # @return [Hash]
218
218
  # Parameters found in {#url}.
219
219
  def link_vars
220
- @link_vars ||= uri_parse( @url ).rewrite.query_parameters.freeze
220
+ return {} if (!parsed = uri_parse( @url ))
221
+
222
+ @link_vars ||= parsed.rewrite.query_parameters.freeze
221
223
  end
222
224
 
223
225
  # @return [Array<Element::Cookie>]
@@ -221,7 +221,7 @@ class List
221
221
  when String
222
222
  platforms.to_sym
223
223
  when Hash
224
- platforms.symbolize_keys
224
+ platforms.my_symbolize_keys
225
225
  when Enumerable, Array
226
226
  platforms.to_a.flatten.map( &:to_sym ).uniq.sort
227
227
  end
@@ -245,7 +245,7 @@ class Report
245
245
 
246
246
  data['plugins'] = data['plugins'].inject({}) do |h, (k, v)|
247
247
  k = k.to_sym
248
- h[k] = v.symbolize_keys(false)
248
+ h[k] = v.my_symbolize_keys(false)
249
249
  next h if !h[k][:options]
250
250
 
251
251
  h[k][:options] = v['options'].map do |option|
@@ -26,9 +26,9 @@ class Framework < Proxy
26
26
  %w(list_reporters list_plugins).each do |m|
27
27
  translate m do |data|
28
28
  data.map do |c|
29
- c = c.symbolize_keys
29
+ c = c.my_symbolize_keys
30
30
  c[:options] = c[:options].map do |o|
31
- o = o.symbolize_keys
31
+ o = o.my_symbolize_keys
32
32
  o[:name] = o[:name].to_sym
33
33
  o[:type] = o[:type].to_sym
34
34
  o
@@ -39,18 +39,18 @@ class Framework < Proxy
39
39
  end
40
40
 
41
41
  translate :list_platforms do |platforms|
42
- platforms.inject({}) { |h, (k, v)| h[k] = v.symbolize_keys; h }
42
+ platforms.inject({}) { |h, (k, v)| h[k] = v.my_symbolize_keys; h }
43
43
  end
44
44
 
45
45
  translate :statistics do |stats|
46
- stats.symbolize_keys
46
+ stats.my_symbolize_keys
47
47
  end
48
48
 
49
49
  translate :progress do |data, options = {}|
50
50
  sitemap = data.delete('sitemap')
51
51
  issues = data.delete('issues')
52
52
 
53
- data = data.symbolize_keys
53
+ data = data.my_symbolize_keys
54
54
  data[:status] = data[:status].to_sym
55
55
 
56
56
  if issues
@@ -62,7 +62,7 @@ class Framework < Proxy
62
62
  end
63
63
 
64
64
  if data[:instances]
65
- data[:instances] = data[:instances].map(&:symbolize_keys)
65
+ data[:instances] = data[:instances].map(&:my_symbolize_keys)
66
66
  end
67
67
 
68
68
  if sitemap
@@ -26,9 +26,9 @@ class Service < Proxy
26
26
  %w(list_reporters list_plugins).each do |m|
27
27
  translate m do |data|
28
28
  data.map do |c|
29
- c = c.symbolize_keys
29
+ c = c.my_symbolize_keys
30
30
  c[:options] = c[:options].map do |o|
31
- o = o.symbolize_keys
31
+ o = o.my_symbolize_keys
32
32
  o[:name] = o[:name].to_sym
33
33
  o[:type] = o[:type].to_sym
34
34
  o
@@ -39,18 +39,18 @@ class Service < Proxy
39
39
  end
40
40
 
41
41
  translate :list_platforms do |platforms|
42
- platforms.inject({}) { |h, (k, v)| h[k] = v.symbolize_keys; h }
42
+ platforms.inject({}) { |h, (k, v)| h[k] = v.my_symbolize_keys; h }
43
43
  end
44
44
 
45
45
  translate :progress do |data|
46
46
  sitemap = data.delete('sitemap')
47
47
  issues = data.delete('issues')
48
48
 
49
- data = data.symbolize_keys
49
+ data = data.my_symbolize_keys
50
50
  data[:status] = data[:status].to_sym
51
51
 
52
52
  if data[:instances]
53
- data[:instances] = data[:instances].map(&:symbolize_keys)
53
+ data[:instances] = data[:instances].map(&:my_symbolize_keys)
54
54
  end
55
55
 
56
56
  if issues
@@ -68,7 +68,7 @@ class Service < Proxy
68
68
  sitemap = data.delete('sitemap')
69
69
  issues = data.delete('issues')
70
70
 
71
- data = data.symbolize_keys
71
+ data = data.my_symbolize_keys
72
72
  data[:status] = data[:status].to_sym
73
73
 
74
74
  if issues
@@ -76,7 +76,7 @@ class Service < Proxy
76
76
  end
77
77
 
78
78
  if data[:instances]
79
- data[:instances] = data[:instances].map(&:symbolize_keys)
79
+ data[:instances] = data[:instances].map(&:my_symbolize_keys)
80
80
  end
81
81
 
82
82
  if sitemap
@@ -76,15 +76,21 @@ class Dispatcher
76
76
  @consumed_pids = []
77
77
  @pool = Reactor.global.create_queue
78
78
 
79
+ print_status "Populating the pool with #{@options.dispatcher.pool_size} Instances."
79
80
  if @options.dispatcher.pool_size > 0
80
81
  @options.dispatcher.pool_size.times { add_instance_to_pool( false ) }
81
82
  end
82
83
 
84
+ print_status 'Waiting for Instances to come on-line.'
85
+
83
86
  # Check up on the pool and start the server once it has been filled.
84
87
  Reactor.global.at_interval( 0.1 ) do |task|
88
+ print_debug "Instances: #{@pool.size}/#{@options.dispatcher.pool_size}"
85
89
  next if @options.dispatcher.pool_size != @pool.size
86
90
  task.done
87
91
 
92
+ print_status 'Instances are on-line.'
93
+
88
94
  _services.each do |name, service|
89
95
  @server.add_handler( name, service.new( @options, self ) )
90
96
  end
@@ -284,6 +290,14 @@ class Dispatcher
284
290
 
285
291
  # Starts the dispatcher's server
286
292
  def run
293
+ Reactor.global.on_error do |_, e|
294
+ print_error "Arachni::Reactor: #{e}"
295
+
296
+ e.backtrace.each do |l|
297
+ print_error "Arachni::Reactor: #{l}"
298
+ end
299
+ end
300
+
287
301
  print_status 'Ready'
288
302
  @server.start
289
303
  rescue => e
@@ -326,17 +340,15 @@ class Dispatcher
326
340
  print_status "Instance added to pool -- PID: #{pid} - " +
327
341
  "Port: #{port} - Owner: #{owner}"
328
342
 
329
- url = "#{@options.dispatcher.external_address}:#{port}"
330
-
331
343
  # Wait until the Instance has booted before adding it to the pool.
332
- Client::Instance.when_ready( url, token ) do
344
+ Client::Instance.when_ready( "#{@options.rpc.server_address}:#{port}", token ) do
333
345
  @operation_in_progress = false
334
346
 
335
347
  @pool << {
336
348
  'token' => token,
337
349
  'pid' => pid,
338
350
  'port' => port,
339
- 'url' => url,
351
+ 'url' => "#{@options.dispatcher.external_address}:#{port}",
340
352
  'owner' => owner,
341
353
  'birthdate' => Time.now.to_s
342
354
  }
@@ -350,7 +362,8 @@ class Dispatcher
350
362
  end
351
363
 
352
364
  def connect_to_peer( url )
353
- Client::Dispatcher.new( @options, url )
365
+ @rpc_clients ||= {}
366
+ @rpc_clients[url] ||= Client::Dispatcher.new( @options, url )
354
367
  end
355
368
 
356
369
  def struct_to_h( struct )
@@ -52,12 +52,18 @@ class Server::Dispatcher::Node
52
52
  @nodes_info_cache = []
53
53
 
54
54
  if (neighbour = @options.dispatcher.neighbour)
55
- # add neighbour and announce him to everyone
56
- add_neighbour( neighbour, true )
57
-
58
- # grab the neighbour's neighbours
55
+ # Grab the neighbour's neighbours.
59
56
  connect_to_peer( neighbour ).neighbours do |urls|
60
- fail "Neighbour '#{neighbour}' is unreachable." if urls.rpc_exception?
57
+ if urls.rpc_exception?
58
+ add_dead_neighbour( neighbour )
59
+ print_info "Neighbour seems dead: #{neighbour}"
60
+ add_dead_neighbour( neighbour )
61
+ next
62
+ end
63
+
64
+ # Add neighbour and announce it to everyone.
65
+ add_neighbour( neighbour, true )
66
+
61
67
  urls.each { |url| @neighbours << url if url != @url }
62
68
  end
63
69
  end
@@ -229,7 +235,8 @@ class Server::Dispatcher::Node
229
235
  end
230
236
 
231
237
  def connect_to_peer( url )
232
- Client::Dispatcher.new( @options, url ).node
238
+ @rpc_clients ||= {}
239
+ @rpc_clients[url] ||= Client::Dispatcher.new( @options, url ).node
233
240
  end
234
241
 
235
242
  end
@@ -27,7 +27,7 @@ module Distributor
27
27
  # The hash must hold the `'url'` and the `'token'`. In subsequent calls
28
28
  # the `'token'` can be omitted.
29
29
  def connect_to_instance( instance )
30
- instance = instance.symbolize_keys
30
+ instance = instance.my_symbolize_keys
31
31
  @instance_connections ||= {}
32
32
 
33
33
  if @instance_connections[instance[:url]]