Skip to content

Commit

Permalink
Land #169, Add pkcs12 private data type
Browse files Browse the repository at this point in the history
  • Loading branch information
adfoster-r7 committed Apr 11, 2023
2 parents 9aff6c2 + 57437d6 commit a00c341
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 3 deletions.
84 changes: 84 additions & 0 deletions app/models/metasploit/credential/pkcs12.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
require 'openssl'
require 'base64'

# A private Pkcs12 file.
class Metasploit::Credential::Pkcs12 < Metasploit::Credential::Private
#
# Attributes
#

# @!attribute data
# A private pkcs12 file, base64 encoded - i.e. starting with 'MIIMhgIBAzCCDFAGCSqGSIb3DQEHAaCC....'
#
# @return [String]

#
#
# Validations
#
#

#
# Attribute Validations
#

validates :data,
presence: true
#
# Method Validations
#

validate :readable

#
# Instance Methods
#

# Converts the private pkcs12 data in {#data} to an `OpenSSL::PKCS12` instance.
#
# @return [OpenSSL::PKCS12]
# @raise [ArgumentError] if {#data} cannot be loaded
def openssl_pkcs12
if data
begin
password = ''
OpenSSL::PKCS12.new(Base64.strict_decode64(data), password)
rescue OpenSSL::PKCS12::PKCS12Error => error
raise ArgumentError.new(error)
end
end
end

# The {#data key data}'s fingerprint, suitable for displaying to the
# user.
#
# @return [String]
def to_s
return '' unless data

cert = openssl_pkcs12.certificate
result = []
result << "subject:#{cert.subject.to_s}"
result << "issuer:#{cert.issuer.to_s}"
result.join(',')
end

private

#
# Validates that {#data} can be read by OpenSSL and a `OpenSSL::PKCS12` can be created from {#data}. Any exception
# raised will be reported as a validation error.
#
# @return [void]
def readable
if data
begin
openssl_pkcs12
rescue => error
errors.add(:data, "#{error.class} #{error}")
end
end
end

Metasploit::Concern.run(self)
end
1 change: 1 addition & 0 deletions app/models/metasploit/credential/private.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class Metasploit::Credential::Private < ApplicationRecord
Metasploit::Credential::Password
Metasploit::Credential::SSHKey
Metasploit::Credential::KrbEncKey
Metasploit::Credential::Pkcs12
}

#
Expand Down
7 changes: 6 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ en:
metasploit/credential/ntlm_hash: "NTLM hash"
metasploit/credential/ssh_key: "SSH key"
metasploit/credential/krb_enc_key: 'Krb enc key'
metasploit/credential/pkcs12: 'Pkcs12 (pfx)'
errors:
models:
metasploit/credential/core:
Expand All @@ -83,10 +84,14 @@ en:
attributes:
data:
format: "is not in the KrbEncKey data format of 'msf_krbenckey:<ENCTYPE>:<KEY>:<SALT>', where the key and salt are in hexadecimal characters"
metasploit/credential/pkcs12:
attributes:
data:
format: "is not a Base64 encoded pkcs12 file without a password"
metasploit/credential/ssh_key:
attributes:
data:
encrypted: "is encrypted, but Metasploit::Credential::SSHKey only supports unencrypred private keys."
encrypted: "is encrypted, but Metasploit::Credential::SSHKey only supports unencrypted private keys."
not_private: "is not a private key."
errors:
messages:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class CreateIndexOnPrivateDataAndTypeForPkcs12 < ActiveRecord::Migration[6.1]
def up
# Drop the existing index created by 20161107153145_recreate_index_on_private_data_and_type.rb, and recreate it
# with Metasploit::Credential::Pkcs12 ignored
remove_index :metasploit_credential_privates, [:type, :data], if_exists: true
change_table :metasploit_credential_privates do |t|
t.index [:type, :data],
unique: true,
where: "NOT (type = 'Metasploit::Credential::SSHKey' or type = 'Metasploit::Credential::Pkcs12')"
end

# Create a new index similar to 20161107203710_create_index_on_private_data_and_type_for_ssh_key.rb
sql = <<~EOF
CREATE UNIQUE INDEX IF NOT EXISTS "index_metasploit_credential_privates_on_type_and_data_pkcs12" ON
"metasploit_credential_privates" ("type", decode(md5(data), 'hex'))
WHERE type in ('Metasploit::Credential::Pkcs12')
EOF
execute(sql)
end

def down
# Restore the original metasploit_credential_privates index from /Users/adfoster/Documents/code/metasploit-credential/db/migrate/20161107153145_recreate_index_on_private_data_and_type.rb
# XXX: this would crash if there are any Pkcs12 entries present, so for the simplicity of avoiding a data migration we keep the pkcs12 type ommitted from the index
remove_index :metasploit_credential_privates, [:type, :data], if_exists: true
change_table :metasploit_credential_privates do |t|
t.index [:type, :data],
unique: true,
where: "NOT (type = 'Metasploit::Credential::SSHKey' or type = 'Metasploit::Credential::Pkcs12')"
end
remove_index :metasploit_credential_privates, name: :index_metasploit_credential_privates_on_type_and_data_pkcs12, if_exists: true
end
end
1 change: 1 addition & 0 deletions lib/metasploit/credential.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module Credential
autoload :Origin
autoload :Password
autoload :PasswordHash
autoload :Pkcs12
autoload :PostgresMD5
autoload :Private
autoload :Public
Expand Down
2 changes: 2 additions & 0 deletions lib/metasploit/credential/creation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,8 @@ def create_credential_private(opts={})
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
Expand Down
37 changes: 37 additions & 0 deletions spec/factories/metasploit/credential/pkcs12.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
FactoryBot.define do
factory :metasploit_credential_pkcs12,
class: Metasploit::Credential::Pkcs12 do
transient do
# key size tuned for speed. DO NOT use for production, it is below current recommended key size of 2048
key_size { 1024 }
# signing algorithm for the pkcs12 cert
signing_algorithm { 'SHA256' }
# the cert subject
subject { '/C=BE/O=Test/OU=Test/CN=Test' }
# the cert issuer
issuer { '/C=BE/O=Test/OU=Test/CN=Test' }
end

data {
password = ''
pkcs12_name = ''

private_key = OpenSSL::PKey::RSA.new(key_size)
public_key = private_key.public_key

cert = OpenSSL::X509::Certificate.new
cert.subject = OpenSSL::X509::Name.parse(subject)
cert.issuer = OpenSSL::X509::Name.parse(issuer)
cert.not_before = Time.now
cert.not_after = Time.now + 365 * 24 * 60 * 60
cert.public_key = public_key
cert.serial = 0x0
cert.version = 2
cert.sign(private_key, OpenSSL::Digest.new(signing_algorithm))

pkcs12 = OpenSSL::PKCS12.create(password, pkcs12_name, private_key, cert)
pkcs12_base64 = Base64.strict_encode64(pkcs12.to_der)
pkcs12_base64
}
end
end
24 changes: 22 additions & 2 deletions spec/lib/metasploit/credential/creation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,20 +134,30 @@
nonreplayable_hash: "Metasploit::Credential::NonreplayableHash",
ntlm_hash: "Metasploit::Credential::NTLMHash",
postgres_md5: "Metasploit::Credential::PostgresMD5",
ssh_key: "Metasploit::Credential::SSHKey"
ssh_key: "Metasploit::Credential::SSHKey",
krb_enc_key: "Metasploit::Credential::KrbEncKey",
pkcs12: "Metasploit::Credential::Pkcs12"
}.each_pair do |private_type, public_class|
context "Origin[manual], Public[Username], Private[#{private_type}]" do
let(:ssh_key) {
key_class = OpenSSL::PKey.const_get(:RSA)
key_class.generate(512).to_s
}
let(:krb_enc_key) {
FactoryBot.build(:metasploit_credential_krb_enc_key).data
}
let(:pkcs12) {
FactoryBot.build(:metasploit_credential_pkcs12).data
}
let(:private_data) { {
password: 'password',
blank_password: '',
nonreplayable_hash: '435ba65d2e46d35bc656086694868d1ab2c0f9fd',
ntlm_hash: 'aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0',
postgres_md5: 'md5ac4bbe016b808c3c0b816981f240dcae',
ssh_key: ssh_key
ssh_key: ssh_key,
krb_enc_key: krb_enc_key,
pkcs12: pkcs12
}}
let(:credential_data) {{
workspace_id: workspace.id,
Expand Down Expand Up @@ -821,6 +831,16 @@
expect{ test_object.create_credential_private(opts) }.to change{ Metasploit::Credential::KrbEncKey.count }.by(1)
end
end

context 'when :private_type is pkcs12' do
it 'creates a Metasploit::Credential::Pkcs12' do
opts = {
private_data: FactoryBot.build(:metasploit_credential_pkcs12).data,
private_type: :pkcs12
}
expect{ test_object.create_credential_private(opts) }.to change{ Metasploit::Credential::Pkcs12.count }.by(1)
end
end
end

context '#create_credential_core' do
Expand Down
116 changes: 116 additions & 0 deletions spec/models/metasploit/credential/pkcs12_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
RSpec.describe Metasploit::Credential::Pkcs12, type: :model do
it_should_behave_like 'Metasploit::Concern.run'

context 'factories' do
context 'metasploit_credential_pkcs12' do
subject(:metasploit_credential_pkcs12) do
FactoryBot.build(:metasploit_credential_pkcs12)
end

it { is_expected.to be_valid }
end
end

context 'validations' do
it { is_expected.to validate_presence_of :data }

context 'on #data' do
subject(:data_errors) do
pkcs12.errors[:data]
end

let(:pkcs12) do
FactoryBot.build(:metasploit_credential_pkcs12)
end

context '#readable' do
context 'with #data' do
context 'with error' do
#
# Shared Examples
#

shared_examples_for 'exception' do
it 'includes error class' do
exception_class_name = exception.class.to_s
expect(
data_errors.any? { |error|
error.include? exception_class_name
}
).to be true
end

it 'includes error message' do
exception_message = exception.to_s

expect(
data_errors.any? { |error|
error.include? exception_message
}
).to be true
end
end

#
# Callbacks
#

before(:example) do
expect(pkcs12).to receive(:openssl_pkcs12).and_raise(exception)

pkcs12.valid?
end

context 'with ArgumentError' do
let(:exception) do
ArgumentError.new("Bad Argument")
end

it_should_behave_like 'exception'
end

context 'with OpenSSL::PKCS12::PKCS12Error' do
let(:exception) do
OpenSSL::PKCS12::PKCS12Error.new('mac verify failure')
end

it_should_behave_like 'exception'
end
end

context 'without error' do
before(:example) do
pkcs12.valid?
end

it { is_expected.to be_empty }
end
end

context 'without #data' do
let(:error) do
I18n.translate!('errors.messages.blank')
end

#
# Callbacks
#

before(:example) do
pkcs12.data = nil

pkcs12.valid?
end

it { is_expected.to include(error) }
end
end
end
end

describe 'human name' do
it 'properly determines the model\'s human name' do
expect(described_class.model_name.human).to eq('Pkcs12 (pfx)')
end
end
end
1 change: 1 addition & 0 deletions spec/models/metasploit/credential/private_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
Metasploit::Credential::Password
Metasploit::Credential::SSHKey
Metasploit::Credential::KrbEncKey
Metasploit::Credential::Pkcs12
}
end
end
Expand Down

0 comments on commit a00c341

Please sign in to comment.