new_cms_scanner 0.13.7

Sign up to get free protection for your applications and to get access to all the features.
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