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