new_cms_scanner 0.13.7

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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +26 -0
  4. data/app/app.rb +24 -0
  5. data/app/controllers/core/cli_options.rb +117 -0
  6. data/app/controllers/core.rb +82 -0
  7. data/app/controllers/interesting_findings.rb +25 -0
  8. data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
  9. data/app/finders/interesting_findings/headers.rb +17 -0
  10. data/app/finders/interesting_findings/robots_txt.rb +20 -0
  11. data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
  12. data/app/finders/interesting_findings/xml_rpc.rb +61 -0
  13. data/app/finders/interesting_findings.rb +25 -0
  14. data/app/formatters/cli.rb +65 -0
  15. data/app/formatters/cli_no_color.rb +9 -0
  16. data/app/formatters/cli_no_colour.rb +17 -0
  17. data/app/formatters/json.rb +14 -0
  18. data/app/models/fantastico_fileslist.rb +34 -0
  19. data/app/models/headers.rb +44 -0
  20. data/app/models/interesting_finding.rb +48 -0
  21. data/app/models/robots_txt.rb +31 -0
  22. data/app/models/search_replace_db_2.rb +17 -0
  23. data/app/models/user.rb +35 -0
  24. data/app/models/version.rb +49 -0
  25. data/app/models/xml_rpc.rb +78 -0
  26. data/app/user_agents.txt +46 -0
  27. data/app/views/cli/core/banner.erb +1 -0
  28. data/app/views/cli/core/finished.erb +8 -0
  29. data/app/views/cli/core/help.erb +4 -0
  30. data/app/views/cli/core/started.erb +6 -0
  31. data/app/views/cli/core/version.erb +1 -0
  32. data/app/views/cli/interesting_findings/_array.erb +10 -0
  33. data/app/views/cli/interesting_findings/findings.erb +23 -0
  34. data/app/views/cli/scan_aborted.erb +5 -0
  35. data/app/views/cli/usage.erb +3 -0
  36. data/app/views/json/core/banner.erb +1 -0
  37. data/app/views/json/core/finished.erb +10 -0
  38. data/app/views/json/core/help.erb +4 -0
  39. data/app/views/json/core/started.erb +5 -0
  40. data/app/views/json/core/version.erb +1 -0
  41. data/app/views/json/interesting_findings/findings.erb +24 -0
  42. data/app/views/json/scan_aborted.erb +5 -0
  43. data/lib/cms_scanner/browser/actions.rb +48 -0
  44. data/lib/cms_scanner/browser/options.rb +90 -0
  45. data/lib/cms_scanner/browser.rb +96 -0
  46. data/lib/cms_scanner/cache/file_store.rb +77 -0
  47. data/lib/cms_scanner/cache/typhoeus.rb +25 -0
  48. data/lib/cms_scanner/controller.rb +105 -0
  49. data/lib/cms_scanner/controllers.rb +67 -0
  50. data/lib/cms_scanner/errors/http.rb +72 -0
  51. data/lib/cms_scanner/errors/scan.rb +14 -0
  52. data/lib/cms_scanner/errors.rb +11 -0
  53. data/lib/cms_scanner/exit_code.rb +25 -0
  54. data/lib/cms_scanner/finders/base_finders.rb +45 -0
  55. data/lib/cms_scanner/finders/finder/breadth_first_dictionary_attack.rb +121 -0
  56. data/lib/cms_scanner/finders/finder/enumerator.rb +77 -0
  57. data/lib/cms_scanner/finders/finder/fingerprinter.rb +48 -0
  58. data/lib/cms_scanner/finders/finder/smart_url_checker/findings.rb +33 -0
  59. data/lib/cms_scanner/finders/finder/smart_url_checker.rb +60 -0
  60. data/lib/cms_scanner/finders/finder.rb +75 -0
  61. data/lib/cms_scanner/finders/finding.rb +54 -0
  62. data/lib/cms_scanner/finders/findings.rb +26 -0
  63. data/lib/cms_scanner/finders/independent_finder.rb +30 -0
  64. data/lib/cms_scanner/finders/independent_finders.rb +26 -0
  65. data/lib/cms_scanner/finders/same_type_finder.rb +19 -0
  66. data/lib/cms_scanner/finders/same_type_finders.rb +26 -0
  67. data/lib/cms_scanner/finders/unique_finder.rb +19 -0
  68. data/lib/cms_scanner/finders/unique_finders.rb +47 -0
  69. data/lib/cms_scanner/finders.rb +12 -0
  70. data/lib/cms_scanner/formatter/buffer.rb +17 -0
  71. data/lib/cms_scanner/formatter.rb +149 -0
  72. data/lib/cms_scanner/helper.rb +7 -0
  73. data/lib/cms_scanner/numeric.rb +13 -0
  74. data/lib/cms_scanner/parsed_cli.rb +37 -0
  75. data/lib/cms_scanner/progressbar_null_output.rb +23 -0
  76. data/lib/cms_scanner/public_suffix/domain.rb +42 -0
  77. data/lib/cms_scanner/references.rb +132 -0
  78. data/lib/cms_scanner/scan.rb +88 -0
  79. data/lib/cms_scanner/target/hashes.rb +45 -0
  80. data/lib/cms_scanner/target/platform/php.rb +62 -0
  81. data/lib/cms_scanner/target/platform.rb +3 -0
  82. data/lib/cms_scanner/target/scope.rb +103 -0
  83. data/lib/cms_scanner/target/server/apache.rb +27 -0
  84. data/lib/cms_scanner/target/server/generic.rb +72 -0
  85. data/lib/cms_scanner/target/server/iis.rb +29 -0
  86. data/lib/cms_scanner/target/server/nginx.rb +27 -0
  87. data/lib/cms_scanner/target/server.rb +6 -0
  88. data/lib/cms_scanner/target.rb +124 -0
  89. data/lib/cms_scanner/typhoeus/hydra.rb +12 -0
  90. data/lib/cms_scanner/typhoeus/response.rb +27 -0
  91. data/lib/cms_scanner/version.rb +6 -0
  92. data/lib/cms_scanner/vulnerability.rb +46 -0
  93. data/lib/cms_scanner/web_site.rb +145 -0
  94. data/lib/cms_scanner.rb +141 -0
  95. metadata +426 -0
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cms_scanner/formatter/buffer'
4
+
5
+ module CMSScanner
6
+ # Formatter
7
+ module Formatter
8
+ # Module to be able to do Formatter.load() & Formatter.availables
9
+ # and do that as well when the Formatter is included in another module
10
+ module ClassMethods
11
+ # @param [ String ] format
12
+ # @param [ Array<String> ] custom_views
13
+ #
14
+ # @return [ Formatter::Base ]
15
+ def load(format = nil, custom_views = nil)
16
+ format ||= 'cli'
17
+ custom_views ||= []
18
+
19
+ f = const_get(format.tr('-', '_').camelize).new
20
+ custom_views.each { |v| f.views_directories << v }
21
+ f
22
+ end
23
+
24
+ # @return [ Array<String> ] The list of the available formatters (except the Base one)
25
+ # @note: the #load method above should then be used to create the associated formatter
26
+ def availables
27
+ formatters = NS::Formatter.constants.select do |const|
28
+ name = NS::Formatter.const_get(const)
29
+ name.is_a?(Class) && name != NS::Formatter::Base
30
+ end
31
+
32
+ formatters.map { |sym| sym.to_s.underscore.dasherize }
33
+ end
34
+ end
35
+
36
+ extend ClassMethods
37
+
38
+ def self.included(base)
39
+ base.extend(ClassMethods)
40
+ end
41
+
42
+ # This module should be implemented in the code which uses this Framework to
43
+ # be able to override/implements instance methods for all the Formatters
44
+ # w/o having to include/write the methods in each formatters.
45
+ #
46
+ # Example: to override the #views_directories (see the wpscan-v3/lib/wpscan/formatter.rb)
47
+ module InstanceMethods
48
+ end
49
+
50
+ # Base Formatter
51
+ class Base
52
+ attr_reader :controller_name
53
+
54
+ def initialize
55
+ # Can't put this at the top level of the class, due to the NS::
56
+ extend NS::Formatter::InstanceMethods
57
+ end
58
+
59
+ # @return [ String ] The underscored name of the class
60
+ def format
61
+ self.class.name.demodulize.underscore
62
+ end
63
+
64
+ # @return [ Boolean ]
65
+ def user_interaction?
66
+ format == 'cli'
67
+ end
68
+
69
+ # @return [ String ] The underscored format to use as a base
70
+ def base_format; end
71
+
72
+ # @return [ Array<String> ]
73
+ def formats
74
+ [format, base_format].compact
75
+ end
76
+
77
+ # This is called after the scan
78
+ # and used in some formatters (e.g JSON)
79
+ # to indent results
80
+ def beautify; end
81
+
82
+ # @see #render
83
+ def output(tpl, vars = {}, controller_name = nil)
84
+ puts render(tpl, vars, controller_name)
85
+ end
86
+
87
+ ERB_SUPPORTS_KVARGS = ::ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
88
+
89
+ # @param [ String ] tpl
90
+ # @param [ Hash ] vars
91
+ # @param [ String ] controller_name
92
+ def render(tpl, vars = {}, controller_name = nil)
93
+ template_vars(vars)
94
+ @controller_name = controller_name if controller_name
95
+
96
+ # '-' is used to disable new lines when -%> is used
97
+ # See http://www.ruby-doc.org/stdlib-2.1.1/libdoc/erb/rdoc/ERB.html
98
+ # Since ruby 2.6, KVARGS are supported and passing argument is deprecated in ruby 3+
99
+ if ERB_SUPPORTS_KVARGS
100
+ ERB.new(File.read(view_path(tpl)), trim_mode: '-').result(binding)
101
+ else
102
+ ERB.new(File.read(view_path(tpl)), nil, '-').result(binding)
103
+ end
104
+ end
105
+
106
+ # @param [ Hash ] vars
107
+ #
108
+ # @return [ Void ]
109
+ def template_vars(vars)
110
+ vars.each do |key, value|
111
+ instance_variable_set("@#{key}", value) unless key == :views_directories
112
+ end
113
+ end
114
+
115
+ # @param [ String ] tpl
116
+ #
117
+ # @return [ String ] The path of the view
118
+ def view_path(tpl)
119
+ if tpl[0, 1] == '@' # Global Template
120
+ tpl = tpl.delete('@')
121
+ else
122
+ raise 'The controller_name can not be nil' unless controller_name
123
+
124
+ tpl = "#{controller_name}/#{tpl}"
125
+ end
126
+
127
+ raise "Wrong tpl format: '#{tpl}'" unless %r{\A[\w/_]+\z}.match?(tpl)
128
+
129
+ views_directories.reverse_each do |dir|
130
+ formats.each do |format|
131
+ potential_file = File.join(dir, format, "#{tpl}.erb")
132
+
133
+ return potential_file if File.exist?(potential_file)
134
+ end
135
+ end
136
+
137
+ raise "View not found for #{format}/#{tpl}"
138
+ end
139
+
140
+ # @return [ Array<String> ] The directories to look into for views
141
+ def views_directories
142
+ @views_directories ||= [
143
+ APP_DIR, NS::APP_DIR,
144
+ File.join(Dir.home, ".#{NS.app_name}"), File.join(Dir.pwd, ".#{NS.app_name}")
145
+ ].uniq.reduce([]) { |acc, elem| acc << Pathname.new(elem).join('views').to_s }
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @param [ String ] file The file path
4
+ def redirect_output_to_file(file)
5
+ $stdout.reopen(file, 'w')
6
+ $stdout.sync = true
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hack of the Numeric class
4
+ class Numeric
5
+ # @return [ String ] A human readable string of the value
6
+ def bytes_to_human
7
+ units = %w[B KB MB GB TB]
8
+ e = abs.zero? ? abs : (Math.log(abs) / Math.log(1024)).floor
9
+ s = format('%<s>.3f', s: (abs.to_f / (1024**e)))
10
+
11
+ s.sub(/\.?0*$/, " #{units[e]}")
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ # Class to hold the parsed CLI options and have them available via
5
+ # methods, such as #verbose?, rather than from the hash.
6
+ # This is similar to an OpenStruct, but class wise (rather than instance), and with
7
+ # the logic to update the Browser options accordinly
8
+ class ParsedCli
9
+ # @return [ Hash ]
10
+ def self.options
11
+ @options ||= {}
12
+ end
13
+
14
+ # Sets the CLI options, and put them into the Browser as well
15
+ # @param [ Hash ] options
16
+ def self.options=(options)
17
+ @options = options.dup || {}
18
+
19
+ NS::Browser.reset
20
+ NS::Browser.instance(@options)
21
+ end
22
+
23
+ # @return [ Boolean ]
24
+ def self.verbose?
25
+ options[:verbose] ? true : false
26
+ end
27
+
28
+ # Unknown methods will return nil, this is the expected behaviour
29
+ # rubocop:disable Style/MissingRespondToMissing
30
+ def self.method_missing(method_name, *_args, &_block)
31
+ super if method_name == :new
32
+
33
+ options[method_name.to_sym]
34
+ end
35
+ # rubocop:enable Style/MissingRespondToMissing
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby-progressbar/outputs/null'
4
+
5
+ module CMSScanner
6
+ # Adds the feature to log message sent to #log
7
+ # So they can be retrieved at some point, like after a password attack with a JSON output
8
+ # which won't display the progressbar but still call #log for errors etc
9
+ class ProgressBarNullOutput < ::ProgressBar::Outputs::Null
10
+ # @retutn [ Array<String> ]
11
+ def logs
12
+ @logs ||= []
13
+ end
14
+
15
+ # Override of parent method
16
+ # @return [ Array<String> ] return the logs when no string provided
17
+ def log(string = nil)
18
+ return logs if string.nil?
19
+
20
+ logs << string unless logs.include?(string)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PublicSuffix
4
+ # Monkey Patch to include the match logic
5
+ class Domain
6
+ # For Sanity
7
+ def ==(other)
8
+ name == other.name
9
+ end
10
+
11
+ # @return [ Boolean ]
12
+ #
13
+ def match(pattern)
14
+ pattern = PublicSuffix.parse(pattern) unless pattern.is_a?(PublicSuffix::Domain)
15
+
16
+ return name == pattern.name unless pattern.trd
17
+ return false unless tld == pattern.tld && sld == pattern.sld
18
+
19
+ matching_pattern?(pattern)
20
+ end
21
+
22
+ protected
23
+
24
+ # @rturn [ Boolean ]
25
+ def matching_pattern?(pattern)
26
+ pattern_trds = pattern.trd.split('.')
27
+ domain_trds = trd.split('.')
28
+
29
+ case pattern_trds.first
30
+ when '*'
31
+ pattern_trds[1..-1] == domain_trds[1..-1]
32
+ when '**'
33
+ pa = pattern_trds[1..-1]
34
+ pa_size = pa.size
35
+
36
+ domain_trds[domain_trds.size - pa_size, pa_size] == pa
37
+ else
38
+ name == pattern.name
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ # References related to the issue
5
+ module References
6
+ extend ActiveSupport::Concern
7
+
8
+ # See ActiveSupport::Concern
9
+ module ClassMethods
10
+ # @return [ Array<Symbol> ]
11
+ def references_keys
12
+ @references_keys ||= %i[cve exploitdb url metasploit packetstorm securityfocus youtube]
13
+ end
14
+ end
15
+
16
+ # @param [ Hash ] refs
17
+ def references=(refs)
18
+ @references = {}
19
+
20
+ self.class.references_keys.each do |key|
21
+ next unless refs.key?(key)
22
+
23
+ @references[key] = if key == :youtube
24
+ Array(refs[:youtube]).map { |id| youtube_url(id) }
25
+ else
26
+ Array(refs[key]).map(&:to_s)
27
+ end
28
+ end
29
+ end
30
+
31
+ # @return [ Hash ]
32
+ def references
33
+ @references ||= {}
34
+ end
35
+
36
+ # @return [ Array<String> ] All the references URLs
37
+ def references_urls
38
+ cve_urls + exploitdb_urls + urls + msf_urls +
39
+ packetstorm_urls + securityfocus_urls + youtube_urls
40
+ end
41
+
42
+ # @return [ Array<String> ] The CVEs
43
+ def cves
44
+ references[:cve] || []
45
+ end
46
+
47
+ # @return [ Array<String> ]
48
+ def cve_urls
49
+ cves.reduce([]) { |acc, elem| acc << cve_url(elem) }
50
+ end
51
+
52
+ # @return [ String ] The URL to the CVE
53
+ def cve_url(cve)
54
+ "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-#{cve}"
55
+ end
56
+
57
+ # @return [ Array<String> ] The ExploitDB ID
58
+ def exploitdb_ids
59
+ references[:exploitdb] || []
60
+ end
61
+
62
+ # @return [ Array<String> ]
63
+ def exploitdb_urls
64
+ exploitdb_ids.reduce([]) { |acc, elem| acc << exploitdb_url(elem) }
65
+ end
66
+
67
+ # @return [ String ]
68
+ def exploitdb_url(id)
69
+ "https://www.exploit-db.com/exploits/#{id}/"
70
+ end
71
+
72
+ # @return [ String<Array> ]
73
+ def urls
74
+ references[:url] || []
75
+ end
76
+
77
+ # @return [ Array<String> ] The metasploit modules
78
+ def msf_modules
79
+ references[:metasploit] || []
80
+ end
81
+
82
+ # @return [ Array<String> ]
83
+ def msf_urls
84
+ msf_modules.reduce([]) { |acc, elem| acc << msf_url(elem) }
85
+ end
86
+
87
+ # @return [ String ] The URL to the metasploit module page
88
+ def msf_url(mod)
89
+ "https://www.rapid7.com/db/modules/#{mod.sub(%r{^/}, '')}/"
90
+ end
91
+
92
+ # @return [ Array<String> ] The Packetstormsecurity IDs
93
+ def packetstorm_ids
94
+ @packetstorm_ids ||= references[:packetstorm] || []
95
+ end
96
+
97
+ # @return [ Array<String> ]
98
+ def packetstorm_urls
99
+ packetstorm_ids.reduce([]) { |acc, elem| acc << packetstorm_url(elem) }
100
+ end
101
+
102
+ # @return [ String ]
103
+ def packetstorm_url(id)
104
+ "https://packetstormsecurity.com/files/#{id}/"
105
+ end
106
+
107
+ # @return [ Array<String> ] The Security Focus IDs
108
+ def securityfocus_ids
109
+ references[:securityfocus] || []
110
+ end
111
+
112
+ # @return [ Array<String> ]
113
+ def securityfocus_urls
114
+ securityfocus_ids.reduce([]) { |acc, elem| acc << securityfocus_url(elem) }
115
+ end
116
+
117
+ # @return [ String ]
118
+ def securityfocus_url(id)
119
+ "https://www.securityfocus.com/bid/#{id}/"
120
+ end
121
+
122
+ # @return [ Array<String> ]
123
+ def youtube_urls
124
+ references[:youtube] || []
125
+ end
126
+
127
+ # @return [ String ]
128
+ def youtube_url(id)
129
+ "https://www.youtube.com/watch?v=#{id}"
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ # Scan
5
+ class Scan
6
+ attr_reader :run_error
7
+
8
+ def initialize
9
+ NS.start_memory = GetProcessMem.new.bytes
10
+
11
+ controllers << NS::Controller::Core.new
12
+
13
+ exit_hook
14
+
15
+ yield self if block_given?
16
+ end
17
+
18
+ # @return [ Controllers ]
19
+ def controllers
20
+ @controllers ||= NS::Controllers.new
21
+ end
22
+
23
+ def run
24
+ controllers.run
25
+ rescue OptParseValidator::NoRequiredOption => e
26
+ @run_error = e
27
+
28
+ formatter.output('@usage', msg: e.message)
29
+ rescue NoMemoryError, ScriptError, SecurityError, SignalException, StandardError, SystemStackError => e
30
+ @run_error = e
31
+
32
+ output_params = {
33
+ reason: e.is_a?(Interrupt) ? 'Canceled by User' : e.message,
34
+ trace: e.backtrace,
35
+ verbose: NS::ParsedCli.verbose || run_error_exit_code == NS::ExitCode::EXCEPTION
36
+ }
37
+
38
+ output_params[:url] = controllers.first.target.url if NS::ParsedCli.url
39
+
40
+ formatter.output('@scan_aborted', output_params)
41
+ ensure
42
+ formatter.beautify
43
+ end
44
+
45
+ # Used for convenience
46
+ # @See Formatter
47
+ def formatter
48
+ controllers.first.formatter
49
+ end
50
+
51
+ # @return [ Hash ]
52
+ def datastore
53
+ controllers.first.datastore
54
+ end
55
+
56
+ # Hook to be able to have an exit code returned
57
+ # depending on the findings / errors
58
+ # :nocov:
59
+ def exit_hook
60
+ # Avoid hooking the exit when rspec is running, otherwise it will always return 0
61
+ # and Travis won't detect failed builds. Couldn't find a better way, even though
62
+ # some people managed to https://github.com/rspec/rspec-core/pull/410
63
+ return if defined?(RSpec)
64
+
65
+ at_exit do
66
+ exit(run_error_exit_code) if run_error
67
+
68
+ # The parsed_option[:url] must be checked to avoid raising erros when only -h/-v are given
69
+ exit(NS::ExitCode::VULNERABLE) if NS::ParsedCli.url && controllers.first.target.vulnerable?
70
+ exit(NS::ExitCode::OK)
71
+ end
72
+ end
73
+ # :nocov:
74
+
75
+ # @return [ Integer ] The exit code related to the run_error
76
+ def run_error_exit_code
77
+ return NS::ExitCode::CLI_OPTION_ERROR if run_error.is_a?(OptParseValidator::Error) ||
78
+ run_error.is_a?(OptionParser::ParseError)
79
+
80
+ return NS::ExitCode::INTERRUPTED if run_error.is_a?(Interrupt)
81
+
82
+ return NS::ExitCode::ERROR if run_error.is_a?(NS::Error::Standard) ||
83
+ run_error.is_a?(CMSScanner::Error::Standard)
84
+
85
+ NS::ExitCode::EXCEPTION
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ # Scope system logic
5
+ class Target < WebSite
6
+ # @note Comments are deleted to avoid cache generation details
7
+ #
8
+ # @param [ Typhoeus::Response, String ] page
9
+ #
10
+ # @return [ String ] The md5sum of the page
11
+ def self.page_hash(page)
12
+ page = NS::Browser.get(page, followlocation: true) unless page.is_a?(Typhoeus::Response)
13
+
14
+ # Removes comments and script tags before computing the hash
15
+ # to remove any potential cached stuff
16
+ html = Nokogiri::HTML(page.body)
17
+ html.xpath('//script|//comment()').each(&:remove)
18
+
19
+ Digest::MD5.hexdigest(html)
20
+ end
21
+
22
+ # @return [ String ] The hash of the homepage
23
+ def homepage_hash
24
+ @homepage_hash ||= self.class.page_hash(url)
25
+ end
26
+
27
+ # @note This is used to detect potential custom 404 responding with a 200
28
+ # @return [ String ] The hash of a 404
29
+ def error_404_hash
30
+ @error_404_hash ||= self.class.page_hash(error_404_res)
31
+ end
32
+
33
+ # @param [ Typhoeus::Response, String ] page
34
+ # @return [ Boolean ] Wether or not the page is a the homepage or a 404 based on its md5sum
35
+ def homepage_or_404?(page)
36
+ homepage_and_404_hashes.include?(self.class.page_hash(page))
37
+ end
38
+
39
+ protected
40
+
41
+ def homepage_and_404_hashes
42
+ @homepage_and_404_hashes ||= [homepage_hash, error_404_hash].freeze
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ class Target < WebSite
5
+ module Platform
6
+ # Some PHP specific implementation
7
+ module PHP
8
+ DEBUG_LOG_PATTERN = /(?:\[\d{2}-[a-zA-Z]{3}-\d{4}\s\d{2}:\d{2}:\d{2}\s[A-Z]{3}\]|
9
+ PHP\s(?:Fatal|Warning|Strict|Error|Notice):)/x.freeze
10
+ FPD_PATTERN = /Fatal error:.+? in (.+?) on/.freeze
11
+ ERROR_LOG_PATTERN = /PHP Fatal error/i.freeze
12
+
13
+ # @param [ String ] path
14
+ # @param [ Regexp ] pattern
15
+ # @param [ Hash ] params The request params
16
+ #
17
+ # @return [ Boolean ]
18
+ def log_file?(path, pattern, params = {})
19
+ # Only the first 700 bytes of the file are retrieved to avoid getting entire log file
20
+ # which can be huge (~ 2Go)
21
+ res = head_and_get(path, [200], get: params.merge(headers: { 'Range' => 'bytes=0-700' }))
22
+
23
+ res.body&.match?(pattern) ? true : false
24
+ end
25
+
26
+ # @param [ String ] path
27
+ # @param [ Hash ] params The request params
28
+ #
29
+ # @return [ Boolean ] true if url(path) is a debug log, false otherwise
30
+ def debug_log?(path, params = {})
31
+ log_file?(path, DEBUG_LOG_PATTERN, params)
32
+ end
33
+
34
+ # @param [ String ] path
35
+ # @param [ Hash ] params The request params
36
+ #
37
+ # @return [ Boolean ] Wether or not url(path) is an error log file
38
+ def error_log?(path, params = {})
39
+ log_file?(path, ERROR_LOG_PATTERN, params)
40
+ end
41
+
42
+ # @param [ String ] path
43
+ # @param [ Hash ] params The request params
44
+ #
45
+ # @return [ Boolean ] true if url(path) contains a FPD, false otherwise
46
+ def full_path_disclosure?(path = nil, params = {})
47
+ !full_path_disclosure_entries(path, params).empty?
48
+ end
49
+
50
+ # @param [ String ] path
51
+ # @param [ Hash ] params The request params
52
+ #
53
+ # @return [ Array<String> ] The FPD found, or an empty array if none
54
+ def full_path_disclosure_entries(path = nil, params = {})
55
+ res = NS::Browser.get(url(path), params)
56
+
57
+ res.body.scan(FPD_PATTERN).flatten
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cms_scanner/target/platform/php'