wpscanteam/CMSScanner

View on GitHub
lib/cms_scanner/finders/finder/breadth_first_dictionary_attack.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true

module CMSScanner
  module Finders
    class Finder
      # Module to provide an easy way to perform password attacks
      module BreadthFirstDictionaryAttack
        # @param [ Array<CMSScanner::Model::User> ] users
        # @param [ String ] wordlist_path
        # @param [ Hash ] opts
        # @option opts [ Boolean ] :show_progression
        #
        # @yield [ CMSScanner::User ] When a valid combination is found
        #
        # Due to Typhoeus threads shenanigans, in rare cases the progress-bar might
        # be incorrectly updated, hence the 'rescue ProgressBar::InvalidProgressError'
        #
        # TODO: Make rubocop happy about metrics etc
        #
        # rubocop:disable all
        def attack(users, wordlist_path, opts = {})
          wordlist = File.open(wordlist_path)

          create_progress_bar(total: users.size * wordlist.count, show_progression: opts[:show_progression])

          queue_count         = 0
          # Keep the number of requests sent for each users
          # to be able to correctly update the progress when a password is found
          user_requests_count = {}

          users.each { |u| user_requests_count[u.username] = 0 }

          File.foreach(wordlist, chomp: true) do |password|
            remaining_users = users.select { |u| u.password.nil? }

            break if remaining_users.empty?

            remaining_users.each do |user|
              request = login_request(user.username, password)

              user_requests_count[user.username] += 1

              request.on_complete do |res|
                progress_bar.title = "Trying #{user.username} / #{password}"

                progress_bar.increment unless progress_bar.progress == progress_bar.total

                if valid_credentials?(res)
                  user.password = password

                  begin
                    progress_bar.total -= wordlist.count - user_requests_count[user.username]
                  rescue ProgressBar::InvalidProgressError
                  end

                  yield user
                elsif errored_response?(res)
                  output_error(res)
                end
              end

              hydra.queue(request)
              queue_count += 1

              if queue_count >= hydra.max_concurrency
                hydra.run
                queue_count = 0
              end
            end
          end

          hydra.run
          progress_bar.stop
        end
        # rubocop:enable all

        # @param [ String ] username
        # param [ String ] password
        #
        # @return [ Typhoeus::Request ]
        def login_request(username, password)
          # To Implement in the finder related to the attack
        end

        # @param [ Typhoeus::Response ] response
        #
        # @return [ Boolean ] Whether or not credentials related to the request are valid
        def valid_credentials?(response)
          # To Implement in the finder related to the attack
        end

        # @param [ Typhoeus::Response ] response
        #
        # @return [ Boolean ] Whether or not something wrong happened
        #                     other than wrong credentials
        def errored_response?(response)
          # To Implement in the finder related to the attack
        end

        protected

        # @param [ Typhoeus::Response ] response
        def output_error(response)
          error = if response.timed_out?
                    'Request timed out.'
                  elsif response.code.zero?
                    "No response from remote server. WAF/IPS? (#{response.return_message})"
                  elsif response.code.to_s.start_with?('50')
                    'Server error, try reducing the number of threads.'
                  elsif NS::ParsedCli.verbose?
                    "Unknown response received Code: #{response.code}\nBody: #{response.body}"
                  else
                    "Unknown response received Code: #{response.code}"
                  end

          progress_bar.log("Error: #{error}")
        end
      end
    end
  end
end