Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pkcs12 private data type #169

Merged
merged 2 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(',')
adfoster-r7 marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @jmartin-r7 - I Believe we sync'd up on this and we were happy with this approach

Is this a PR that we could merge in ahead of time before going live with 6.3 to derisk things? Or do you think it would be safe enough to merge together with the Kerberos effort

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to do a scaling test but should be reasonable to land early.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With ~25k entries the migration takes no time at all:

-- remove_index(:metasploit_credential_privates, [:type, :data], {:if_exists=>true})
   -> 0.0080s
-- change_table(:metasploit_credential_privates)
   -> 0.5110s
-- remove_index(:metasploit_credential_privates, {:name=>:index_metasploit_credential_privates_on_type_and_data_pkcs12, :if_exists=>true})
   -> 0.0043s

That seems good to me to go ahead with, unless the creds number isn't high enough and we want a second verification with a larger number of creds

Count:

=> select count(*) from metasploit_credential_privates;
24665

Groupings:

=> select type, count(*) from metasploit_credential_privates group by type;

type                              | count
--                                | --
Metasploit::Credential::NTLMHash  | 12341
Metasploit::Credential::KrbEncKey | 6168
Metasploit::Credential::SSHKey    | 6155
Metasploit::Credential::Password  | 1

Created with a quick rc file:

<ruby>
def report_creds(
  user, hash, type: :ntlm_hash, jtr_format: '', realm_key: nil, realm_value: nil,
  rhost: nil, service_name: 'smb', rport: '445', myworkspace_id: nil, module_fullname: nil
)
  rhost ||= "192.168.#{rand(5..240)}.#{rand(5..240)}"
  service_data = {
    address: rhost,
    port: rport,
    service_name: service_name,
    protocol: 'tcp',
    workspace_id: myworkspace_id
  }
  credential_data = {
    module_fullname: module_fullname,
    origin_type: :service,
    private_data: hash,
    private_type: type,
    jtr_format: jtr_format,
    username: user
  }.merge(service_data)
  credential_data[:realm_key] = realm_key if realm_key
  credential_data[:realm_value] = realm_value if realm_value

  cl = framework.db.create_credential_and_login(credential_data)
  cl.respond_to?(:core_id) ? cl.core_id : nil
end

require 'securerandom'

myworkspace_id = framework.db.default_workspace.id
module_fullname = 'exploit/multi/http/gitlab_file_read_rce'

def rand_crypto(char_len)
    SecureRandom.hex(char_len)
end

(1..2500).each do |i|
    $stderr.puts "#{i}"
    report_creds(
        "user_#{i}_without_realm",
        "aad3b435b51404eeaad3b435b51404ee:#{rand_crypto(16)}",
        type: :ntlm_hash, module_fullname: module_fullname, myworkspace_id: myworkspace_id
    )
    report_creds(
        "user#{i}_with_realm", "aad3b435b51404eeaad3b435b51404ee:#{rand_crypto(16)}",
        type: :ntlm_hash, module_fullname: module_fullname, myworkspace_id: myworkspace_id,
        realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, realm_value: 'example.local'
    )
    krb_key = {
        enctype: Rex::Proto::Kerberos::Crypto::Encryption::AES256,
        salt: "DEMO.LOCALuser_#{i}_with_krbkey".b,
        key: rand_crypto(64)
    }
    report_creds(
        "user_#{i}_with_realm", Metasploit::Credential::KrbEncKey.build_data(**krb_key),
        type: :krb_enc_key, module_fullname: module_fullname, myworkspace_id: myworkspace_id,
        realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, realm_value: 'demo.local'
    )

   ssh_key = OpenSSL::PKey::RSA.generate(1024).to_s
   report_creds(
       "user_#{i}_with_realm", ssh_key,
       type: :ssh_key, module_fullname: module_fullname, myworkspace_id: myworkspace_id,
       realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, realm_value: 'demo.local',
       rport: 22,
       service_name: 'ssh'
   )
end
</ruby>

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