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.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +26 -0
  4. data/app/app.rb +24 -0
  5. data/app/controllers/core/cli_options.rb +117 -0
  6. data/app/controllers/core.rb +82 -0
  7. data/app/controllers/interesting_findings.rb +25 -0
  8. data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
  9. data/app/finders/interesting_findings/headers.rb +17 -0
  10. data/app/finders/interesting_findings/robots_txt.rb +20 -0
  11. data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
  12. data/app/finders/interesting_findings/xml_rpc.rb +61 -0
  13. data/app/finders/interesting_findings.rb +25 -0
  14. data/app/formatters/cli.rb +65 -0
  15. data/app/formatters/cli_no_color.rb +9 -0
  16. data/app/formatters/cli_no_colour.rb +17 -0
  17. data/app/formatters/json.rb +14 -0
  18. data/app/models/fantastico_fileslist.rb +34 -0
  19. data/app/models/headers.rb +44 -0
  20. data/app/models/interesting_finding.rb +48 -0
  21. data/app/models/robots_txt.rb +31 -0
  22. data/app/models/search_replace_db_2.rb +17 -0
  23. data/app/models/user.rb +35 -0
  24. data/app/models/version.rb +49 -0
  25. data/app/models/xml_rpc.rb +78 -0
  26. data/app/user_agents.txt +46 -0
  27. data/app/views/cli/core/banner.erb +1 -0
  28. data/app/views/cli/core/finished.erb +8 -0
  29. data/app/views/cli/core/help.erb +4 -0
  30. data/app/views/cli/core/started.erb +6 -0
  31. data/app/views/cli/core/version.erb +1 -0
  32. data/app/views/cli/interesting_findings/_array.erb +10 -0
  33. data/app/views/cli/interesting_findings/findings.erb +23 -0
  34. data/app/views/cli/scan_aborted.erb +5 -0
  35. data/app/views/cli/usage.erb +3 -0
  36. data/app/views/json/core/banner.erb +1 -0
  37. data/app/views/json/core/finished.erb +10 -0
  38. data/app/views/json/core/help.erb +4 -0
  39. data/app/views/json/core/started.erb +5 -0
  40. data/app/views/json/core/version.erb +1 -0
  41. data/app/views/json/interesting_findings/findings.erb +24 -0
  42. data/app/views/json/scan_aborted.erb +5 -0
  43. data/lib/cms_scanner/browser/actions.rb +48 -0
  44. data/lib/cms_scanner/browser/options.rb +90 -0
  45. data/lib/cms_scanner/browser.rb +96 -0
  46. data/lib/cms_scanner/cache/file_store.rb +77 -0
  47. data/lib/cms_scanner/cache/typhoeus.rb +25 -0
  48. data/lib/cms_scanner/controller.rb +105 -0
  49. data/lib/cms_scanner/controllers.rb +67 -0
  50. data/lib/cms_scanner/errors/http.rb +72 -0
  51. data/lib/cms_scanner/errors/scan.rb +14 -0
  52. data/lib/cms_scanner/errors.rb +11 -0
  53. data/lib/cms_scanner/exit_code.rb +25 -0
  54. data/lib/cms_scanner/finders/base_finders.rb +45 -0
  55. data/lib/cms_scanner/finders/finder/breadth_first_dictionary_attack.rb +121 -0
  56. data/lib/cms_scanner/finders/finder/enumerator.rb +77 -0
  57. data/lib/cms_scanner/finders/finder/fingerprinter.rb +48 -0
  58. data/lib/cms_scanner/finders/finder/smart_url_checker/findings.rb +33 -0
  59. data/lib/cms_scanner/finders/finder/smart_url_checker.rb +60 -0
  60. data/lib/cms_scanner/finders/finder.rb +75 -0
  61. data/lib/cms_scanner/finders/finding.rb +54 -0
  62. data/lib/cms_scanner/finders/findings.rb +26 -0
  63. data/lib/cms_scanner/finders/independent_finder.rb +30 -0
  64. data/lib/cms_scanner/finders/independent_finders.rb +26 -0
  65. data/lib/cms_scanner/finders/same_type_finder.rb +19 -0
  66. data/lib/cms_scanner/finders/same_type_finders.rb +26 -0
  67. data/lib/cms_scanner/finders/unique_finder.rb +19 -0
  68. data/lib/cms_scanner/finders/unique_finders.rb +47 -0
  69. data/lib/cms_scanner/finders.rb +12 -0
  70. data/lib/cms_scanner/formatter/buffer.rb +17 -0
  71. data/lib/cms_scanner/formatter.rb +149 -0
  72. data/lib/cms_scanner/helper.rb +7 -0
  73. data/lib/cms_scanner/numeric.rb +13 -0
  74. data/lib/cms_scanner/parsed_cli.rb +37 -0
  75. data/lib/cms_scanner/progressbar_null_output.rb +23 -0
  76. data/lib/cms_scanner/public_suffix/domain.rb +42 -0
  77. data/lib/cms_scanner/references.rb +132 -0
  78. data/lib/cms_scanner/scan.rb +88 -0
  79. data/lib/cms_scanner/target/hashes.rb +45 -0
  80. data/lib/cms_scanner/target/platform/php.rb +62 -0
  81. data/lib/cms_scanner/target/platform.rb +3 -0
  82. data/lib/cms_scanner/target/scope.rb +103 -0
  83. data/lib/cms_scanner/target/server/apache.rb +27 -0
  84. data/lib/cms_scanner/target/server/generic.rb +72 -0
  85. data/lib/cms_scanner/target/server/iis.rb +29 -0
  86. data/lib/cms_scanner/target/server/nginx.rb +27 -0
  87. data/lib/cms_scanner/target/server.rb +6 -0
  88. data/lib/cms_scanner/target.rb +124 -0
  89. data/lib/cms_scanner/typhoeus/hydra.rb +12 -0
  90. data/lib/cms_scanner/typhoeus/response.rb +27 -0
  91. data/lib/cms_scanner/version.rb +6 -0
  92. data/lib/cms_scanner/vulnerability.rb +46 -0
  93. data/lib/cms_scanner/web_site.rb +145 -0
  94. data/lib/cms_scanner.rb +141 -0
  95. 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Error
5
+ # Used instead of the Timeout::Error
6
+ class MaxScanDurationReached < Standard
7
+ # :nocov:
8
+ def to_s
9
+ 'Max Scan Duration Reached'
10
+ end
11
+ # :nocov:
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMSScanner
4
+ module Error
5
+ class Standard < StandardError
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative 'errors/http'
11
+ require_relative 'errors/scan'
@@ -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