new_cms_scanner 0.13.7

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