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,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cms_scanner/browser/actions'
|
4
|
+
require 'cms_scanner/browser/options'
|
5
|
+
|
6
|
+
module CMSScanner
|
7
|
+
# Singleton used to perform HTTP/HTTPS request to the target
|
8
|
+
class Browser
|
9
|
+
extend Actions
|
10
|
+
|
11
|
+
# @param [ Hash ] parsed_options
|
12
|
+
#
|
13
|
+
# @return [ Void ]
|
14
|
+
def initialize(parsed_options = {})
|
15
|
+
self.throttle = 0
|
16
|
+
|
17
|
+
load_options(parsed_options.dup)
|
18
|
+
end
|
19
|
+
|
20
|
+
private_class_method :new
|
21
|
+
|
22
|
+
# @param [ Hash ] parsed_options
|
23
|
+
#
|
24
|
+
# @return [ Browser ] The instance
|
25
|
+
def self.instance(parsed_options = {})
|
26
|
+
@@instance ||= new(parsed_options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.reset
|
30
|
+
@@instance = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param [ String ] url
|
34
|
+
# @param [ Hash ] params
|
35
|
+
#
|
36
|
+
# @return [ Typhoeus::Request ]
|
37
|
+
def forge_request(url, params = {})
|
38
|
+
Typhoeus::Request.new(url, request_params(params))
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [ Hash ] The request params used to connect tothe target as well as potential other systems such as API
|
42
|
+
def default_connect_request_params
|
43
|
+
params = {}
|
44
|
+
|
45
|
+
if disable_tls_checks
|
46
|
+
# See http://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html
|
47
|
+
params[:ssl_verifypeer] = false
|
48
|
+
params[:ssl_verifyhost] = 0
|
49
|
+
# TLSv1.0 and plus, allows to use a protocol potentially lower than the OS default
|
50
|
+
params[:sslversion] = :tlsv1
|
51
|
+
end
|
52
|
+
|
53
|
+
{
|
54
|
+
connecttimeout: :connect_timeout, cache_ttl: :cache_ttl,
|
55
|
+
proxy: :proxy, timeout: :request_timeout
|
56
|
+
}.each do |typhoeus_opt, browser_opt|
|
57
|
+
attr_value = public_send(browser_opt)
|
58
|
+
params[typhoeus_opt] = attr_value unless attr_value.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
params
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [ Hash ]
|
65
|
+
# The params are not cached (using @params ||= for example), so that they are set accordingly if updated
|
66
|
+
# by a controller/other piece of code
|
67
|
+
def default_request_params
|
68
|
+
params = default_connect_request_params.merge(
|
69
|
+
headers: { 'User-Agent' => user_agent, 'Referer' => url }.merge(headers || {}),
|
70
|
+
accept_encoding: 'gzip, deflate',
|
71
|
+
method: :get
|
72
|
+
)
|
73
|
+
|
74
|
+
{ cookiejar: :cookie_jar, cookiefile: :cookie_jar, cookie: :cookie_string }.each do |typhoeus_opt, browser_opt|
|
75
|
+
attr_value = public_send(browser_opt)
|
76
|
+
params[typhoeus_opt] = attr_value unless attr_value.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
params[:proxyuserpwd] = "#{proxy_auth[:username]}:#{proxy_auth[:password]}" if proxy_auth
|
80
|
+
params[:userpwd] = "#{http_auth[:username]}:#{http_auth[:password]}" if http_auth
|
81
|
+
|
82
|
+
params[:headers]['Host'] = vhost if vhost
|
83
|
+
|
84
|
+
params
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param [ Hash ] params
|
88
|
+
#
|
89
|
+
# @return [ Hash ]
|
90
|
+
def request_params(params = {})
|
91
|
+
default_request_params.merge(params) do |key, oldval, newval|
|
92
|
+
key == :headers ? oldval.merge(newval) : newval
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
module Cache
|
5
|
+
# Cache Implementation using files
|
6
|
+
class FileStore
|
7
|
+
attr_reader :storage_path, :serializer
|
8
|
+
|
9
|
+
# The serializer must have the 2 methods #load and #dump
|
10
|
+
# (Marshal and YAML have them)
|
11
|
+
# YAML is Human Readable, contrary to Marshal which store in a binary format
|
12
|
+
# Marshal does not need any "require"
|
13
|
+
#
|
14
|
+
# @param [ String ] storage_path
|
15
|
+
# @param [ Constant ] serializer
|
16
|
+
def initialize(storage_path, serializer = Marshal)
|
17
|
+
@storage_path = File.expand_path(storage_path)
|
18
|
+
@serializer = serializer
|
19
|
+
|
20
|
+
FileUtils.mkdir_p(@storage_path) unless Dir.exist?(@storage_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO: rename this to clear ?
|
24
|
+
def clean
|
25
|
+
Dir[File.join(storage_path, '*')].each do |f|
|
26
|
+
File.delete(f) unless File.symlink?(f)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param [ String ] key
|
31
|
+
#
|
32
|
+
# @return [ Mixed ]
|
33
|
+
def read_entry(key)
|
34
|
+
return if expired_entry?(key)
|
35
|
+
|
36
|
+
serializer.load(File.read(entry_path(key)))
|
37
|
+
rescue StandardError
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param [ String ] key
|
42
|
+
# @param [ Mixed ] data_to_store
|
43
|
+
# @param [ Integer ] cache_ttl
|
44
|
+
def write_entry(key, data_to_store, cache_ttl)
|
45
|
+
return unless cache_ttl.to_i.positive?
|
46
|
+
|
47
|
+
File.write(entry_path(key), serializer.dump(data_to_store))
|
48
|
+
File.write(entry_expiration_path(key), Time.now.to_i + cache_ttl)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param [ String ] key
|
52
|
+
#
|
53
|
+
# @return [ String ] The file path associated to the key
|
54
|
+
def entry_path(key)
|
55
|
+
File.join(storage_path, key)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [ String ] key
|
59
|
+
#
|
60
|
+
# @return [ String ] The expiration file path associated to the key
|
61
|
+
def entry_expiration_path(key)
|
62
|
+
"#{entry_path(key)}.expiration"
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# @param [ String ] key
|
68
|
+
#
|
69
|
+
# @return [ Boolean ]
|
70
|
+
def expired_entry?(key)
|
71
|
+
File.read(entry_expiration_path(key)).to_i <= Time.now.to_i
|
72
|
+
rescue StandardError
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cms_scanner/cache/file_store'
|
4
|
+
|
5
|
+
module CMSScanner
|
6
|
+
module Cache
|
7
|
+
# Cache implementation for Typhoeus
|
8
|
+
class Typhoeus < FileStore
|
9
|
+
# @param [ Typhoeus::Request ] request
|
10
|
+
#
|
11
|
+
# @return [ Typhoeus::Response ]
|
12
|
+
def get(request)
|
13
|
+
read_entry(request.hash.to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [ Typhoeus::Request ] request
|
17
|
+
# @param [ Typhoeus::Response ] response
|
18
|
+
def set(request, response)
|
19
|
+
return if response.timed_out? || response.code&.zero?
|
20
|
+
|
21
|
+
write_entry(request.hash.to_s, response, request.cache_ttl)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
module Controller
|
5
|
+
# Base Controller
|
6
|
+
class Base
|
7
|
+
include OptParseValidator
|
8
|
+
|
9
|
+
# @return [ Array<OptParseValidator::OptBase> ]
|
10
|
+
def cli_options; end
|
11
|
+
|
12
|
+
def before_scan; end
|
13
|
+
|
14
|
+
def run; end
|
15
|
+
|
16
|
+
def after_scan; end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
self.class == other.class
|
20
|
+
end
|
21
|
+
|
22
|
+
# Reset all the class attibutes
|
23
|
+
# Currently only used in specs
|
24
|
+
def self.reset
|
25
|
+
@@target = nil
|
26
|
+
@@datastore = nil
|
27
|
+
@@formatter = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [ Target ]
|
31
|
+
def target
|
32
|
+
@@target ||= NS::Target.new(NS::ParsedCli.url, NS::ParsedCli.options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [ OptParsevalidator::OptParser ] parser
|
36
|
+
def self.option_parser=(parser)
|
37
|
+
@@option_parser = parser
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [ OptParsevalidator::OptParser ]
|
41
|
+
def option_parser
|
42
|
+
@@option_parser
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [ Hash ]
|
46
|
+
def datastore
|
47
|
+
@@datastore ||= {}
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [ Formatter::Base ]
|
51
|
+
def formatter
|
52
|
+
@@formatter ||= NS::Formatter.load(NS::ParsedCli.format, datastore[:views])
|
53
|
+
end
|
54
|
+
|
55
|
+
# @see Formatter#output
|
56
|
+
#
|
57
|
+
# @return [ Void ]
|
58
|
+
def output(tpl, vars = {})
|
59
|
+
formatter.output(*tpl_params(tpl, vars))
|
60
|
+
end
|
61
|
+
|
62
|
+
# @see Formatter#render
|
63
|
+
#
|
64
|
+
# @return [ String ]
|
65
|
+
def render(tpl, vars = {})
|
66
|
+
formatter.render(*tpl_params(tpl, vars))
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [ Boolean ]
|
70
|
+
def user_interaction?
|
71
|
+
formatter.user_interaction? && !NS::ParsedCli.output
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [ String ]
|
75
|
+
def tmp_directory
|
76
|
+
File.join('/tmp', NS.app_name)
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
# @param [ String ] tpl
|
82
|
+
# @param [ Hash ] vars
|
83
|
+
#
|
84
|
+
# @return [ Array<String> ]
|
85
|
+
def tpl_params(tpl, vars)
|
86
|
+
[
|
87
|
+
tpl,
|
88
|
+
instance_variable_values.merge(vars),
|
89
|
+
self.class.name.demodulize.underscore
|
90
|
+
]
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [ Hash ] All the instance variable keys (and their values) and the verbose value
|
94
|
+
def instance_variable_values
|
95
|
+
h = { verbose: NS::ParsedCli.verbose }
|
96
|
+
instance_variables.each do |a|
|
97
|
+
s = a.to_s
|
98
|
+
n = s[1..s.size]
|
99
|
+
h[n.to_sym] = instance_variable_get(a)
|
100
|
+
end
|
101
|
+
h
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
# Controllers Container
|
5
|
+
class Controllers < Array
|
6
|
+
attr_reader :option_parser, :running
|
7
|
+
|
8
|
+
# @param [ OptParsevalidator::OptParser ] options_parser
|
9
|
+
def initialize(option_parser = OptParseValidator::OptParser.new(nil, 40))
|
10
|
+
@option_parser = option_parser
|
11
|
+
|
12
|
+
register_config_files
|
13
|
+
|
14
|
+
option_parser.config_files.result_key = 'cli_options'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adds the potential option file paths to the option_parser
|
18
|
+
def register_config_files
|
19
|
+
[Dir.home, Dir.pwd].each do |dir|
|
20
|
+
option_parser.config_files.class.supported_extensions.each do |ext|
|
21
|
+
option_parser.config_files << Pathname.new(dir).join(".#{NS.app_name}", "scan.#{ext}").to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param [ Controller::Base ] controller
|
27
|
+
#
|
28
|
+
# @retun [ Controllers ] self
|
29
|
+
def <<(controller)
|
30
|
+
options = controller.cli_options
|
31
|
+
|
32
|
+
unless include?(controller)
|
33
|
+
option_parser.add(*options) if options
|
34
|
+
super(controller)
|
35
|
+
end
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def run
|
40
|
+
NS::ParsedCli.options = option_parser.results
|
41
|
+
first.class.option_parser = option_parser # To be able to output the help when -h/--hh
|
42
|
+
|
43
|
+
redirect_output_to_file(NS::ParsedCli.output) if NS::ParsedCli.output
|
44
|
+
|
45
|
+
Timeout.timeout(NS::ParsedCli.max_scan_duration, NS::Error::MaxScanDurationReached) do
|
46
|
+
each(&:before_scan)
|
47
|
+
|
48
|
+
@running = true
|
49
|
+
|
50
|
+
each(&:run)
|
51
|
+
end
|
52
|
+
ensure
|
53
|
+
# The rescue is there to prevent unfinished requests to raise an error, which would prevent
|
54
|
+
# the reverse_each to run
|
55
|
+
# rubocop:disable Style/RescueModifier
|
56
|
+
NS::Browser.instance.hydra.abort rescue nil
|
57
|
+
# rubocop:enable Style/RescueModifier
|
58
|
+
|
59
|
+
# Reverse is used here as the app/controllers/core#after_scan finishes the output
|
60
|
+
# and must be the last one to be executed. It also guarantee that stats will be output
|
61
|
+
# even when an error occurs, which could help in debugging.
|
62
|
+
# However, the #after_scan methods are only executed if the scan was running, and won't be
|
63
|
+
# called when there is a CLI error, or just -h/--hh/--version for example
|
64
|
+
reverse_each(&:after_scan) if running
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
module Error
|
5
|
+
# Target Down Error
|
6
|
+
class TargetDown < Standard
|
7
|
+
attr_reader :response
|
8
|
+
|
9
|
+
def initialize(response)
|
10
|
+
@response = response
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"The url supplied '#{response.request.url}' seems to be down (#{response.return_message})"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# HTTP Authentication Required Error
|
19
|
+
class HTTPAuthRequired < Standard
|
20
|
+
# :nocov:
|
21
|
+
def to_s
|
22
|
+
'HTTP authentication required (or was invalid), please provide it with --http-auth'
|
23
|
+
end
|
24
|
+
# :nocov:
|
25
|
+
end
|
26
|
+
|
27
|
+
# Proxy Authentication Required Error
|
28
|
+
class ProxyAuthRequired < Standard
|
29
|
+
# :nocov:
|
30
|
+
def to_s
|
31
|
+
'Proxy authentication required (or was invalid), please provide it with --proxy-auth'
|
32
|
+
end
|
33
|
+
# :nocov:
|
34
|
+
end
|
35
|
+
|
36
|
+
# Access Forbidden Error
|
37
|
+
class AccessForbidden < Standard
|
38
|
+
attr_reader :random_user_agent_used
|
39
|
+
|
40
|
+
# @param [ Boolean ] random_user_agent_used
|
41
|
+
def initialize(random_user_agent_used)
|
42
|
+
@random_user_agent_used = random_user_agent_used
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
msg = if random_user_agent_used
|
47
|
+
'Well... --random-user-agent didn\'t work, use --force to skip this check if needed.'
|
48
|
+
else
|
49
|
+
'Please re-try with --random-user-agent'
|
50
|
+
end
|
51
|
+
|
52
|
+
"The target is responding with a 403, this might be due to a WAF. #{msg}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# HTTP Redirect Error
|
57
|
+
class HTTPRedirect < Standard
|
58
|
+
attr_reader :redirect_uri
|
59
|
+
|
60
|
+
# @param [ String ] url
|
61
|
+
def initialize(url)
|
62
|
+
@redirect_uri = Addressable::URI.parse(url).normalize
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
"The URL supplied redirects to #{redirect_uri}. Use the --ignore-main-redirect "\
|
67
|
+
'option to ignore the redirection and scan the target, or change the --url option ' \
|
68
|
+
'value to the redirected URL.'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
# Exit Code Values
|
5
|
+
module ExitCode
|
6
|
+
# No error, scan finished w/o any vulnerabilies found
|
7
|
+
OK = 0
|
8
|
+
|
9
|
+
# All exceptions raised by OptParseValidator and OptionParser
|
10
|
+
CLI_OPTION_ERROR = 1
|
11
|
+
|
12
|
+
# Interrupt received
|
13
|
+
INTERRUPTED = 2
|
14
|
+
|
15
|
+
# Unhandled/unexpected Exception occured
|
16
|
+
EXCEPTION = 3
|
17
|
+
|
18
|
+
# Error, scan did not finish
|
19
|
+
ERROR = 0
|
20
|
+
|
21
|
+
# The target has at least one vulnerability.
|
22
|
+
# Currently, the interesting findings do not count as vulnerable things
|
23
|
+
VULNERABLE = 0
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
module Finders
|
5
|
+
# Base class container for the Finders (i.e IndependentFinders etc)
|
6
|
+
class BaseFinders < Array
|
7
|
+
# @return [ Findings ]
|
8
|
+
def findings
|
9
|
+
@findings ||= NS::Finders::Findings.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Should be implemented in child classes
|
13
|
+
def run; end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
# @param [ Symbol ] mode :mixed, :passive or :aggressive
|
18
|
+
# @return [ Array<Symbol> ] The symbols to call for the mode
|
19
|
+
def symbols_from_mode(mode)
|
20
|
+
symbols = %i[passive aggressive]
|
21
|
+
|
22
|
+
return symbols if mode.nil? || mode == :mixed
|
23
|
+
|
24
|
+
symbols.include?(mode) ? Array(mode) : []
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param [ CMSScanner::Finders::Finder ] finder
|
28
|
+
# @param [ Symbol ] symbol See return values of #symbols_from_mode
|
29
|
+
# @param [ Hash ] opts
|
30
|
+
def run_finder(finder, symbol, opts)
|
31
|
+
Array(finder.send(symbol, opts.merge(found: findings))).compact.each do |found|
|
32
|
+
findings << found
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Allow child classes to filter the findings, such as return the best one
|
37
|
+
# or remove the low confidence ones.
|
38
|
+
#
|
39
|
+
# @return [ Findings ]
|
40
|
+
def filter_findings
|
41
|
+
findings
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMSScanner
|
4
|
+
module Finders
|
5
|
+
class Finder
|
6
|
+
# Module to provide an easy way to perform password attacks
|
7
|
+
module BreadthFirstDictionaryAttack
|
8
|
+
# @param [ Array<CMSScanner::Model::User> ] users
|
9
|
+
# @param [ String ] wordlist_path
|
10
|
+
# @param [ Hash ] opts
|
11
|
+
# @option opts [ Boolean ] :show_progression
|
12
|
+
#
|
13
|
+
# @yield [ CMSScanner::User ] When a valid combination is found
|
14
|
+
#
|
15
|
+
# Due to Typhoeus threads shenanigans, in rare cases the progress-bar might
|
16
|
+
# be incorrectly updated, hence the 'rescue ProgressBar::InvalidProgressError'
|
17
|
+
#
|
18
|
+
# TODO: Make rubocop happy about metrics etc
|
19
|
+
#
|
20
|
+
# rubocop:disable all
|
21
|
+
def attack(users, wordlist_path, opts = {})
|
22
|
+
wordlist = File.open(wordlist_path)
|
23
|
+
|
24
|
+
create_progress_bar(total: users.size * wordlist.count, show_progression: opts[:show_progression])
|
25
|
+
|
26
|
+
queue_count = 0
|
27
|
+
# Keep the number of requests sent for each users
|
28
|
+
# to be able to correctly update the progress when a password is found
|
29
|
+
user_requests_count = {}
|
30
|
+
|
31
|
+
users.each { |u| user_requests_count[u.username] = 0 }
|
32
|
+
|
33
|
+
File.foreach(wordlist, chomp: true) do |password|
|
34
|
+
remaining_users = users.select { |u| u.password.nil? }
|
35
|
+
|
36
|
+
break if remaining_users.empty?
|
37
|
+
|
38
|
+
remaining_users.each do |user|
|
39
|
+
request = login_request(user.username, password)
|
40
|
+
|
41
|
+
user_requests_count[user.username] += 1
|
42
|
+
|
43
|
+
request.on_complete do |res|
|
44
|
+
progress_bar.title = "Trying #{user.username} / #{password}"
|
45
|
+
|
46
|
+
progress_bar.increment unless progress_bar.progress == progress_bar.total
|
47
|
+
|
48
|
+
if valid_credentials?(res)
|
49
|
+
user.password = password
|
50
|
+
|
51
|
+
begin
|
52
|
+
progress_bar.total -= wordlist.count - user_requests_count[user.username]
|
53
|
+
rescue ProgressBar::InvalidProgressError
|
54
|
+
end
|
55
|
+
|
56
|
+
yield user
|
57
|
+
elsif errored_response?(res)
|
58
|
+
output_error(res)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
hydra.queue(request)
|
63
|
+
queue_count += 1
|
64
|
+
|
65
|
+
if queue_count >= hydra.max_concurrency
|
66
|
+
hydra.run
|
67
|
+
queue_count = 0
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
hydra.run
|
73
|
+
progress_bar.stop
|
74
|
+
end
|
75
|
+
# rubocop:enable all
|
76
|
+
|
77
|
+
# @param [ String ] username
|
78
|
+
# param [ String ] password
|
79
|
+
#
|
80
|
+
# @return [ Typhoeus::Request ]
|
81
|
+
def login_request(username, password)
|
82
|
+
# To Implement in the finder related to the attack
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [ Typhoeus::Response ] response
|
86
|
+
#
|
87
|
+
# @return [ Boolean ] Whether or not credentials related to the request are valid
|
88
|
+
def valid_credentials?(response)
|
89
|
+
# To Implement in the finder related to the attack
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param [ Typhoeus::Response ] response
|
93
|
+
#
|
94
|
+
# @return [ Boolean ] Whether or not something wrong happened
|
95
|
+
# other than wrong credentials
|
96
|
+
def errored_response?(response)
|
97
|
+
# To Implement in the finder related to the attack
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
# @param [ Typhoeus::Response ] response
|
103
|
+
def output_error(response)
|
104
|
+
error = if response.timed_out?
|
105
|
+
'Request timed out.'
|
106
|
+
elsif response.code.zero?
|
107
|
+
"No response from remote server. WAF/IPS? (#{response.return_message})"
|
108
|
+
elsif response.code.to_s.start_with?('50')
|
109
|
+
'Server error, try reducing the number of threads.'
|
110
|
+
elsif NS::ParsedCli.verbose?
|
111
|
+
"Unknown response received Code: #{response.code}\nBody: #{response.body}"
|
112
|
+
else
|
113
|
+
"Unknown response received Code: #{response.code}"
|
114
|
+
end
|
115
|
+
|
116
|
+
progress_bar.log("Error: #{error}")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|