diff --git a/app/models/metasploit/credential/postgres_md5.rb b/app/models/metasploit/credential/postgres_md5.rb new file mode 100644 index 00000000..2a850876 --- /dev/null +++ b/app/models/metasploit/credential/postgres_md5.rb @@ -0,0 +1,41 @@ +# A {Metasploit::Credential::PasswordHash password hash} that can be {Metasploit::Credential::ReplayableHash replayed} +# to authenticate to PostgreSQL servers. It is composed of a hexadecimal string of 32 charachters prepended by the string +# 'md5' +class Metasploit::Credential::PostgresMD5 < Metasploit::Credential::ReplayableHash + + DATA_REGEXP = /md5([a-f0-9]{32})/ + + # + # Callbacks + # + + before_validation :normalize_data + + # + # Validations + # + + validate :data_format + + private + + # Normalizes {#data} by making it all lowercase so that the unique validation and index on + # ({Metasploit::Credential::Private#type}, {#data}) catches collision in a case-insensitive manner without the need + # to use case-insensitive comparisons. + def normalize_data + if data + self.data = data.downcase + end + end + + def data_format + unless DATA_REGEXP.match(data) + errors.add(:data, 'is not in Postgres MD5 Hash format') + end + end + + public + + Metasploit::Concern.run(self) + +end \ No newline at end of file diff --git a/lib/metasploit/credential/creation.rb b/lib/metasploit/credential/creation.rb index e3453048..6beceddc 100644 --- a/lib/metasploit/credential/creation.rb +++ b/lib/metasploit/credential/creation.rb @@ -396,6 +396,9 @@ def create_credential_private(opts={}) 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? diff --git a/lib/metasploit/credential/version.rb b/lib/metasploit/credential/version.rb index 0a19cec2..da3eb99b 100644 --- a/lib/metasploit/credential/version.rb +++ b/lib/metasploit/credential/version.rb @@ -7,7 +7,7 @@ module Version # The minor version number, scoped to the {MAJOR} version number. MINOR = 14 # The patch number, scoped to the {MINOR} version number. - PATCH = 0 + PATCH = 1 # The full version string, including the {MAJOR}, {MINOR}, {PATCH}, and optionally, the {PRERELEASE} in the # {http://semver.org/spec/v2.0.0.html semantic versioning v2.0.0} format. diff --git a/spec/factories/metasploit/credential/postgres_md5.rb b/spec/factories/metasploit/credential/postgres_md5.rb new file mode 100644 index 00000000..a2933d45 --- /dev/null +++ b/spec/factories/metasploit/credential/postgres_md5.rb @@ -0,0 +1,11 @@ +FactoryGirl.define do + klass = Metasploit::Credential::PostgresMD5 + + factory :metasploit_credential_postgres_md5, + class: klass, + parent: :metasploit_credential_replayable_hash do + data { + "md5#{SecureRandom.hex(16)}" + } + end +end diff --git a/spec/models/metasploit/credential/postgres_md5_spec.rb b/spec/models/metasploit/credential/postgres_md5_spec.rb new file mode 100644 index 00000000..ee1e60da --- /dev/null +++ b/spec/models/metasploit/credential/postgres_md5_spec.rb @@ -0,0 +1,124 @@ +require 'spec_helper' + +describe Metasploit::Credential::PostgresMD5 do + it_should_behave_like 'Metasploit::Concern.run' + + it { should be_a Metasploit::Credential::ReplayableHash } + + context 'CONSTANTS' do + context 'DATA_REGEXP' do + subject(:data_regexp) do + described_class::DATA_REGEXP + end + + it 'is valid if the string is md5 and 32 hex chars' do + hash = "md5#{SecureRandom.hex(16)}" + expect(data_regexp).to match(hash) + end + + it 'is not valid if it does not start with md5' do + expect(data_regexp).not_to match(SecureRandom.hex(16)) + end + + it 'is not valid for an invalid length' do + expect(data_regexp).not_to match(SecureRandom.hex(6)) + end + + it 'is not valid if it is not hex chars after the md5 tag' do + bogus = "md5#{SecureRandom.hex(15)}jk" + expect(data_regexp).not_to match(bogus) + end + + end + end + + context 'callbacks' do + context 'before_validation' do + context '#data' do + subject(:data) do + postgres_md5.data + end + + let(:postgres_md5) do + FactoryGirl.build( + :metasploit_credential_postgres_md5, + data: given_data + ) + end + + before(:each) do + postgres_md5.valid? + end + + context 'with nil' do + let(:given_data) do + nil + end + + it { should be_nil } + end + + context 'with upper case characters' do + let(:given_data) do + 'ABCDEF1234567890' + end + + it 'makes them lower case' do + expect(data).to eq(given_data.downcase) + end + end + + context 'with all lower case characters' do + let(:given_data) do + 'abcdef1234567890' + end + + it 'does not change the case' do + expect(data).to eq(given_data) + end + end + end + end + end + + context 'factories' do + context 'metasploit_credential_ntlm_hash' do + subject(:metasploit_credential_postgres_md5) do + FactoryGirl.build(:metasploit_credential_postgres_md5) + end + + it { should be_valid } + end + end + + context 'validations' do + context '#data_format' do + subject(:data_errors) do + postgres_md5.errors[:data] + end + + let(:data) { "md5#{SecureRandom.hex(16)}" } + + let(:postgres_md5) do + FactoryGirl.build( + :metasploit_credential_postgres_md5, + data: data + ) + end + + context 'with a valid postgres md5 hash' do + it 'should be valid' do + expect(postgres_md5).to be_valid + end + end + + context 'with an invalid postgres md5 hash' do + let(:data) { "invalidstring" } + it 'should not be valid' do + expect(postgres_md5).to_not be_valid + end + end + end + end + +end