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,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
|