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,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
# Scope system logic
|
5
|
+
class Target < WebSite
|
6
|
+
# @return [ Array<PublicSuffix::Domain, String> ]
|
7
|
+
def scope
|
8
|
+
@scope ||= Scope.new
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param [ String, Addressable::URI ] url An absolute URL or URI
|
12
|
+
#
|
13
|
+
# @return [ Boolean ] true if the url given is in scope
|
14
|
+
def in_scope?(url_or_uri)
|
15
|
+
url_or_uri = Addressable::URI.parse(url_or_uri.strip) unless url_or_uri.is_a?(Addressable::URI)
|
16
|
+
|
17
|
+
scope.include?(url_or_uri.host)
|
18
|
+
rescue StandardError
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [ Typhoeus::Response ] res
|
23
|
+
# @param [ String ] xpath
|
24
|
+
#
|
25
|
+
# @yield [ Addressable::URI, Nokogiri::XML::Element ] The in scope url and its associated tag
|
26
|
+
#
|
27
|
+
# @return [ Array<Addressable::URI> ] The in scope absolute URIs detected in the response's body
|
28
|
+
#
|
29
|
+
# @note It is highly recommended to use the xpath parameter to focus on the uris needed, as this method can be quite
|
30
|
+
# time consuming when there are a lof of uris to check
|
31
|
+
def in_scope_uris(res, xpath = '//@href|//@src|//@data-src')
|
32
|
+
found = []
|
33
|
+
|
34
|
+
uris_from_page(res, xpath) do |uri, tag|
|
35
|
+
next unless in_scope?(uri)
|
36
|
+
|
37
|
+
yield uri, tag if block_given?
|
38
|
+
|
39
|
+
found << uri
|
40
|
+
end
|
41
|
+
|
42
|
+
found
|
43
|
+
end
|
44
|
+
|
45
|
+
# Similar to Target#url_pattern but considering the in scope domains as well
|
46
|
+
#
|
47
|
+
# @return [ Regexp ] The pattern related to the target url and in scope domains,
|
48
|
+
# it also matches escaped /, such as in JSON JS data: http:\/\/t.com\/
|
49
|
+
# rubocop:disable Metrics/AbcSize
|
50
|
+
def scope_url_pattern
|
51
|
+
return @scope_url_pattern if @scope_url_pattern
|
52
|
+
|
53
|
+
domains = [uri.host + uri.path]
|
54
|
+
|
55
|
+
domains += if scope.domains.empty?
|
56
|
+
Array(scope.invalid_domains[1..-1])
|
57
|
+
else
|
58
|
+
Array(scope.domains[1..-1]).map(&:to_s) + scope.invalid_domains
|
59
|
+
end
|
60
|
+
|
61
|
+
domains.map! { |d| Regexp.escape(d.delete_suffix('/')).gsub('\*', '.*').gsub('/', '\\\\\?/') }
|
62
|
+
|
63
|
+
domains[0].gsub!(Regexp.escape(uri.host), "#{Regexp.escape(uri.host)}(?::\\d+)?") if uri.port
|
64
|
+
|
65
|
+
@scope_url_pattern = %r{https?:\\?/\\?/(?:#{domains.join('|')})\\?/?}i
|
66
|
+
end
|
67
|
+
# rubocop:enable Metrics/AbcSize
|
68
|
+
|
69
|
+
# Scope Implementation
|
70
|
+
class Scope
|
71
|
+
# @return [ Array<PublicSuffix::Domain> ] The valid domains in scope
|
72
|
+
def domains
|
73
|
+
@domains ||= []
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [ Array<String> ] The invalid domains in scope (such as IP addresses etc)
|
77
|
+
def invalid_domains
|
78
|
+
@invalid_domains ||= []
|
79
|
+
end
|
80
|
+
|
81
|
+
def <<(element)
|
82
|
+
if PublicSuffix.valid?(element, ignore_private: true)
|
83
|
+
domains << PublicSuffix.parse(element, ignore_private: true)
|
84
|
+
else
|
85
|
+
invalid_domains << element
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [ Boolean ] Wether or not the host is in the scope
|
90
|
+
def include?(host)
|
91
|
+
if PublicSuffix.valid?(host, ignore_private: true)
|
92
|
+
domain = PublicSuffix.parse(host, ignore_private: true)
|
93
|
+
|
94
|
+
domains.each { |d| return true if domain.match(d) }
|
95
|
+
else
|
96
|
+
invalid_domains.each { |d| return true if host == d }
|
97
|
+
end
|
98
|
+
|
99
|
+
false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
class Target < WebSite
|
5
|
+
module Server
|
6
|
+
# Some Apche specific implementation
|
7
|
+
module Apache
|
8
|
+
# @param [ String ] path
|
9
|
+
# @param [ Hash ] params The request params
|
10
|
+
#
|
11
|
+
# @return [ Symbol ] :Apache
|
12
|
+
def server(_path = nil, _params = {})
|
13
|
+
:Apache
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [ String ] path
|
17
|
+
# @param [ Hash ] params The request params
|
18
|
+
#
|
19
|
+
# @return [ Array<String> ] The first level of directories/files listed,
|
20
|
+
# or an empty array if none
|
21
|
+
def directory_listing_entries(path = nil, params = {})
|
22
|
+
super(path, params, 'td a')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
class Target < WebSite
|
5
|
+
module Server
|
6
|
+
# Generic Server methods
|
7
|
+
module Generic
|
8
|
+
# @param [ String ] path
|
9
|
+
# @param [ Hash ] params The request params
|
10
|
+
#
|
11
|
+
# @return [ Symbol ] The detected remote server (:Apache, :IIS, :Nginx)
|
12
|
+
def server(path = nil, params = {})
|
13
|
+
headers = headers(path, params)
|
14
|
+
|
15
|
+
return unless headers
|
16
|
+
|
17
|
+
case headers[:server]
|
18
|
+
when /\Aapache/i
|
19
|
+
:Apache
|
20
|
+
when /\AMicrosoft-IIS/i
|
21
|
+
:IIS
|
22
|
+
when /\Anginx/
|
23
|
+
:Nginx
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [ String ] path
|
28
|
+
# @param [ Hash ] params The request params
|
29
|
+
#
|
30
|
+
# @return [ Hash ] The headers
|
31
|
+
def headers(path = nil, params = {})
|
32
|
+
# The HEAD method might be rejected by some servers ... maybe switch to GET ?
|
33
|
+
NS::Browser.head(url(path), params).headers
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [ String ] path
|
37
|
+
# @param [ Hash ] params The request params
|
38
|
+
#
|
39
|
+
# @return [ Boolean ] true if url(path) has the directory
|
40
|
+
# listing enabled, false otherwise
|
41
|
+
def directory_listing?(path = nil, params = {})
|
42
|
+
res = NS::Browser.get(url(path), params)
|
43
|
+
|
44
|
+
res.code == 200 && res.body.include?('<h1>Index of')
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param [ String ] path
|
48
|
+
# @param [ Hash ] params The request params
|
49
|
+
# @param [ String ] selector
|
50
|
+
# @param [ Regexp ] ignore
|
51
|
+
#
|
52
|
+
# @return [ Array<String> ] The first level of directories/files listed,
|
53
|
+
# or an empty array if none
|
54
|
+
def directory_listing_entries(path = nil, params = {}, selector = 'pre a', ignore = /parent directory/i)
|
55
|
+
return [] unless directory_listing?(path, params)
|
56
|
+
|
57
|
+
found = []
|
58
|
+
|
59
|
+
NS::Browser.get(url(path), params).html.css(selector).each do |node|
|
60
|
+
entry = node.text.to_s
|
61
|
+
|
62
|
+
next if entry&.match?(ignore)
|
63
|
+
|
64
|
+
found << entry
|
65
|
+
end
|
66
|
+
|
67
|
+
found
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
class Target < WebSite
|
5
|
+
module Server
|
6
|
+
# Some IIS specific implementation
|
7
|
+
module IIS
|
8
|
+
# @param [ String ] path
|
9
|
+
# @param [ Hash ] params The request params
|
10
|
+
#
|
11
|
+
# @return [ Symbol ] :IIS
|
12
|
+
def server(_path = nil, _params = {})
|
13
|
+
:IIS
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [ String ] path
|
17
|
+
# @param [ Hash ] params The request params
|
18
|
+
#
|
19
|
+
# @return [ Boolean ] true if url(path) has the directory
|
20
|
+
# listing enabled, false otherwise
|
21
|
+
def directory_listing?(path = nil, params = {})
|
22
|
+
res = NS::Browser.get(url(path), params)
|
23
|
+
|
24
|
+
res.code == 200 && res.body =~ %r{<H1>#{uri.host} - /} ? true : false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
class Target < WebSite
|
5
|
+
module Server
|
6
|
+
# Some Nginx specific implementation
|
7
|
+
module Nginx
|
8
|
+
# @param [ String ] path
|
9
|
+
# @param [ Hash ] params The request params
|
10
|
+
#
|
11
|
+
# @return [ Symbol ] :Nginx
|
12
|
+
def server(_path = nil, _params = {})
|
13
|
+
:Nginx
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [ String ] path
|
17
|
+
# @param [ Hash ] params The request params
|
18
|
+
#
|
19
|
+
# @return [ Array<String> ] The first level of directories/files listed,
|
20
|
+
# or an empty array if none
|
21
|
+
def directory_listing_entries(path = nil, params = {})
|
22
|
+
super(path, params, 'pre a', /\A\.\./i)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cms_scanner/web_site'
|
4
|
+
require 'cms_scanner/target/platform'
|
5
|
+
require 'cms_scanner/target/server'
|
6
|
+
require 'cms_scanner/target/scope'
|
7
|
+
require 'cms_scanner/target/hashes'
|
8
|
+
|
9
|
+
module CMSScanner
|
10
|
+
# Target to Scan
|
11
|
+
class Target < WebSite
|
12
|
+
include Server::Generic
|
13
|
+
|
14
|
+
# @param [ String ] url
|
15
|
+
# @param [ Hash ] opts
|
16
|
+
# @option opts [ Array<PublicSuffix::Domain, String> ] :scope
|
17
|
+
def initialize(url, opts = {})
|
18
|
+
super(url, opts)
|
19
|
+
|
20
|
+
scope << uri.host
|
21
|
+
Array(opts[:scope]).each { |s| scope << s }
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [ Hash ] opts
|
25
|
+
#
|
26
|
+
# @return [ Findings ]
|
27
|
+
def interesting_findings(opts = {})
|
28
|
+
@interesting_findings ||= NS::Finders::InterestingFindings::Base.find(self, opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Weteher or not vulnerabilities have been found.
|
32
|
+
# Used to set the exit code of the scanner
|
33
|
+
# and it should be overriden in the implementation
|
34
|
+
#
|
35
|
+
# @return [ Boolean ]
|
36
|
+
def vulnerable?
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [ Regexp ] The pattern related to the target url, also matches escaped /, such as
|
41
|
+
# in JSON JS data: http:\/\/t.com\/
|
42
|
+
def url_pattern
|
43
|
+
@url_pattern ||= Regexp.new(Regexp.escape(url).gsub(/https?/i, 'https?').gsub('/', '\\\\\?/'), Regexp::IGNORECASE)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param [ String ] xpath
|
47
|
+
# @param [ Regexp ] pattern
|
48
|
+
# @param [ Typhoeus::Response, String ] page
|
49
|
+
#
|
50
|
+
# @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ]
|
51
|
+
# @yield [ MatchData, Nokogiri::XML::Element ]
|
52
|
+
def xpath_pattern_from_page(xpath, pattern, page = nil)
|
53
|
+
page = NS::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response)
|
54
|
+
matches = []
|
55
|
+
|
56
|
+
page.html.xpath(xpath).each do |node|
|
57
|
+
next unless node.text.strip =~ pattern
|
58
|
+
|
59
|
+
yield Regexp.last_match, node if block_given?
|
60
|
+
|
61
|
+
matches << [Regexp.last_match, node]
|
62
|
+
end
|
63
|
+
|
64
|
+
matches
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param [ Regexp ] pattern
|
68
|
+
# @param [ Typhoeus::Response, String ] page
|
69
|
+
#
|
70
|
+
# @return [ Array<Array<MatchData, Nokogiri::XML::Comment>> ]
|
71
|
+
# @yield [ MatchData, Nokogiri::XML::Comment ]
|
72
|
+
def comments_from_page(pattern, page = nil)
|
73
|
+
xpath_pattern_from_page('//comment()', pattern, page) do |match, node|
|
74
|
+
yield match, node if block_given?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param [ Regexp ] pattern
|
79
|
+
# @param [ Typhoeus::Response, String ] page
|
80
|
+
#
|
81
|
+
# @return [ Array<Array<MatchData, Nokogiri::XML::Element>> ]
|
82
|
+
# @yield [ MatchData, Nokogiri::XML::Element ]
|
83
|
+
def javascripts_from_page(pattern, page = nil)
|
84
|
+
xpath_pattern_from_page('//script', pattern, page) do |match, node|
|
85
|
+
yield match, node if block_given?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param [ Typhoeus::Response, String ] page
|
90
|
+
# @param [ String ] xpath
|
91
|
+
#
|
92
|
+
# @yield [ Addressable::URI, Nokogiri::XML::Element ] The url and its associated tag
|
93
|
+
#
|
94
|
+
# @return [ Array<Addressable::URI> ] The absolute URIs detected in the response's body from the HTML tags
|
95
|
+
#
|
96
|
+
# @note It is highly recommended to use the xpath parameter to focus on the uris needed, as this method can be quite
|
97
|
+
# time consuming when there are a lof of uris to check
|
98
|
+
def uris_from_page(page = nil, xpath = '//@href|//@src|//@data-src')
|
99
|
+
page = NS::Browser.get(url(page)) unless page.is_a?(Typhoeus::Response)
|
100
|
+
found = []
|
101
|
+
|
102
|
+
page.html.xpath(xpath).each do |node|
|
103
|
+
attr_value = node.text.to_s
|
104
|
+
|
105
|
+
next unless attr_value && !attr_value.empty?
|
106
|
+
|
107
|
+
node_uri = begin
|
108
|
+
uri.join(attr_value.strip)
|
109
|
+
rescue StandardError
|
110
|
+
# Skip potential malformed URLs etc.
|
111
|
+
next
|
112
|
+
end
|
113
|
+
|
114
|
+
next unless node_uri.host
|
115
|
+
|
116
|
+
yield node_uri, node.parent if block_given? && !found.include?(node_uri)
|
117
|
+
|
118
|
+
found << node_uri
|
119
|
+
end
|
120
|
+
|
121
|
+
found.uniq
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typhoeus
|
4
|
+
# Custom Response class
|
5
|
+
class Response
|
6
|
+
# @return [ Nokogiri::XML ] The response's body parsed by Nokogiri::HTML
|
7
|
+
def html
|
8
|
+
@html ||= Nokogiri::HTML(body.encode('UTF-8', invalid: :replace, undef: :replace))
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [ Nokogiri::XML ] The response's body parsed by Nokogiri::XML
|
12
|
+
def xml
|
13
|
+
@xml ||= Nokogiri::XML(body.encode('UTF-8', invalid: :replace, undef: :replace))
|
14
|
+
end
|
15
|
+
|
16
|
+
# Override of the original to ensure an integer is returned
|
17
|
+
# @return [ Integer ]
|
18
|
+
def request_size
|
19
|
+
super || 0
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [ Integer ]
|
23
|
+
def size
|
24
|
+
(body.nil? ? 0 : body.size) + (response_headers.nil? ? 0 : response_headers.size)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
# Generic Vulnerability
|
5
|
+
class Vulnerability
|
6
|
+
include References
|
7
|
+
|
8
|
+
attr_reader :title, :type, :fixed_in, :introduced_in, :cvss
|
9
|
+
|
10
|
+
# @param [ String ] title
|
11
|
+
# @param [ Hash ] references
|
12
|
+
# @option references [ Array<String>, String ] :cve
|
13
|
+
# @option references [ Array<String>, String ] :secunia
|
14
|
+
# @option references [ Array<String>, String ] :osvdb
|
15
|
+
# @option references [ Array<String>, String ] :exploitdb
|
16
|
+
# @option references [ Array<String> ] :url URL(s) to related advisories etc
|
17
|
+
# @option references [ Array<String>, String ] :metasploit The related metasploit module(s)
|
18
|
+
# @option references [ Array<String> ] :youtube
|
19
|
+
# @param [ String ] type
|
20
|
+
# @param [ String ] fixed_in
|
21
|
+
# @param [ String ] introduced_in
|
22
|
+
# @param [ HashSymbol ] cvss
|
23
|
+
# @option cvss [ String ] :score
|
24
|
+
# @option cvss [ String ] :vector
|
25
|
+
def initialize(title, references: {}, type: nil, fixed_in: nil, introduced_in: nil, cvss: nil)
|
26
|
+
@title = title
|
27
|
+
@type = type
|
28
|
+
@fixed_in = fixed_in
|
29
|
+
@introduced_in = introduced_in
|
30
|
+
@cvss = { score: cvss[:score], vector: cvss[:vector] } if cvss
|
31
|
+
|
32
|
+
self.references = references
|
33
|
+
end
|
34
|
+
|
35
|
+
# param [ Vulnerability ] other
|
36
|
+
#
|
37
|
+
# @return [ Boolean ]
|
38
|
+
def ==(other)
|
39
|
+
title == other.title &&
|
40
|
+
type == other.type &&
|
41
|
+
references == other.references &&
|
42
|
+
fixed_in == other.fixed_in &&
|
43
|
+
cvss == other.cvss
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
# WebSite Implementation
|
5
|
+
class WebSite
|
6
|
+
attr_reader :uri, :opts
|
7
|
+
|
8
|
+
# @param [ String ] site_url
|
9
|
+
# @param [ Hash ] opts
|
10
|
+
def initialize(site_url, opts = {})
|
11
|
+
self.url = site_url
|
12
|
+
@opts = opts
|
13
|
+
end
|
14
|
+
|
15
|
+
def url=(site_url)
|
16
|
+
new_url = site_url.dup
|
17
|
+
|
18
|
+
# Add a trailing slash to the URL
|
19
|
+
new_url << '/' if new_url[-1, 1] != '/'
|
20
|
+
|
21
|
+
# Use the validator to ensure the URL has a correct format
|
22
|
+
OptParseValidator::OptURL.new([]).validate(new_url)
|
23
|
+
|
24
|
+
@uri = Addressable::URI.parse(new_url).normalize
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [ String ] path Optional path to merge with the uri
|
28
|
+
#
|
29
|
+
# @return [ String ]
|
30
|
+
def url(path = nil)
|
31
|
+
return @uri.to_s unless path
|
32
|
+
|
33
|
+
@uri.join(Addressable::URI.encode(path).gsub('#', '%23')).to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [ String ] The IP address of the target
|
37
|
+
def ip
|
38
|
+
@ip ||= IPSocket.getaddress(uri.host)
|
39
|
+
rescue SocketError
|
40
|
+
'Unknown'
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_writer :homepage_res
|
44
|
+
|
45
|
+
# @return [ Typhoeus::Response ]
|
46
|
+
#
|
47
|
+
# As webmock does not support redirects mocking, coverage is ignored
|
48
|
+
# :nocov:
|
49
|
+
def homepage_res
|
50
|
+
@homepage_res ||= NS::Browser.get_and_follow_location(url)
|
51
|
+
end
|
52
|
+
# :nocov:
|
53
|
+
|
54
|
+
# @return [ String ]
|
55
|
+
def homepage_url
|
56
|
+
@homepage_url ||= homepage_res.effective_url
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [ Typhoeus::Response ]
|
60
|
+
def error_404_res
|
61
|
+
@error_404_res ||= NS::Browser.get_and_follow_location(error_404_url)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [ String ] The URL of an unlikely existant page
|
65
|
+
def error_404_url
|
66
|
+
@error_404_url ||= uri.join("#{Digest::MD5.hexdigest(rand(999_999).to_s)[0..6]}.html").to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
# Checks if the remote website is up.
|
70
|
+
#
|
71
|
+
# @param [ String ] path
|
72
|
+
#
|
73
|
+
# @return [ Boolean ]
|
74
|
+
def online?(path = nil)
|
75
|
+
NS::Browser.get(url(path)).code.nonzero? ? true : false
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param [ String ] path
|
79
|
+
#
|
80
|
+
# @return [ Boolean ]
|
81
|
+
def http_auth?(path = nil)
|
82
|
+
NS::Browser.get(url(path)).code == 401
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [ String ] path
|
86
|
+
#
|
87
|
+
# @return [ Boolean ]
|
88
|
+
def access_forbidden?(path = nil)
|
89
|
+
NS::Browser.get(url(path)).code == 403
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param [ String ] path
|
93
|
+
#
|
94
|
+
# @return [ Boolean ]
|
95
|
+
def proxy_auth?(path = nil)
|
96
|
+
NS::Browser.get(url(path)).code == 407
|
97
|
+
end
|
98
|
+
|
99
|
+
# @param [ String ] url
|
100
|
+
#
|
101
|
+
# @return [ String ] The redirection url or nil
|
102
|
+
#
|
103
|
+
# As webmock does not support redirects mocking, coverage is ignored
|
104
|
+
# :nocov:
|
105
|
+
def redirection(url = nil)
|
106
|
+
url ||= @uri.to_s
|
107
|
+
|
108
|
+
return unless [301, 302].include?(NS::Browser.get(url).code)
|
109
|
+
|
110
|
+
res = NS::Browser.get(url, followlocation: true, maxredirs: 10)
|
111
|
+
|
112
|
+
res.effective_url == url ? nil : res.effective_url
|
113
|
+
end
|
114
|
+
# :nocov:
|
115
|
+
|
116
|
+
# @return [ Hash ] The Typhoeus params to use to perform head requests
|
117
|
+
def head_or_get_params
|
118
|
+
@head_or_get_params ||= if NS::Browser.head(homepage_url).code == 405
|
119
|
+
{ method: :get, maxfilesize: 1 }
|
120
|
+
else
|
121
|
+
{ method: :head }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Perform a HEAD request to the path provided, then if its response code
|
126
|
+
# is in the array of codes given, a GET is done and the response returned. Otherwise the
|
127
|
+
# HEAD response is returned.
|
128
|
+
#
|
129
|
+
# @param [ String ] path
|
130
|
+
# @param [ Array<String> ] codes
|
131
|
+
# @param [ Hash ] params The requests params
|
132
|
+
# @option params [ Hash ] :head Request params for the HEAD
|
133
|
+
# @option params [ hash ] :get Request params for the GET
|
134
|
+
#
|
135
|
+
# @return [ Typhoeus::Response ]
|
136
|
+
def head_and_get(path, codes = [200], params = {})
|
137
|
+
url_to_get = url(path)
|
138
|
+
head_params = (params[:head] || {}).merge(head_or_get_params)
|
139
|
+
|
140
|
+
head_res = NS::Browser.forge_request(url_to_get, head_params).run
|
141
|
+
|
142
|
+
codes.include?(head_res.code) ? NS::Browser.get(url_to_get, params[:get] || {}) : head_res
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|