cms_scanner 0.0.2

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 (147) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +6 -0
  5. data/.travis.yml +14 -0
  6. data/Gemfile +6 -0
  7. data/README.md +20 -0
  8. data/Rakefile +9 -0
  9. data/app/app.rb +4 -0
  10. data/app/controllers.rb +2 -0
  11. data/app/controllers/core.rb +46 -0
  12. data/app/controllers/core/cli_options.rb +68 -0
  13. data/app/controllers/interesting_files.rb +12 -0
  14. data/app/finders.rb +1 -0
  15. data/app/finders/interesting_files.rb +21 -0
  16. data/app/finders/interesting_files/fantastico_fileslist.rb +23 -0
  17. data/app/finders/interesting_files/headers.rb +15 -0
  18. data/app/finders/interesting_files/robots_txt.rb +22 -0
  19. data/app/finders/interesting_files/search_replace_db_2.rb +28 -0
  20. data/app/finders/interesting_files/xml_rpc.rb +62 -0
  21. data/app/formatters.rb +3 -0
  22. data/app/formatters/cli.rb +18 -0
  23. data/app/formatters/cli_no_colour.rb +15 -0
  24. data/app/formatters/json.rb +12 -0
  25. data/app/models.rb +5 -0
  26. data/app/models/fantastico_fileslist.rb +20 -0
  27. data/app/models/headers.rb +37 -0
  28. data/app/models/interesting_file.rb +30 -0
  29. data/app/models/robots_txt.rb +20 -0
  30. data/app/models/xml_rpc.rb +35 -0
  31. data/app/views/cli/core/finished.erb +4 -0
  32. data/app/views/cli/core/started.erb +3 -0
  33. data/app/views/cli/interesting_files/findings.erb +19 -0
  34. data/app/views/cli/scan_aborted.erb +4 -0
  35. data/app/views/json/core/finished.erb +3 -0
  36. data/app/views/json/core/started.erb +3 -0
  37. data/app/views/json/interesting_files/findings.erb +1 -0
  38. data/app/views/json/scan_aborted.erb +4 -0
  39. data/cms_scanner.gemspec +37 -0
  40. data/examples/views/cli/wp_custom/test.erb +1 -0
  41. data/examples/views/json/wp_custom/test.erb +1 -0
  42. data/examples/wpscan.rb +29 -0
  43. data/lib/cms_scanner.rb +71 -0
  44. data/lib/cms_scanner/browser.rb +68 -0
  45. data/lib/cms_scanner/browser/actions.rb +48 -0
  46. data/lib/cms_scanner/browser/options.rb +53 -0
  47. data/lib/cms_scanner/cache/file_store.rb +75 -0
  48. data/lib/cms_scanner/cache/typhoeus.rb +21 -0
  49. data/lib/cms_scanner/controller.rb +90 -0
  50. data/lib/cms_scanner/controllers.rb +34 -0
  51. data/lib/cms_scanner/errors/auth_errors.rb +15 -0
  52. data/lib/cms_scanner/finders.rb +5 -0
  53. data/lib/cms_scanner/finders/finder.rb +27 -0
  54. data/lib/cms_scanner/finders/finding.rb +32 -0
  55. data/lib/cms_scanner/finders/findings.rb +25 -0
  56. data/lib/cms_scanner/finders/independent_finder.rb +30 -0
  57. data/lib/cms_scanner/finders/independent_finders.rb +41 -0
  58. data/lib/cms_scanner/formatter.rb +118 -0
  59. data/lib/cms_scanner/formatter/buffer.rb +15 -0
  60. data/lib/cms_scanner/target.rb +33 -0
  61. data/lib/cms_scanner/target/platform.rb +2 -0
  62. data/lib/cms_scanner/target/platform/php.rb +39 -0
  63. data/lib/cms_scanner/target/platform/wordpress.rb +35 -0
  64. data/lib/cms_scanner/target/platform/wordpress/custom_directories.rb +62 -0
  65. data/lib/cms_scanner/target/server.rb +3 -0
  66. data/lib/cms_scanner/target/server/apache.rb +43 -0
  67. data/lib/cms_scanner/target/server/generic.rb +34 -0
  68. data/lib/cms_scanner/target/server/iis.rb +48 -0
  69. data/lib/cms_scanner/version.rb +4 -0
  70. data/lib/cms_scanner/web_site.rb +68 -0
  71. data/lib/helper.rb +24 -0
  72. data/spec/app/controllers/core_spec.rb +152 -0
  73. data/spec/app/controllers/interesting_files_spec.rb +50 -0
  74. data/spec/app/finders/interesting_files/fantastico_fileslist_spec.rb +68 -0
  75. data/spec/app/finders/interesting_files/headers_spec.rb +38 -0
  76. data/spec/app/finders/interesting_files/robots_txt_spec.rb +56 -0
  77. data/spec/app/finders/interesting_files/search_replace_db_2_spec.rb +55 -0
  78. data/spec/app/finders/interesting_files/xml_rpc_spec.rb +138 -0
  79. data/spec/app/finders/interesting_files_spec.rb +13 -0
  80. data/spec/app/formatters/cli_no_colour_spec.rb +17 -0
  81. data/spec/app/formatters/cli_spec.rb +21 -0
  82. data/spec/app/formatters/json_spec.rb +33 -0
  83. data/spec/app/models/fantastico_fileslist_spec.rb +32 -0
  84. data/spec/app/models/headers_spec.rb +52 -0
  85. data/spec/app/models/interesting_file_spec.rb +51 -0
  86. data/spec/app/models/robots_txt_spec.rb +28 -0
  87. data/spec/app/models/xml_rpc_spec.rb +47 -0
  88. data/spec/cache/.gitignore +4 -0
  89. data/spec/dummy_finders.rb +41 -0
  90. data/spec/fixtures/interesting_files/fantastico_fileslist/fantastico_fileslist.txt +12 -0
  91. data/spec/fixtures/interesting_files/file.txt +4 -0
  92. data/spec/fixtures/interesting_files/headers/interesting.txt +14 -0
  93. data/spec/fixtures/interesting_files/headers/no_interesting.txt +12 -0
  94. data/spec/fixtures/interesting_files/robots_txt/robots.txt +10 -0
  95. data/spec/fixtures/interesting_files/search_replace_db_2/searchreplacedb2.php +188 -0
  96. data/spec/fixtures/interesting_files/xml_rpc/homepage_in_scope_pingback.html +7 -0
  97. data/spec/fixtures/interesting_files/xml_rpc/homepage_out_of_scope_pingback.html +7 -0
  98. data/spec/fixtures/interesting_files/xml_rpc/xmlrpc.php +1 -0
  99. data/spec/fixtures/output.txt +0 -0
  100. data/spec/fixtures/target/platform/php/debug_log/debug.log +2 -0
  101. data/spec/fixtures/target/platform/php/fpd/wp_rss_functions.php +2 -0
  102. data/spec/fixtures/target/platform/wordpress/custom_directories/custom_w_spaces.html +10 -0
  103. data/spec/fixtures/target/platform/wordpress/custom_directories/default.html +14 -0
  104. data/spec/fixtures/target/platform/wordpress/custom_directories/https.html +12 -0
  105. data/spec/fixtures/target/platform/wordpress/detection/default.html +4 -0
  106. data/spec/fixtures/target/platform/wordpress/detection/not_wp.html +8 -0
  107. data/spec/fixtures/target/platform/wordpress/detection/wp_includes.html +3 -0
  108. data/spec/fixtures/target/server/apache/directory_listing/2.2.16.html +15 -0
  109. data/spec/fixtures/target/server/generic/server/apache/basic.txt +5 -0
  110. data/spec/fixtures/target/server/generic/server/iis/basic.txt +6 -0
  111. data/spec/fixtures/target/server/generic/server/not_detected.txt +3 -0
  112. data/spec/fixtures/target/server/iis/directory_listing/no_parent.html +3 -0
  113. data/spec/fixtures/target/server/iis/directory_listing/with_parent.html +3 -0
  114. data/spec/fixtures/views/base/ctrl/local.erb +1 -0
  115. data/spec/fixtures/views/base/ctrl/test.erb +3 -0
  116. data/spec/fixtures/views/base/global.erb +1 -0
  117. data/spec/fixtures/views/base/test.erb +2 -0
  118. data/spec/fixtures/views/based_format/test.erb +1 -0
  119. data/spec/fixtures/views/json/render_me.erb +4 -0
  120. data/spec/lib/browser_spec.rb +141 -0
  121. data/spec/lib/cache/file_store_spec.rb +101 -0
  122. data/spec/lib/cache/typhoeus_spec.rb +30 -0
  123. data/spec/lib/cms_scanner_spec.rb +45 -0
  124. data/spec/lib/controller_spec.rb +23 -0
  125. data/spec/lib/controllers_spec.rb +52 -0
  126. data/spec/lib/finders/findings_spec.rb +49 -0
  127. data/spec/lib/finders/independent_finders_spec.rb +98 -0
  128. data/spec/lib/formatter_spec.rb +136 -0
  129. data/spec/lib/sub_scanner_spec.rb +27 -0
  130. data/spec/lib/target/platforms_spec.rb +13 -0
  131. data/spec/lib/target/servers_spec.rb +13 -0
  132. data/spec/lib/target_spec.rb +50 -0
  133. data/spec/lib/web_site_spec.rb +124 -0
  134. data/spec/shared_examples.rb +11 -0
  135. data/spec/shared_examples/browser_actions.rb +32 -0
  136. data/spec/shared_examples/finding.rb +20 -0
  137. data/spec/shared_examples/formatter_buffer.rb +8 -0
  138. data/spec/shared_examples/formatter_class_methods.rb +26 -0
  139. data/spec/shared_examples/independent_finder.rb +33 -0
  140. data/spec/shared_examples/target/platform/php.rb +58 -0
  141. data/spec/shared_examples/target/platform/wordpress.rb +41 -0
  142. data/spec/shared_examples/target/platform/wordpress/custom_directories.rb +50 -0
  143. data/spec/shared_examples/target/server/apache.rb +33 -0
  144. data/spec/shared_examples/target/server/generic.rb +34 -0
  145. data/spec/shared_examples/target/server/iis.rb +38 -0
  146. data/spec/spec_helper.rb +41 -0
  147. metadata +432 -0
@@ -0,0 +1,48 @@
1
+ module CMSScanner
2
+ class Browser
3
+ # Browser Actions (get, post etc)
4
+ module Actions
5
+ # @param [ String ] url
6
+ # @param [ Hash ] params
7
+ #
8
+ # @return [ Typhoeus::Response ]
9
+ def get(url, params = {})
10
+ process(url, params.merge(method: :get))
11
+ end
12
+
13
+ # @param [ String ] url
14
+ # @param [ Hash ] params
15
+ #
16
+ # @return [ Typhoeus::Response ]
17
+ def post(url, params = {})
18
+ process(url, params.merge(method: :post))
19
+ end
20
+
21
+ # @param [ String ] url
22
+ # @param [ Hash ] params
23
+ #
24
+ # @return [ Typhoeus::Response ]
25
+ def head(url, params = {})
26
+ process(url, params.merge(method: :head))
27
+ end
28
+
29
+ # @param [ String ] url
30
+ # @param [ Hash ] params
31
+ #
32
+ # @return [ Typhoeus::Response ]
33
+ def get_and_follow_location(url, params = {})
34
+ get(url, params.merge(followlocation: true))
35
+ end
36
+
37
+ protected
38
+
39
+ # @param [ String ] url
40
+ # @param [ Hash ] params
41
+ #
42
+ # @return [ Typhoeus::Response ]
43
+ def process(url, params)
44
+ Typhoeus::Request.new(url, NS::Browser.instance.request_params(params)).run
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,53 @@
1
+ module Typhoeus
2
+ # Hack to have a setter for the :max_concurrency
3
+ # Which will be officially added in the next version
4
+ # See: https://github.com/typhoeus/typhoeus/issues/366
5
+ class Hydra
6
+ attr_accessor :max_concurrency
7
+ end
8
+ end
9
+
10
+ module CMSScanner
11
+ # Options available in the Browser
12
+ class Browser
13
+ OPTIONS = [
14
+ :cache_ttl,
15
+ :cookie_jar,
16
+ :cookie_string,
17
+ :connect_timeout,
18
+ :http_auth,
19
+ :max_threads,
20
+ :proxy,
21
+ :proxy_auth,
22
+ :request_timeout,
23
+ :user_agent
24
+ ]
25
+
26
+ attr_accessor(*OPTIONS)
27
+
28
+ # @param [ Hash ] options
29
+ def load_options(options = {})
30
+ OPTIONS.each do |sym|
31
+ send("#{sym}=", options[sym]) if options.key?(sym)
32
+ end
33
+ end
34
+
35
+ def hydra
36
+ @hydra ||= Typhoeus::Hydra.new(max_concurrency: max_threads || 1)
37
+ end
38
+
39
+ # Set the threads attribute and update
40
+ # the max_concurrency of Typhoeus::Hydra
41
+ #
42
+ # @param [ Integer ] number
43
+ def max_threads=(number)
44
+ @max_threads = number.to_i > 0 ? number.to_i : 1
45
+ hydra.max_concurrency = @max_threads
46
+ end
47
+
48
+ # Default user agent
49
+ def user_agent
50
+ @user_agent ||= "CMSScanner v#{VERSION}"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,75 @@
1
+ module CMSScanner
2
+ module Cache
3
+ # Cache Implementation using files
4
+ class FileStore
5
+ attr_reader :storage_path, :serializer
6
+
7
+ # The serializer must have the 2 methods #load and #dump
8
+ # (Marshal and YAML have them)
9
+ # YAML is Human Readable, contrary to Marshal which store in a binary format
10
+ # Marshal does not need any "require"
11
+ #
12
+ # @param [ String ] storage_path
13
+ # @param [ ] serializer
14
+ def initialize(storage_path, serializer = Marshal)
15
+ @storage_path = File.expand_path(storage_path)
16
+ @serializer = serializer
17
+
18
+ FileUtils.mkdir_p(@storage_path) unless Dir.exist?(@storage_path)
19
+ end
20
+
21
+ # TODO: rename this to clear ?
22
+ def clean
23
+ Dir[File.join(storage_path, '*')].each do |f|
24
+ File.delete(f) unless File.symlink?(f)
25
+ end
26
+ end
27
+
28
+ # @param [ String ] key
29
+ #
30
+ # @return [ Mixed ]
31
+ def read_entry(key)
32
+ file_path = entry_path(key)
33
+
34
+ return if expired_entry?(key)
35
+
36
+ serializer.load(File.read(file_path))
37
+ rescue
38
+ nil
39
+ end
40
+
41
+ # @param [ String ] key
42
+ # @param [ Mixed ] data_to_store
43
+ # @param [ Integer ] cache_ttl
44
+ def write_entry(key, data_to_store, cache_ttl)
45
+ return unless cache_ttl.to_i > 0
46
+
47
+ File.write(entry_path(key), serializer.dump(data_to_store))
48
+ File.write(entry_expiration_path(key), Time.now.to_i + cache_ttl)
49
+ end
50
+
51
+ # @param [ String ] key
52
+ #
53
+ # @return [ String ] The file path associated to the key
54
+ def entry_path(key)
55
+ File.join(storage_path, key)
56
+ end
57
+
58
+ # @param [ String ] key
59
+ #
60
+ # @return [ String ] The expiration file path associated to the key
61
+ def entry_expiration_path(key)
62
+ entry_path(key) + '.expiration'
63
+ end
64
+
65
+ private
66
+
67
+ # @param [ String ] key
68
+ #
69
+ # @return [ Boolean ]
70
+ def expired_entry?(key)
71
+ File.read(entry_expiration_path(key)).to_i <= Time.now.to_i rescue true
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,21 @@
1
+ require 'cms_scanner/cache/file_store'
2
+
3
+ module CMSScanner
4
+ module Cache
5
+ # Cache implementation for Typhoeus
6
+ class Typhoeus < FileStore
7
+ # @param [ Typhoeus::Request ] request
8
+ #
9
+ # @return [ Typhoeus::Response ]
10
+ def get(request)
11
+ read_entry(request.hash.to_s)
12
+ end
13
+
14
+ # @param [ Typhoeus::Request ] request
15
+ # @param [ Typhoeus::Response ] response
16
+ def set(request, response)
17
+ write_entry(request.hash.to_s, response, request.cache_ttl)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,90 @@
1
+ module CMSScanner
2
+ module Controller
3
+ # Base Controller
4
+ class Base
5
+ include OptParseValidator
6
+
7
+ # @return [ Array<OptParseValidator::OptBase> ]
8
+ def cli_options; end
9
+
10
+ def before_scan; end
11
+
12
+ def run; end
13
+
14
+ def after_scan; end
15
+
16
+ def ==(other)
17
+ self.class == other.class
18
+ end
19
+
20
+ # @return [ Target ]
21
+ def target
22
+ @@target ||= NS::Target.new(parsed_options[:url])
23
+ end
24
+
25
+ # Set the parsed options and initialize the browser
26
+ # with them
27
+ #
28
+ # @param [ Hash ] options
29
+ def self.parsed_options=(options)
30
+ @@parsed_options = options
31
+
32
+ NS::Browser.instance(options)
33
+ end
34
+
35
+ # @return [ Hash ]
36
+ def parsed_options
37
+ @@parsed_options ||= {}
38
+ end
39
+
40
+ # @return [ Hash ]
41
+ def datastore
42
+ @@datastore ||= {}
43
+ end
44
+
45
+ # @return [ Formatter::Base ]
46
+ def formatter
47
+ @@formatter ||= NS::Formatter.load(parsed_options[:format], datastore[:views])
48
+ end
49
+
50
+ # @see Formatter#output
51
+ #
52
+ # @return [ Void ]
53
+ def output(tpl, vars = {})
54
+ formatter.output(*tpl_params(tpl, vars))
55
+ end
56
+
57
+ # @see Formatter#render
58
+ #
59
+ # @return [ String ]
60
+ def render(tpl, vars = {})
61
+ formatter.render(*tpl_params(tpl, vars))
62
+ end
63
+
64
+ protected
65
+
66
+ # @param [ String ] tpl
67
+ # @param [ Hash ] vars
68
+ #
69
+ # @return [ Array<String> ]
70
+ def tpl_params(tpl, vars)
71
+ [
72
+ tpl,
73
+ instance_variable_values.merge(vars),
74
+ self.class.name.demodulize.underscore
75
+ ]
76
+ end
77
+
78
+ # @return [ Hash ] All the instance variable keys (and their values) and the verbose value
79
+ def instance_variable_values
80
+ h = { verbose: parsed_options[:verbose] }
81
+ instance_variables.each do |a|
82
+ s = a.to_s
83
+ n = s[1..s.size]
84
+ h[n.to_sym] = instance_variable_get(a)
85
+ end
86
+ h
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,34 @@
1
+ module CMSScanner
2
+ # Controllers Container
3
+ class Controllers < Array
4
+ attr_reader :option_parser
5
+
6
+ def initialize(option_parser = OptParseValidator::OptParser.new)
7
+ @option_parser = option_parser
8
+ end
9
+
10
+ # @param [ Controller::Base ] controller
11
+ #
12
+ # @retun [ Controllers ] self
13
+ def <<(controller)
14
+ options = controller.cli_options
15
+
16
+ unless include?(controller)
17
+ option_parser.add(*options) if options
18
+ super(controller)
19
+ end
20
+ self
21
+ end
22
+
23
+ def run
24
+ parsed_options = option_parser.results
25
+ first.class.parsed_options = parsed_options
26
+
27
+ redirect_output_to_file(parsed_options[:output]) if parsed_options[:output]
28
+
29
+ each(&:before_scan)
30
+ each(&:run)
31
+ reverse.each(&:after_scan)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ module CMSScanner
2
+ # HTTP Authentication Required Error
3
+ class HTTPAuthRequiredError < StandardError
4
+ def message
5
+ 'HTTP authentication required (or was invalid), please provide it with --http-auth'
6
+ end
7
+ end
8
+
9
+ # Proxy Authentication Required Error
10
+ class ProxyAuthRequiredError < StandardError
11
+ def message
12
+ 'Proxy authentication required (or was invalid), please provide it with --proxy-auth'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ require 'cms_scanner/finders/finder'
2
+ require 'cms_scanner/finders/finding'
3
+ require 'cms_scanner/finders/findings'
4
+ require 'cms_scanner/finders/independent_finders'
5
+ require 'cms_scanner/finders/independent_finder'
@@ -0,0 +1,27 @@
1
+ module CMSScanner
2
+ module Finders
3
+ # Finder
4
+ class Finder
5
+ # Constants for common found_by
6
+ DIRECT_FILE_ACCESS = 'Direct File Access (aggressive detection)'
7
+
8
+ attr_accessor :target
9
+
10
+ def initialize(target)
11
+ @target = target
12
+ end
13
+
14
+ # @param [ Hash ] _opts
15
+ def passive(_opts = {})
16
+ end
17
+
18
+ # @param [ Hash ] _opts
19
+ def aggressive(_opts = {})
20
+ end
21
+
22
+ def found_by
23
+ "#{self.class.to_s.demodulize} (#{caller_locations(1, 1)[0].label} detection)"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ module CMSScanner
2
+ module Finders
3
+ # Finding
4
+ module Finding
5
+ FINDING_OPTS = [:confidence, :confirmed_by, :references, :found_by, :interesting_entries]
6
+
7
+ attr_accessor(*FINDING_OPTS)
8
+
9
+ # @return [ Array ]
10
+ def references
11
+ @references ||= []
12
+ end
13
+
14
+ # @return [ Array ]
15
+ def confirmed_by
16
+ @confirmed_by ||= []
17
+ end
18
+
19
+ # Should be overriden in child classes
20
+ # @return [ Array ]
21
+ def interesting_entries
22
+ @interesting_entries ||= []
23
+ end
24
+
25
+ # @param [ Hash ] opts
26
+ # TODO: Maybe use instance_variable_set ?
27
+ def parse_finding_options(opts = {})
28
+ FINDING_OPTS.each { |opt| send("#{opt}=", opts[opt]) if opts.key?(opt) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module CMSScanner
2
+ module Finders
3
+ # Findings container
4
+ class Findings < Array
5
+ # Override to include the confirmed_by logic
6
+ def <<(other)
7
+ each do |found|
8
+ next unless found == other
9
+
10
+ found.confirmed_by << other
11
+ # TODO: Increase confidence (e.g: (found + other) / 1.5 ?)
12
+ return self
13
+ end
14
+
15
+ super(other)
16
+ end
17
+
18
+ # Append the elements of other into self AND returns self
19
+ # This is not the default behaviour of Array#+ but it's intended
20
+ def +(other)
21
+ other.each { |f| self << f }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ module CMSScanner
2
+ module Finders
3
+ # Independent Finder
4
+ module IndependentFinder
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ # Hack to have the #find as a class method
10
+ module ClassMethods
11
+ def find(target, opts = {})
12
+ new(target).find(opts)
13
+ end
14
+ end
15
+
16
+ # @param [ Hash ] opts
17
+ # @option opts [ Symbol ] mode (:mixed, :passive, :aggressive)
18
+ #
19
+ # @return [ Findings ]
20
+ def find(opts = {})
21
+ finders.run(opts)
22
+ end
23
+
24
+ # @return [ Array ]
25
+ def finders
26
+ @finders ||= NS::Finders::IndependentFinders.new
27
+ end
28
+ end
29
+ end
30
+ end