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