arachni 0.2.4 → 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 (79) hide show
  1. data/CHANGELOG.md +33 -0
  2. data/README.md +2 -4
  3. data/Rakefile +15 -4
  4. data/bin/arachni +0 -0
  5. data/bin/arachni_web +0 -0
  6. data/bin/arachni_web_autostart +0 -0
  7. data/bin/arachni_xmlrpc +0 -0
  8. data/bin/arachni_xmlrpcd +0 -0
  9. data/bin/arachni_xmlrpcd_monitor +0 -0
  10. data/lib/arachni.rb +1 -1
  11. data/lib/framework.rb +36 -6
  12. data/lib/http.rb +12 -5
  13. data/lib/module/auditor.rb +482 -59
  14. data/lib/module/base.rb +17 -0
  15. data/lib/module/manager.rb +26 -2
  16. data/lib/module/trainer.rb +1 -12
  17. data/lib/module/utilities.rb +12 -0
  18. data/lib/parser/auditable.rb +8 -3
  19. data/lib/parser/elements.rb +11 -0
  20. data/lib/parser/page.rb +3 -1
  21. data/lib/parser/parser.rb +130 -18
  22. data/lib/rpc/xml/server/dispatcher.rb +21 -0
  23. data/lib/spider.rb +141 -82
  24. data/lib/ui/cli/cli.rb +2 -3
  25. data/lib/ui/web/addon_manager.rb +273 -0
  26. data/lib/ui/web/addons/autodeploy.rb +172 -0
  27. data/lib/ui/web/addons/autodeploy/lib/manager.rb +291 -0
  28. data/lib/ui/web/addons/autodeploy/views/index.erb +124 -0
  29. data/lib/ui/web/addons/sample.rb +78 -0
  30. data/lib/ui/web/addons/sample/views/index.erb +4 -0
  31. data/lib/ui/web/addons/scheduler.rb +139 -0
  32. data/lib/ui/web/addons/scheduler/views/index.erb +131 -0
  33. data/lib/ui/web/addons/scheduler/views/options.erb +93 -0
  34. data/lib/ui/web/dispatcher_manager.rb +80 -13
  35. data/lib/ui/web/instance_manager.rb +87 -0
  36. data/lib/ui/web/scheduler.rb +166 -0
  37. data/lib/ui/web/server.rb +142 -202
  38. data/lib/ui/web/server/public/js/jquery-ui-timepicker.js +985 -0
  39. data/lib/ui/web/server/public/plugins/sample/style.css +0 -0
  40. data/lib/ui/web/server/public/style.css +42 -0
  41. data/lib/ui/web/server/views/addon.erb +15 -0
  42. data/lib/ui/web/server/views/addons.erb +46 -0
  43. data/lib/ui/web/server/views/dispatchers.erb +1 -1
  44. data/lib/ui/web/server/views/instance.erb +9 -11
  45. data/lib/ui/web/server/views/layout.erb +14 -1
  46. data/lib/ui/web/server/views/welcome.erb +7 -6
  47. data/lib/ui/web/utilities.rb +134 -0
  48. data/modules/audit/code_injection_timing.rb +6 -2
  49. data/modules/audit/code_injection_timing/payloads.txt +2 -2
  50. data/modules/audit/os_cmd_injection_timing.rb +7 -3
  51. data/modules/audit/os_cmd_injection_timing/payloads.txt +1 -1
  52. data/modules/audit/sqli_blind_rdiff.rb +18 -233
  53. data/modules/audit/sqli_blind_rdiff/payloads.txt +5 -0
  54. data/modules/audit/sqli_blind_timing.rb +9 -2
  55. data/path_extractors/anchors.rb +1 -1
  56. data/path_extractors/forms.rb +1 -1
  57. data/path_extractors/frames.rb +1 -1
  58. data/path_extractors/generic.rb +1 -1
  59. data/path_extractors/links.rb +1 -1
  60. data/path_extractors/meta_refresh.rb +1 -1
  61. data/path_extractors/scripts.rb +1 -1
  62. data/path_extractors/sitemap.rb +1 -1
  63. data/plugins/proxy/server.rb +3 -2
  64. data/plugins/waf_detector.rb +0 -3
  65. metadata +37 -34
  66. data/lib/anemone/cookie_store.rb +0 -35
  67. data/lib/anemone/core.rb +0 -371
  68. data/lib/anemone/exceptions.rb +0 -5
  69. data/lib/anemone/http.rb +0 -144
  70. data/lib/anemone/page.rb +0 -338
  71. data/lib/anemone/page_store.rb +0 -160
  72. data/lib/anemone/storage.rb +0 -34
  73. data/lib/anemone/storage/base.rb +0 -75
  74. data/lib/anemone/storage/exceptions.rb +0 -15
  75. data/lib/anemone/storage/mongodb.rb +0 -89
  76. data/lib/anemone/storage/pstore.rb +0 -50
  77. data/lib/anemone/storage/redis.rb +0 -90
  78. data/lib/anemone/storage/tokyo_cabinet.rb +0 -57
  79. data/lib/anemone/tentacle.rb +0 -40
data/lib/module/base.rb CHANGED
@@ -115,6 +115,19 @@ class Base
115
115
  def clean_up( )
116
116
  end
117
117
 
118
+ #
119
+ # ABSTRACT - OPTIONAL
120
+ #
121
+ # Prevents auditting elements that have been previously
122
+ # logged by any of the modules returned by this method.
123
+ #
124
+ # @return [Array] module names
125
+ #
126
+ def redundant
127
+ # [ 'sqli', 'sqli_blind_rdiff' ]
128
+ []
129
+ end
130
+
118
131
  #
119
132
  # ABSTRACT - REQUIRED
120
133
  #
@@ -169,6 +182,10 @@ class Base
169
182
  Arachni::Module::Manager.register_results( results )
170
183
  end
171
184
 
185
+ def set_framework( framework )
186
+ @framework = framework
187
+ end
188
+
172
189
  end
173
190
  end
174
191
  end
@@ -36,7 +36,7 @@ module Module
36
36
  # @author: Tasos "Zapotek" Laskos
37
37
  # <tasos.laskos@gmail.com>
38
38
  # <zapotek@segfault.gr>
39
- # @version: 0.1
39
+ # @version: 0.1.1
40
40
  #
41
41
  class Manager < Arachni::ComponentManager
42
42
 
@@ -48,7 +48,8 @@ class Manager < Arachni::ComponentManager
48
48
  def initialize( opts )
49
49
  super( opts.dir['modules'], Arachni::Modules )
50
50
  @opts = opts
51
- @@results = []
51
+ @@results = []
52
+ @@issue_set = Set.new
52
53
  end
53
54
 
54
55
  #
@@ -60,8 +61,31 @@ class Manager < Arachni::ComponentManager
60
61
  #
61
62
  def self.register_results( results )
62
63
  @@results |= results
64
+ results.each { |issue| @@issue_set << self.issue_set_id_from_issue( issue ) }
63
65
  end
64
66
 
67
+ def self.issue_set_id_from_issue( issue )
68
+ issue_url = URI( issue.url )
69
+ issue_url_str = issue_url.scheme + "://" + issue_url.host + issue_url.path
70
+ return "#{issue.mod_name}:#{issue.elem}:#{issue.var}:#{issue_url_str}"
71
+ end
72
+
73
+ def self.issue_set_id_from_elem( mod_name, elem )
74
+ elem_url = URI( elem.action )
75
+ elem_url_str = elem_url.scheme + "://" + elem_url.host + elem_url.path
76
+
77
+ return "#{mod_name}:#{elem.type}:#{elem.altered}:#{elem_url_str}"
78
+ end
79
+
80
+ def self.issue_set
81
+ @@issue_set
82
+ end
83
+
84
+ def issue_set
85
+ @@issue_set
86
+ end
87
+
88
+
65
89
  #
66
90
  # Class method
67
91
  #
@@ -27,6 +27,7 @@ class Trainer
27
27
 
28
28
  include Output
29
29
  include ElementDB
30
+ include Utilities
30
31
 
31
32
  attr_writer :page
32
33
  attr_accessor :http
@@ -70,18 +71,6 @@ class Trainer
70
71
 
71
72
  end
72
73
 
73
- #
74
- # Decodes URLs to reverse multiple encodes and removes NULL characters
75
- #
76
- def url_sanitize( url )
77
-
78
- while( url =~ /%/ )
79
- url = ( URI.decode( url ).to_s.unpack( 'A*' )[0] )
80
- end
81
-
82
- return URI.encode( url )
83
- end
84
-
85
74
  def follow?( url )
86
75
  @parser.url = @page.url
87
76
 
@@ -24,6 +24,18 @@ module Module
24
24
  #
25
25
  module Utilities
26
26
 
27
+ #
28
+ # Decodes URLs to reverse multiple encodes and removes NULL characters
29
+ #
30
+ def url_sanitize( url )
31
+
32
+ while( url =~ /%/ )
33
+ url = ( URI.decode( url ).to_s.unpack( 'A*' )[0] )
34
+ end
35
+
36
+ return URI.encode( url )
37
+ end
38
+
27
39
  #
28
40
  # Gets path from URL
29
41
  #
@@ -49,6 +49,11 @@ class Auditable
49
49
  @auditor = auditor
50
50
  end
51
51
 
52
+ def get_auditor
53
+ @auditor
54
+ end
55
+
56
+
52
57
  #
53
58
  # Delegate output related methods to the auditor
54
59
  #
@@ -142,7 +147,7 @@ class Auditable
142
147
  return if skip?( elem )
143
148
 
144
149
  # inform the user about what we're auditing
145
- print_status( get_status_str( opts[:altered] ) )
150
+ print_status( get_status_str( opts[:altered] ) ) if !opts[:silent]
146
151
 
147
152
  # submit the element with the injection values
148
153
  req = elem.submit( opts )
@@ -179,7 +184,7 @@ class Auditable
179
184
  var_combo = []
180
185
  if( !hash || hash.size == 0 ) then return [] end
181
186
 
182
- if( self.is_a?( Arachni::Parser::Element::Form ) )
187
+ if( self.is_a?( Arachni::Parser::Element::Form ) && !opts[:skip_orig] )
183
188
 
184
189
  if !audited?( audit_id( Arachni::Parser::Element::Form::FORM_VALUES_ORIGINAL ) )
185
190
  # this is the original hash, in case the default values
@@ -295,7 +300,7 @@ class Auditable
295
300
  print_error( 'Failed to get responses, backing out... ' )
296
301
  next
297
302
  else
298
- print_status( 'Analyzing response #' + res.request.id.to_s + '...' )
303
+ print_status( 'Analyzing response #' + res.request.id.to_s + '...' ) if elem.opts && !elem.opts[:silent]
299
304
  end
300
305
 
301
306
  # call the block, if there's one
@@ -48,6 +48,8 @@ class Base < Arachni::Element::Auditable
48
48
 
49
49
  attr_accessor :auditable
50
50
 
51
+ attr_accessor :orig
52
+
51
53
  #
52
54
  # Relatively 'raw' hash holding the element's attributes, values, etc.
53
55
  #
@@ -113,6 +115,8 @@ class Link < Base
113
115
  @method = 'get'
114
116
 
115
117
  @auditable = @raw['vars']
118
+ @orig = @auditable.deep_clone
119
+ @orig.freeze
116
120
  end
117
121
 
118
122
  def http_request( url, opts )
@@ -155,6 +159,8 @@ class Form < Base
155
159
  @method = @raw['attrs']['method']
156
160
 
157
161
  @auditable = simple['auditable'] || {}
162
+ @orig = @auditable.deep_clone
163
+ @orig.freeze
158
164
  end
159
165
 
160
166
  def http_request( url, opts )
@@ -248,6 +254,9 @@ class Cookie < Base
248
254
  |cookie|
249
255
  Options.instance.exclude_cookies.include?( cookie )
250
256
  }
257
+
258
+ @orig = @auditable.deep_clone
259
+ @orig.freeze
251
260
  end
252
261
 
253
262
  def http_request( url, opts )
@@ -274,6 +283,8 @@ class Header < Base
274
283
  @method = 'header'
275
284
 
276
285
  @auditable = @raw
286
+ @orig = @auditable.deep_clone
287
+ @orig.freeze
277
288
  end
278
289
 
279
290
  def http_request( url, opts )
data/lib/parser/page.rb CHANGED
@@ -19,7 +19,7 @@ class Parser
19
19
  # @author: Tasos "Zapotek" Laskos
20
20
  # <tasos.laskos@gmail.com>
21
21
  # <zapotek@segfault.gr>
22
- # @version: 0.2
22
+ # @version: 0.2.1
23
23
  #
24
24
  class Page
25
25
 
@@ -60,6 +60,8 @@ class Page
60
60
  #
61
61
  attr_accessor :response_headers
62
62
 
63
+ attr_accessor :paths
64
+
63
65
  #
64
66
  # @see Parser#links
65
67
  #
data/lib/parser/parser.rb CHANGED
@@ -10,9 +10,11 @@
10
10
  module Arachni
11
11
 
12
12
  opts = Arachni::Options.instance
13
+ require 'webrick'
13
14
  require opts.dir['lib'] + 'parser/elements'
14
15
  require opts.dir['lib'] + 'parser/page'
15
16
  require opts.dir['lib'] + 'module/utilities'
17
+ require opts.dir['lib'] + 'component_manager'
16
18
 
17
19
  #
18
20
  # Analyzer class
@@ -41,12 +43,41 @@ require opts.dir['lib'] + 'module/utilities'
41
43
  # @author: Tasos "Zapotek" Laskos
42
44
  # <tasos.laskos@gmail.com>
43
45
  # <zapotek@segfault.gr>
44
- # @version: 0.2
46
+ # @version: 0.2.1
45
47
  #
46
48
  class Parser
47
-
49
+ include Arachni::UI::Output
48
50
  include Arachni::Module::Utilities
49
51
 
52
+ module Extractors
53
+ #
54
+ # Base Spider parser class for modules.
55
+ #
56
+ # The aim of such modules is to extract paths from a webpage for the Spider to follow.
57
+ #
58
+ #
59
+ # @author: Tasos "Zapotek" Laskos
60
+ # <tasos.laskos@gmail.com>
61
+ # <zapotek@segfault.gr>
62
+ # @version: 0.1
63
+ # @abstract
64
+ #
65
+ class Paths
66
+
67
+ #
68
+ # This method must be implemented by all modules and must return an array
69
+ # of paths as plain strings
70
+ #
71
+ # @param [Nokogiri] Nokogiri document
72
+ #
73
+ # @return [Array<String>] paths
74
+ #
75
+ def run( doc )
76
+
77
+ end
78
+ end
79
+ end
80
+
50
81
  #
51
82
  # @return [String] the url of the page
52
83
  #
@@ -68,7 +99,7 @@ class Parser
68
99
  def initialize( opts, res )
69
100
  @opts = opts
70
101
 
71
- @url = res.effective_url
102
+ @url = url_sanitize( res.effective_url )
72
103
  @html = res.body
73
104
  @response_headers = res.headers_hash
74
105
  end
@@ -89,6 +120,7 @@ class Parser
89
120
  :html => @html,
90
121
  :headers => [],
91
122
  :response_headers => @response_headers,
123
+ :paths => [],
92
124
  :forms => [],
93
125
  :links => [],
94
126
  :cookies => [],
@@ -108,14 +140,25 @@ class Parser
108
140
 
109
141
  jar = preped.merge( jar )
110
142
 
143
+ c_links = links
144
+
145
+ if !( vars = link_vars( @url ) ).empty?
146
+ url = to_absolute( @url )
147
+ c_links << Arachni::Parser::Element::Link.new( url, {
148
+ 'href' => url,
149
+ 'vars' => vars
150
+ } )
151
+ end
152
+
111
153
  return Page.new( {
112
154
  :url => @url,
113
155
  :query_vars => link_vars( @url ),
114
156
  :html => @html,
115
157
  :headers => headers(),
116
158
  :response_headers => @response_headers,
159
+ :paths => paths(),
117
160
  :forms => @opts.audit_forms ? forms() : [],
118
- :links => @opts.audit_links ? links() : [],
161
+ :links => @opts.audit_links ? c_links : [],
119
162
  :cookies => merge_with_cookiestore( merge_with_cookiejar( cookies_arr ) ),
120
163
  :cookiejar => jar
121
164
  } )
@@ -258,7 +301,7 @@ class Parser
258
301
  if( !elements[i]['attrs'] || !elements[i]['attrs']['action'] )
259
302
  action = @url.to_s
260
303
  else
261
- action = elements[i]['attrs']['action']
304
+ action = url_sanitize( elements[i]['attrs']['action'] )
262
305
  end
263
306
  action = URI.escape( action ).to_s
264
307
 
@@ -322,8 +365,17 @@ class Parser
322
365
  if( !include?( link['href'] ) ) then next end
323
366
  if !in_domain?( URI.parse( link['href'] ) ) then next end
324
367
 
325
- link['vars'] = link_vars( link['href'] )
368
+ link['vars'] = {}
369
+ link_vars( link['href'] ).each_pair {
370
+ |key, val|
371
+ begin
372
+ link['vars'][key] = url_sanitize( val )
373
+ rescue
374
+ link['vars'][key] = val
375
+ end
376
+ }
326
377
 
378
+ link['href'] = url_sanitize( link['href'] )
327
379
 
328
380
  link_arr << Element::Link.new( @url, link )
329
381
 
@@ -356,11 +408,12 @@ class Parser
356
408
  rescue
357
409
  end
358
410
 
411
+
359
412
  # don't ask me why....
360
413
  if @response_headers.to_s.substring?( 'set-cookie' )
361
414
  begin
362
- cookies << WEBrick::Cookie.parse_set_cookies( @response_headers['Set-Cookie'].to_s )
363
- cookies << WEBrick::Cookie.parse_set_cookies( @response_headers['set-cookie'].to_s )
415
+ cookies << ::WEBrick::Cookie.parse_set_cookies( @response_headers['Set-Cookie'].to_s )
416
+ cookies << ::WEBrick::Cookie.parse_set_cookies( @response_headers['set-cookie'].to_s )
364
417
  rescue
365
418
  return cookies_arr
366
419
  end
@@ -390,6 +443,32 @@ class Parser
390
443
  return cookies_arr
391
444
  end
392
445
 
446
+ def dir( url )
447
+ URI( File.dirname( URI( url.to_s ).path ) + '/' )
448
+ end
449
+
450
+ #
451
+ # Array of distinct links to follow
452
+ #
453
+ # @return [Array<URI>]
454
+ #
455
+ def paths
456
+ return @paths unless @paths.nil?
457
+ @paths = []
458
+ return @paths if !doc
459
+
460
+ run_extractors( ).each {
461
+ |path|
462
+ next if path.nil? or path.empty?
463
+ abs = to_absolute( path ) rescue next
464
+
465
+ @paths << abs if in_domain?( abs )
466
+ }
467
+
468
+ @paths.uniq!
469
+ return @paths
470
+ end
471
+
393
472
  #
394
473
  # Extracts variables and their values from a link
395
474
  #
@@ -434,26 +513,36 @@ class Parser
434
513
  end
435
514
  rescue Exception => e
436
515
  return nil if link.nil?
437
- # return link
438
516
  end
439
517
 
440
518
  # remove anchor
441
- link = URI.encode( link.to_s.gsub( /#[a-zA-Z0-9_-]*$/, '' ) )
519
+ link = URI.encode( link.to_s.gsub( /#[a-zA-Z0-9_-]*$/,'' ) )
442
520
 
443
- begin
444
- relative = URI(link)
445
- url = URI.parse( @url )
521
+ if url = base
522
+ base_url = URI( url )
523
+ else
524
+ base_url = URI( @url )
525
+ end
446
526
 
447
- absolute = url.merge(relative)
527
+ relative = URI( link )
528
+ absolute = base_url.merge( relative )
448
529
 
449
- absolute.path = '/' if absolute.path.empty?
450
- rescue Exception => e
451
- return
452
- end
530
+ absolute.path = '/' if absolute.path && absolute.path.empty?
453
531
 
454
532
  return absolute.to_s
455
533
  end
456
534
 
535
+
536
+ def base
537
+ begin
538
+ tmp = doc.search( '//base[@href]' )
539
+ return tmp[0]['href'].dup
540
+ rescue
541
+ return
542
+ end
543
+ end
544
+
545
+
457
546
  #
458
547
  # Returns +true+ if *uri* is in the same domain as the page, returns
459
548
  # +false+ otherwise
@@ -508,6 +597,29 @@ class Parser
508
597
 
509
598
  private
510
599
 
600
+ #
601
+ # Runs all Spider (path extraction) modules and returns an array of paths
602
+ #
603
+ # @return [Array] paths
604
+ #
605
+ def run_extractors
606
+ lib = @opts.dir['root'] + 'path_extractors/'
607
+
608
+
609
+ begin
610
+ @@manager ||= ::Arachni::ComponentManager.new( lib, Extractors )
611
+
612
+ return @@manager.available.map {
613
+ |name|
614
+ @@manager[name].new.run( doc )
615
+ }.flatten.uniq
616
+
617
+ rescue ::Exception => e
618
+ print_error( e.to_s )
619
+ print_debug_backtrace( e )
620
+ end
621
+ end
622
+
511
623
  #
512
624
  # Merges an array of form inputs with an array of form selects
513
625
  #