rapid7/metasploit-credential

View on GitHub
lib/metasploit/credential/creation.rb

Summary

Maintainability
D
2 days
Test Coverage
require 'rex/socket'

# Implements a set of "convenience methods" for creating credentials and related portions of the object graph.  Creates
# {Metasploit::Credential::Core} objects and their attendant relationships as well as {Metasploit::Credential::Login}
# objects and their attendant `Mdm::Host` and `Mdm::Service` objects.
module Metasploit::Credential::Creation

  # Returns true if ActiveRecord has an active database connection, false otherwise.
  # @return [Boolean]
  def active_db?
    ApplicationRecord.connected?
  end

  # This method takes a few simple parameters and creates a new username/password
  # credential that was obtained by cracking a hash. It reuses the relevant
  # components form the originating {Metasploit::Credential::Core} and builds new
  # {Metasploit::Credential::Login} objects based on the ones attached to the originating
  # {Metasploit::Credential::Core}
  #
  # @option opts [String] :username the username to find or create the {Metasploit::Credential::Public} from
  # @option opts [String] :password the password to find or create the {Metasploit::Credential::Password} from
  # @option opts [Fixnum] :core_id the id for the originating {Metasploit::Credential::Core}
  def create_cracked_credential(opts={})
    return nil unless active_db?

    if self.respond_to?(:[]) and self[:task]
      opts[:task_id] ||= self[:task].record.id
    end

    username = opts.fetch(:username)
    password = opts.fetch(:password)
    core_id  = opts.fetch(:core_id)

    private  = nil
    public   = nil
    old_core = nil
    old_realm_id = nil

    retry_transaction do
      private  = Metasploit::Credential::Password.where(data: password).first_or_create!
      public   = Metasploit::Credential::Public.where(username: username).first_or_create!
      old_core = Metasploit::Credential::Core.find(core_id)
      old_realm_id = old_core.realm.id if old_core.realm
    end

    core = nil

    retry_transaction do
      core = Metasploit::Credential::Core.where(public_id: public.id, private_id: private.id, realm_id: old_realm_id, workspace_id: old_core.workspace_id).first_or_initialize
      if core.origin_id.nil?
        origin      = Metasploit::Credential::Origin::CrackedPassword.where(metasploit_credential_core_id: core_id).first_or_create!
        core.origin = origin
      end
      if opts[:task_id]
        core.tasks << Mdm::Task.find(opts[:task_id])
      end
      core.save!
    end

    old_core.logins.each do |login|
      service_id = login.service_id
      new_login = Metasploit::Credential::Login.where(core_id: core.id, service_id: service_id).first_or_initialize
      if new_login.status.blank?
        new_login.status =  Metasploit::Model::Login::Status::UNTRIED
      end
      new_login.save!
    end
    core
  end


  # This method is responsible for creation {Metasploit::Credential::Core} objects
  # and all sub-objects that it is dependent upon.
  #
  # @option opts [String] :jtr_format The format for John the ripper to use to try and crack this
  # @option opts [Symbol] :origin_type The Origin type we are trying to create
  # @option opts [String] :address The address of the `Mdm::Host` to link this Origin to
  # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Origin to
  # @option opts [String] :service_name The service name to use for the `Mdm::Service`
  # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Origin to
  # @option opts [String] :module_fullname The fullname of the Metasploit Module to link this Origin to
  # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
  # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Origin and Core to
  # @option opts [String] :filename The filename of the file that was imported
  # @option opts [Fixnum] :user_id The ID of the `Mdm::User` to link this Origin to
  # @option opts [Fixnum] :session_id The ID of the `Mdm::Session` to link this Origin to
  # @option opts [String] :post_reference_name The reference name of the Metasploit Post module to link the origin to
  # @option opts [String] :private_data The actual data for the private (e.g. password, hash, key etc)
  # @option opts [Symbol] :private_type The type of {Metasploit::Credential::Private} to create
  # @option opts [String] :username The username to use for the {Metasploit::Credential::Public}
  # @raise [KeyError] if a required option is missing
  # @raise [ArgumentError] if an invalid :private_type is specified
  # @raise [ArgumentError] if an invalid :origin_type is specified
  # @return [NilClass] if there is no active database connection
  # @return [Metasploit::Credential::Core]
  # @example Reporting a Bruteforced Credential
  #     create_credential(
  #       origin_type: :service,
  #       address: '192.168.1.100',
  #       port: 445,
  #       service_name: 'smb',
  #       protocol: 'tcp',
  #       module_fullname: 'auxiliary/scanner/smb/smb_login',
  #       workspace_id: myworkspace.id,
  #       private_data: 'password1',
  #       private_type: :password,
  #       username: 'Administrator'
  #     )
  def create_credential(opts={})
    return nil unless active_db?

    if self.respond_to?(:[]) and self[:task]
      opts[:task_id] ||= self[:task].record.id
    end

    if opts[:origin]
      origin = opts[:origin]
    else
      origin = create_credential_origin(opts)
    end
    return nil if origin.nil?

    core_opts = {
        origin: origin,
        workspace_id: opts.fetch(:workspace_id)
    }

    if opts.has_key?(:realm_key) && opts.has_key?(:realm_value)
      core_opts[:realm] = create_credential_realm(opts)
    end

    if opts.has_key?(:private_type) && opts.has_key?(:private_data)
      core_opts[:private] = create_credential_private(opts)
    end

    if opts.has_key?(:username)
      core_opts[:public] = create_credential_public(opts)
    end

    if opts.has_key?(:task_id)
      core_opts[:task_id] = opts[:task_id]
    end

    create_credential_core(core_opts)
  end

  # This method is responsible for creation {Metasploit::Credential::Core} and
  # {Metasploit::Credential::Login}.
  # This method is responsible for creating a {Metasploit::Credential::Login} object
  # which ties a {Metasploit::Credential::Core} to the `Mdm::Service` it is a valid
  # credential for.
  #
  # NOTE: for origin_type: service it must be the same service your going to create a login for.
  #
  # {Metasploit::Credential::Core} options
  # @option opts [String] :jtr_format The format for John the ripper to use to try and crack this
  # @option opts [Symbol] :origin_type The Origin type we are trying to create
  # @option opts [String] :address The address of the `Mdm::Host` to link this Origin to
  # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Origin to
  # @option opts [String] :service_name The service name to use for the `Mdm::Service`
  # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Origin to
  # @option opts [String] :module_fullname The fullname of the Metasploit Module to link this Origin to
  # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
  # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Origin and Core to
  # @option opts [String] :filename The filename of the file that was imported
  # @option opts [Fixnum] :user_id The ID of the `Mdm::User` to link this Origin to
  # @option opts [Fixnum] :session_id The ID of the `Mdm::Session` to link this Origin to
  # @option opts [String] :post_reference_name The reference name of the Metasploit Post module to link the origin to
  # @option opts [String] :private_data The actual data for the private (e.g. password, hash, key etc)
  # @option opts [Symbol] :private_type The type of {Metasploit::Credential::Private} to create
  # {Metasploit::Credential::Login}
  # @option opts [String] :access_level The access level to assign to this login if we know it
  # @option opts [String] :status The status for the Login object
  # @raise [KeyError] if a required option is missing
  # @raise [ArgumentError] if an invalid :private_type is specified
  # @raise [ArgumentError] if an invalid :origin_type is specified
  # @return [NilClass] if there is no active database connection
  # @return [Metasploit::Credential::Core]
  # @example Reporting a Bruteforced Credential and Login
  #     create_credential_and_login(
  #       origin_type: :service,
  #       address: '192.168.1.100',
  #       port: 445,
  #       service_name: 'smb',
  #       protocol: 'tcp',
  #       module_fullname: 'auxiliary/scanner/smb/smb_login',
  #       workspace_id: myworkspace.id,
  #       private_data: 'password1',
  #       private_type: :password,
  #       username: 'Administrator',
  #       service_name: 'smb',
  #       status: status: Metasploit::Model::Login::Status::UNTRIED
  #     )
  def create_credential_and_login(opts={})
    return nil unless active_db?

    if self.respond_to?(:[]) and self[:task]
      opts[:task_id] ||= self[:task].record.id
    end

    core               = opts.fetch(:core, create_credential(opts))
    access_level       = opts.fetch(:access_level, nil)
    last_attempted_at  = opts.fetch(:last_attempted_at, nil)
    status             = opts.fetch(:status, Metasploit::Model::Login::Status::UNTRIED)

    login_object = nil
    retry_transaction do
      service_object = create_credential_service(opts)
      return nil if service_object.nil?
      login_object = Metasploit::Credential::Login.where(core_id: core.id, service_id: service_object.id).first_or_initialize

      if opts[:task_id]
        login_object.tasks << Mdm::Task.find(opts[:task_id])
      end

      login_object.access_level      = access_level if access_level
      login_object.last_attempted_at = last_attempted_at if last_attempted_at
      if status == Metasploit::Model::Login::Status::UNTRIED
        if login_object.last_attempted_at.nil?
          login_object.status = status
        end
      else
        login_object.status = status
      end
      login_object.save!
    end

    login_object
  end

  # This method is responsible for creating {Metasploit::Credential::Core} objects.
  #
  # @option opts [Metasploit::Credential::Origin] :origin The origin object to tie the core to
  # @option opts [Metasploit::Credential::Public] :public The {Metasploit::Credential::Public} component
  # @option opts [Metasploit::Credential::Private] :private The {Metasploit::Credential::Private} component
  # @option opts [Metasploit::Credential::Realm] :realm The {Metasploit::Credential::Realm} component
  # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to tie the Core to
  # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Core to
  # @return [NilClass] if there is no active database connection
  # @return [Metasploit::Credential::Core]
  def create_credential_core(opts={})
    return nil unless active_db?

    if self.respond_to?(:[]) and self[:task]
      opts[:task_id] ||= self[:task].record.id
    end

    origin       = opts.fetch(:origin)
    workspace_id = opts.fetch(:workspace_id)

    private_id   = opts[:private].try(:id)
    public_id    = opts[:public].try(:id)
    realm_id     = opts[:realm].try(:id)

    core = nil
    retry_transaction do
      core = Metasploit::Credential::Core.where(private_id: private_id, public_id: public_id, realm_id: realm_id, workspace_id: workspace_id).first_or_initialize
      if core.origin_id.nil?
        core.origin = origin
      end
      if opts[:task_id]
        core.tasks << Mdm::Task.find(opts[:task_id])
      end
      core.save!
    end

    core
  end

  # This method is responsible for creating a {Metasploit::Credential::Login} object
  # which ties a {Metasploit::Credential::Core} to the `Mdm::Service` it is a valid
  # credential for.
  #
  # @option opts [String] :access_level The access level to assign to this login if we know it
  # @option opts [String] :address The address of the `Mdm::Host` to link this Login to
  # @option opts [DateTime] :last_attempted_at The last time this Login was attempted
  # @option opts [Metasploit::Credential::Core] :core The {Metasploit::Credential::Core} to link this login to
  # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Login to
  # @option opts [String] :service_id The ID of an `Mdm::Service` to link this login to
  # @option opts [String] :service_name The service name to use for the `Mdm::Service`
  # @option opts [String] :status The status for the Login object
  # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Login to
  # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
  # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Login to
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no active database connection
  # @return [Metasploit::Credential::Login]
  def create_credential_login(opts={})
    return nil unless active_db?

    if self.respond_to?(:[]) and self[:task]
      opts[:task_id] ||= self[:task].record.id
    end

    core               = opts.fetch(:core)
    access_level       = opts.fetch(:access_level, nil)
    last_attempted_at  = opts.fetch(:last_attempted_at, nil)
    status             = opts.fetch(:status, Metasploit::Model::Login::Status::UNTRIED)

    login_object = nil
    retry_transaction do
      service_object = Mdm::Service.where(id: opts[:service_id]).first if opts[:service_id]
      service_object = create_credential_service(opts) if service_object.nil?
      return nil if service_object.nil?
      login_object = Metasploit::Credential::Login.where(core_id: core.id, service_id: service_object.id).first_or_initialize

      if opts[:task_id]
        login_object.tasks << Mdm::Task.find(opts[:task_id])
      end

      login_object.access_level      = access_level if access_level
      login_object.last_attempted_at = last_attempted_at if last_attempted_at
      if status == Metasploit::Model::Login::Status::UNTRIED
        if login_object.last_attempted_at.nil?
          login_object.status = status
        end
      else
        login_object.status = status
      end
      login_object.save!
    end

    login_object
  end

  # This method is responsible for creating the various Credential::Origin objects.
  # It takes a key for the Origin type and delegates to the correct sub-method.
  #
  # @option opts [Symbol] :origin_type The Origin type we are trying to create
  # @option opts [String] :address The address of the `Mdm::Host` to link this Origin to
  # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Origin to
  # @option opts [String] :service_name The service name to use for the `Mdm::Service`
  # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Origin to
  # @option opts [String] :module_fullname The fullname of the Metasploit Module to link this Origin to
  # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
  # @option opts [Fixnum] :task_id The ID of the `Mdm::Task` to link this Origin to
  # @option opts [String] :filename The filename of the file that was imported
  # @option opts [Fixnum] :user_id The ID of the `Mdm::User` to link this Origin to
  # @option opts [Fixnum] :session_id The ID of the `Mdm::Session` to link this Origin to
  # @option opts [String] :post_reference_name The reference name of the Metasploit Post module to link the origin to
  # @raise [ArgumentError] if an invalid origin_type was provided
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no connected database
  # @return [Metasploit::Credential::Origin::Manual] if :origin_type was :manual
  # @return [Metasploit::Credential::Origin::Import] if :origin_type was :import
  # @return [Metasploit::Credential::Origin::Service] if :origin_type was :service
  # @return [Metasploit::Credential::Origin::Session] if :origin_type was :session
  def create_credential_origin(opts={})
    return nil unless active_db?
    case opts[:origin_type]
    when :cracked_password
      create_credential_origin_cracked_password(opts)
    when :import
      create_credential_origin_import(opts)
    when :manual
      create_credential_origin_manual(opts)
    when :service
      create_credential_origin_service(opts)
    when :session
      create_credential_origin_session(opts)
    else
      raise ArgumentError, "Unknown Origin Type #{opts[:origin_type]}"
    end
  end

  # This method is responsible for creating {Metasploit::Credential::Origin::CrackedPassword} objects.
  # These are the origins that show that a password Credential was obtained by cracking a hash Credential
  # that previously existed in the database.
  #
  # @option opts [Fixnum] :originating_core_id The ID of the originating Credential core.
  # @return [NilClass] if there is no connected database
  # @return [Metasploit::Credential::Origin::CrackedPassword] The created {Metasploit::Credential::Origin::CrackedPassword} object
  def create_credential_origin_cracked_password(opts={})
    return nil unless active_db?
    originating_core_id = opts.fetch(:originating_core_id)

    retry_transaction do
      Metasploit::Credential::Origin::CrackedPassword.where(metasploit_credential_core_id: originating_core_id ).first_or_create!
    end
  end

  # This method is responsible for creating {Metasploit::Credential::Origin::Import} objects.
  #
  # @option opts [String] :filename The filename of the file that was imported
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no connected database
  # @return [Metasploit::Credential::Origin::Manual] The created {Metasploit::Credential::Origin::Import} object
  def create_credential_origin_import(opts={})
    return nil unless active_db?
    filename = opts.fetch(:filename)

    retry_transaction do
      Metasploit::Credential::Origin::Import.where(filename: filename).first_or_create!
    end
  end

  # This method is responsible for creating {Metasploit::Credential::Origin::Manual} objects.
  #
  # @option opts [Fixnum] :user_id The ID of the `Mdm::User` to link this Origin to
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no connected database
  # @return [Metasploit::Credential::Origin::Manual] The created {Metasploit::Credential::Origin::Manual} object
  def create_credential_origin_manual(opts={})
    return nil unless active_db?
    user_id = opts.fetch(:user_id)

    retry_transaction do
      Metasploit::Credential::Origin::Manual.where(user_id: user_id).first_or_create!
    end
  end

  # This method is responsible for creating {Metasploit::Credential::Origin::Service} objects.
  # If there is not a matching `Mdm::Host` it will create it. If there is not a matching
  # `Mdm::Service` it will create that too.
  #
  # @option opts [String] :address The address of the `Mdm::Host` to link this Origin to
  # @option opts [Fixnum] :port The port number of the `Mdm::Service` to link this Origin to
  # @option opts [String] :service_name The service name to use for the `Mdm::Service`
  # @option opts [String] :protocol The protocol type of the `Mdm::Service` to link this Origin to
  # @option opts [String] :module_fullname The fullname of the Metasploit Module to link this Origin to
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no connected database
  # @return [Metasploit::Credential::Origin::Service] The created {Metasploit::Credential::Origin::Service} object
  def create_credential_origin_service(opts={})
    return nil unless active_db?
    module_fullname  = opts.fetch(:module_fullname)
    service_object = create_credential_service(opts)
    return nil if service_object.nil?

    retry_transaction do
      Metasploit::Credential::Origin::Service.where(service_id: service_object.id, module_full_name: module_fullname).first_or_create!
    end
  end

  # This method is responsible for creating {Metasploit::Credential::Origin::Session} objects.
  #
  # @option opts [Fixnum] :session_id The ID of the `Mdm::Session` to link this Origin to
  # @option opts [String] :post_reference_name The reference name of the Metasploit Post module to link the origin to
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no connected database
  # @return [Metasploit::Credential::Origin::Session] The created {Metasploit::Credential::Origin::Session} object
  def create_credential_origin_session(opts={})
    return nil unless active_db?
    session_id           = opts.fetch(:session_id)
    post_reference_name  = opts.fetch(:post_reference_name)

    retry_transaction do
      Metasploit::Credential::Origin::Session.where(session_id: session_id, post_reference_name: post_reference_name).first_or_create!
    end
  end

  # This method is responsible for the creation of {Metasploit::Credential::Private} objects.
  # It will create the correct subclass based on the type.
  #
  # @option opts [String] :jtr_format The format for John the ripper to use to try and crack this
  # @option opts [String] :private_data The actual data for the private (e.g. password, hash, key etc)
  # @option opts [Symbol] :private_type The type of {Metasploit::Credential::Private} to create
  # @raise [ArgumentError] if a valid type is not supplied
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no active database connection
  # @return [Metasploit::Credential::Password] if the private_type was :password
  # @return [Metasploit::Credential::SSHKey] if the private_type was :ssh_key
  # @return [Metasploit::Credential::NTLMHash] if the private_type was :ntlm_hash
  # @return [Metasploit::Credential::NonreplayableHash] if the private_type was :nonreplayable_hash
  # @return [Metasploit::Credential::KrbEncKey] if the private_type was :krb_enc_key
  def create_credential_private(opts={})
    return nil unless active_db?
    private_data = opts.fetch(:private_data)
    private_type = opts.fetch(:private_type)

    private_object = nil

    retry_transaction do
      if private_data.blank?
        private_object = Metasploit::Credential::BlankPassword.where(data:'').first_or_create
      else
        case private_type
        when :password
          private_object = Metasploit::Credential::Password.where(data: private_data).first_or_create
        when :ssh_key
          private_object = Metasploit::Credential::SSHKey.where(data: private_data).first_or_create
        when :pkcs12
          private_object = Metasploit::Credential::Pkcs12.where(data: private_data).first_or_create
        when :krb_enc_key
          private_object = Metasploit::Credential::KrbEncKey.where(data: private_data).first_or_create
        when :ntlm_hash
          private_object = Metasploit::Credential::NTLMHash.where(data: private_data).first_or_create
          private_object.jtr_format = 'nt,lm'
        when :postgres_md5
          private_object = Metasploit::Credential::PostgresMD5.where(data: private_data).first_or_create
          private_object.jtr_format = 'raw-md5,postgres'
        when :nonreplayable_hash
          private_object = Metasploit::Credential::NonreplayableHash.where(data: private_data).first_or_create
          if opts[:jtr_format].present?
            private_object.jtr_format = opts[:jtr_format]
          end
        else
          raise ArgumentError, "Invalid Private type: #{private_type}"
        end
      end
      private_object.save!
    end
    private_object
  end

  # This method is responsible for the creation of {Metasploit::Credential::Public} objects.
  #
  # @option opts [String] :username The username to use for the {Metasploit::Credential::Public}
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no active database connection
  # @return [Metasploit::Credential::Public]
  def create_credential_public(opts={})
    return nil unless active_db?
    username = opts.fetch(:username)

    retry_transaction do
      if username.blank?
        Metasploit::Credential::BlankUsername.where(username:'').first_or_create!
      else
        Metasploit::Credential::Username.where(username: username).first_or_create!
      end
    end
  end

  # This method is responsible for creating the {Metasploit::Credential::Realm} objects
  # that may be required.
  #
  # @option opts [String] :realm_key The type of Realm this is (e.g. 'Active Directory Domain')
  # @option opts [String] :realm_value The actual Realm name (e.g. contosso)
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no active database connection
  # @return [Metasploit::Credential::Realm] if it successfully creates or finds the object
  def create_credential_realm(opts={})
    return nil unless active_db?
    realm_key   = opts.fetch(:realm_key)
    realm_value = opts.fetch(:realm_value)

    retry_transaction do
      Metasploit::Credential::Realm.where(key: realm_key, value: realm_value).first_or_create!
    end
  end



  # This method is responsible for creating a barebones `Mdm::Service` object
  # for use by Credential object creation.
  #
  # @option opts [String] :address The address of the `Mdm::Host`
  # @option opts [Fixnum] :port The port number of the `Mdm::Service`
  # @option opts [String] :service_name The service name to use for the `Mdm::Service`
  # @option opts [String] :protocol The protocol type of the `Mdm::Service``
  # @option opts [Fixnum] :workspace_id The ID of the `Mdm::Workspace` to use for the `Mdm::Host`
  # @raise [KeyError] if a required option is missing
  # @return [NilClass] if there is no connected database
  # @return [Mdm::Service]
  def create_credential_service(opts={})
    return nil unless active_db?
    address          = opts.fetch(:address)
    return nil unless Rex::Socket.is_ipv4?(address) || Rex::Socket.is_ipv6?(address)
    port             = opts.fetch(:port)
    service_name     = opts.fetch(:service_name)
    protocol         = opts.fetch(:protocol)
    workspace_id     = opts.fetch(:workspace_id)

    host_object    = Mdm::Host.where(address: address, workspace_id: workspace_id).first_or_create
    service_object = Mdm::Service.where(host_id: host_object.id, port: port, proto: protocol).first_or_initialize

    service_object.name  = service_name
    service_object.state = "open"
    service_object.save!

    service_object
  end

  # This method checks to see if a {Metasploit::Credential::Login} exists for a given
  # set of details. If it does exists, we then appropriately set the status to one of our
  # failure statuses.
  #
  # @option opts [String] :address The address of the host we attempted
  # @option opts [Fixnum] :port the port of the service we attempted
  # @option opts [String] :protocol the transport protocol of the service we attempted
  # @option opts [String] :public A string representation of the public we tried
  # @option opts [String] :private A string representation of the private we tried
  # @option opts [Symbol] :status The status symbol from the {Metasploit::Framework::LoginScanner::Result}
  # @raise [KeyError] if any of the above options are missing
  # @return [void] Do not worry about the return value from this method
  def invalidate_login(opts = {})
    return nil unless active_db?
    address     = opts.fetch(:address)
    return nil unless Rex::Socket.is_ipv4?(address) || Rex::Socket.is_ipv6?(address)
    port        = opts.fetch(:port)
    protocol    = opts.fetch(:protocol)
    public      = opts.fetch(:username, nil)
    private     = opts.fetch(:private_data, nil)
    realm_key   = opts.fetch(:realm_key, nil)
    realm_value = opts.fetch(:realm_value, nil)
    status      = opts.fetch(:status)


    pub_obj = Metasploit::Credential::Public.where(username: public).first.try(:id)
    priv_obj = Metasploit::Credential::Private.where(data: private).first.try(:id)
    realm_obj = Metasploit::Credential::Realm.where(key: realm_key, value: realm_value).first.try(:id)

    core = Metasploit::Credential::Core.where(public_id: pub_obj, private_id: priv_obj, realm_id: realm_obj).first

    # Do nothing else if we have no matching core. Otherwise look for a Login.
    if core.present?
      login = core.logins.joins(service: :host).where(services: { port: port, proto: protocol } ).where( hosts: {address: address}).readonly(false).first

      if login.present?
        login.status = status
        login.last_attempted_at = DateTime.now
        login.save!
      end

    end

  end


  private

  # This method wraps a block in a retry if we get a RecordNotUnique validation error.
  # This helps guard against race conditions.
  def retry_transaction(&block)
    tries = 3
    begin
      yield
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
      tries -= 1
      if tries > 0
        retry
      else
        raise
      end
    end
  end
end