rapid7/metasploit-credential

View on GitHub
lib/metasploit/credential/importer/pwdump.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# Implements importation behavior for pwdump files exported by Metasploit as well as files from the John the Ripper
# hash cracking suite: http://www.openwall.com/john/
#
# Please note that in the case of data exported from Metasploit, the dataset will contain information on the `Mdm::Host`
# and `Mdm::Service` objects that are related to the credential.  This means that Metasploit exports will be limited to
# containing {Metasploit::Credential::Login} objects, which is the legacy behavior of this export prior to the creation
# of this library.
class Metasploit::Credential::Importer::Pwdump
  include Metasploit::Credential::Importer::Base
  include Metasploit::Credential::Creation

  #
  # Constants
  #

  # Matches a line starting with a '#'
  COMMENT_LINE_START_REGEX     = /^[\s]*#/

  # The string that John the Ripper uses to designate a lack of password in a credentials entry
  JTR_NO_PASSWORD_STRING = "NO PASSWORD"

  # Matches lines that contain usernames and non-SMB hashes
  NONREPLAYABLE_REGEX               = /^[\s]*([\x21-\x7f]+):([\x21-\x7f]+):::/n

  # Matches lines that contain usernames and plaintext passwords
  PLAINTEXT_REGEX                   = /^[\s]*([\x21-\x7f]+)[\s]+([\x21-\x7f]+)?/n

  # Matches lines taht contain MD5 hashes for PostgreSQL
  POSTGRES_REGEX                    = /^[\s]*([\x21-\x7f]+):md5([0-9a-f]{32})$/

  # Matches a line that we use to get information for creating `Mdm::Host` and `Mdm::Service` objects
  # TODO: change to use named groups from 1.9+
  SERVICE_COMMENT_REGEX             = /^#[\s]*([0-9.]+):([0-9]+)(\x2f(tcp|udp))?[\s]*(\x28([^\x29]*)\x29)?/n

  # Matches the way that John the Ripper exports SMB hashes with no password piece
  SMB_WITH_JTR_BLANK_PASSWORD_REGEX = /^[\s]*([^\s:]+):([0-9]+):NO PASSWORD\*+:NO PASSWORD\*+[^\s]*$/

  # Matches LM/NTLM hash format
  SMB_WITH_HASH_REGEX               = /^[\s]*([^\s:]+):[0-9]+:([A-Fa-f0-9]+:[A-Fa-f0-9]+):[^\s]*$/

  # Matches a line with free-form text - less restrictive than {SMB_WITH_HASH_REGEX}
  SMB_WITH_PLAINTEXT_REGEX          = /^[\s]*([^\s:]+):(.+):[A-Fa-f0-9]*:[A-Fa-f0-9]*:::$/

  # Matches warning lines in legacy pwdump files
  WARNING_REGEX                     = /^[\s]*Warning:/

  #
  # Validations
  #

  validates :filename, presence: true

  #
  # Instance Methods
  #

  # Checks a string for matching {Metasploit::Credential::Exporter::Pwdump::BLANK_CRED_STRING} and returns blank string
  # if it matches that constant.
  # @param check_string [String] the string to check
  # @param dehex [Boolean] convert hex to char if true
  # @return [String]
  def blank_or_string(check_string, dehex=false)
    if check_string.blank? || check_string ==  Metasploit::Credential::Exporter::Pwdump::BLANK_CRED_STRING || check_string == JTR_NO_PASSWORD_STRING
      ""
    else
      if dehex
        Metasploit::Credential::Text.dehex check_string
      else
        check_string
      end
    end
  end

  # Perform the import of the credential data, creating `Mdm::Host` and `Mdm::Service` objects as needed,
  # parsing out data by matching against regex constants that match the various kinds of valid lines found
  # in the file.  Ignore lines which match none of the REGEX constants.
  # @return [void]
  def import!
    service_info = nil
    Metasploit::Credential::Core.transaction do
      input.each_line do |line|
        case line
          when WARNING_REGEX
            next
          when COMMENT_LINE_START_REGEX
            service_info = service_info_from_comment_string(line)
          when SMB_WITH_HASH_REGEX
            info = parsed_regex_results($1, $2)
            username, private = info[:username], info[:private]
            creds_class = Metasploit::Credential::NTLMHash
          when SMB_WITH_JTR_BLANK_PASSWORD_REGEX
            info = parsed_regex_results($1, $2)
            username, private = info[:username], info[:private]
            creds_class = Metasploit::Credential::NTLMHash
          when SMB_WITH_PLAINTEXT_REGEX
            info = parsed_regex_results($1, $2)
            username, private = info[:username], info[:private]
            creds_class = Metasploit::Credential::NTLMHash
          when NONREPLAYABLE_REGEX
            info = parsed_regex_results($1, $2)
            username, private = info[:username], info[:private]
            creds_class = Metasploit::Credential::NonreplayableHash
          when POSTGRES_REGEX
            info = parsed_regex_results($1,"md5#{$2}")
            username, private = info[:username], info[:private]
            creds_class = Metasploit::Credential::PostgresMD5
          when PLAINTEXT_REGEX
            info = parsed_regex_results($1, $2, true)
            username, private = info[:username], info[:private]
            creds_class = Metasploit::Credential::Password
          else
            next
        end

        # Skip unless we have enough to make a Login
        if service_info.present?
          if [service_info[:host_address], service_info[:port], username, private].compact.size != 4
            next
          end
        else
          next
        end

        public_obj = create_credential_public(username: username)

        private_obj = creds_class.where(data: private).first_or_create

        core   = create_credential_core(origin: origin, private: private_obj, public: public_obj, workspace_id: workspace.id)

        login_opts = {
          address:      service_info[:host_address],
          port:         service_info[:port],
          protocol:     service_info[:protocol],
          service_name: service_info[:name],
          workspace_id: workspace.id,
          core:         core,
          status: Metasploit::Model::Login::Status::UNTRIED
        }

        create_credential_login(login_opts)
      end
    end
  end

  def initialize(args={})
    super args
  end

  # Break a line into user, hash
  # @param username [String]
  # @param private [String]
  # @param dehex [Boolean] convert hex to char if true
  # @return [Hash]
  def parsed_regex_results(username, private, dehex=false)
    results = {}
    results[:username] = blank_or_string(username, dehex)
    results[:private]  = blank_or_string(private, dehex)

    results
  end

  # Take an msfpwdump comment string and parse it into information necessary for
  # creating `Mdm::Host` and `Mdm::Service` objects.
  # @param comment_string [String] a string starting with a '#' that conforms to {SERVICE_COMMENT_REGEX}
  # @return [Hash]
  def service_info_from_comment_string(comment_string)
    service_info = {}
    if comment_string[SERVICE_COMMENT_REGEX]
      service_info[:host_address]  = $1
      service_info[:port]          = $2
      service_info[:protocol]      = $4.present? ? $4 : "tcp"
      service_info[:name]          = $6
      service_info
    else
      nil
    end
  end

end