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.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +26 -0
- data/app/app.rb +24 -0
- data/app/controllers/core/cli_options.rb +117 -0
- data/app/controllers/core.rb +82 -0
- data/app/controllers/interesting_findings.rb +25 -0
- data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
- data/app/finders/interesting_findings/headers.rb +17 -0
- data/app/finders/interesting_findings/robots_txt.rb +20 -0
- data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
- data/app/finders/interesting_findings/xml_rpc.rb +61 -0
- data/app/finders/interesting_findings.rb +25 -0
- data/app/formatters/cli.rb +65 -0
- data/app/formatters/cli_no_color.rb +9 -0
- data/app/formatters/cli_no_colour.rb +17 -0
- data/app/formatters/json.rb +14 -0
- data/app/models/fantastico_fileslist.rb +34 -0
- data/app/models/headers.rb +44 -0
- data/app/models/interesting_finding.rb +48 -0
- data/app/models/robots_txt.rb +31 -0
- data/app/models/search_replace_db_2.rb +17 -0
- data/app/models/user.rb +35 -0
- data/app/models/version.rb +49 -0
- data/app/models/xml_rpc.rb +78 -0
- data/app/user_agents.txt +46 -0
- data/app/views/cli/core/banner.erb +1 -0
- data/app/views/cli/core/finished.erb +8 -0
- data/app/views/cli/core/help.erb +4 -0
- data/app/views/cli/core/started.erb +6 -0
- data/app/views/cli/core/version.erb +1 -0
- data/app/views/cli/interesting_findings/_array.erb +10 -0
- data/app/views/cli/interesting_findings/findings.erb +23 -0
- data/app/views/cli/scan_aborted.erb +5 -0
- data/app/views/cli/usage.erb +3 -0
- data/app/views/json/core/banner.erb +1 -0
- data/app/views/json/core/finished.erb +10 -0
- data/app/views/json/core/help.erb +4 -0
- data/app/views/json/core/started.erb +5 -0
- data/app/views/json/core/version.erb +1 -0
- data/app/views/json/interesting_findings/findings.erb +24 -0
- data/app/views/json/scan_aborted.erb +5 -0
- data/lib/cms_scanner/browser/actions.rb +48 -0
- data/lib/cms_scanner/browser/options.rb +90 -0
- data/lib/cms_scanner/browser.rb +96 -0
- data/lib/cms_scanner/cache/file_store.rb +77 -0
- data/lib/cms_scanner/cache/typhoeus.rb +25 -0
- data/lib/cms_scanner/controller.rb +105 -0
- data/lib/cms_scanner/controllers.rb +67 -0
- data/lib/cms_scanner/errors/http.rb +72 -0
- data/lib/cms_scanner/errors/scan.rb +14 -0
- data/lib/cms_scanner/errors.rb +11 -0
- data/lib/cms_scanner/exit_code.rb +25 -0
- data/lib/cms_scanner/finders/base_finders.rb +45 -0
- data/lib/cms_scanner/finders/finder/breadth_first_dictionary_attack.rb +121 -0
- data/lib/cms_scanner/finders/finder/enumerator.rb +77 -0
- data/lib/cms_scanner/finders/finder/fingerprinter.rb +48 -0
- data/lib/cms_scanner/finders/finder/smart_url_checker/findings.rb +33 -0
- data/lib/cms_scanner/finders/finder/smart_url_checker.rb +60 -0
- data/lib/cms_scanner/finders/finder.rb +75 -0
- data/lib/cms_scanner/finders/finding.rb +54 -0
- data/lib/cms_scanner/finders/findings.rb +26 -0
- data/lib/cms_scanner/finders/independent_finder.rb +30 -0
- data/lib/cms_scanner/finders/independent_finders.rb +26 -0
- data/lib/cms_scanner/finders/same_type_finder.rb +19 -0
- data/lib/cms_scanner/finders/same_type_finders.rb +26 -0
- data/lib/cms_scanner/finders/unique_finder.rb +19 -0
- data/lib/cms_scanner/finders/unique_finders.rb +47 -0
- data/lib/cms_scanner/finders.rb +12 -0
- data/lib/cms_scanner/formatter/buffer.rb +17 -0
- data/lib/cms_scanner/formatter.rb +149 -0
- data/lib/cms_scanner/helper.rb +7 -0
- data/lib/cms_scanner/numeric.rb +13 -0
- data/lib/cms_scanner/parsed_cli.rb +37 -0
- data/lib/cms_scanner/progressbar_null_output.rb +23 -0
- data/lib/cms_scanner/public_suffix/domain.rb +42 -0
- data/lib/cms_scanner/references.rb +132 -0
- data/lib/cms_scanner/scan.rb +88 -0
- data/lib/cms_scanner/target/hashes.rb +45 -0
- data/lib/cms_scanner/target/platform/php.rb +62 -0
- data/lib/cms_scanner/target/platform.rb +3 -0
- data/lib/cms_scanner/target/scope.rb +103 -0
- data/lib/cms_scanner/target/server/apache.rb +27 -0
- data/lib/cms_scanner/target/server/generic.rb +72 -0
- data/lib/cms_scanner/target/server/iis.rb +29 -0
- data/lib/cms_scanner/target/server/nginx.rb +27 -0
- data/lib/cms_scanner/target/server.rb +6 -0
- data/lib/cms_scanner/target.rb +124 -0
- data/lib/cms_scanner/typhoeus/hydra.rb +12 -0
- data/lib/cms_scanner/typhoeus/response.rb +27 -0
- data/lib/cms_scanner/version.rb +6 -0
- data/lib/cms_scanner/vulnerability.rb +46 -0
- data/lib/cms_scanner/web_site.rb +145 -0
- data/lib/cms_scanner.rb +141 -0
- 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,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
|