Files
metasploit-gs/spec/modules/auxiliary/admin/kerberos/keytab_spec.rb
T
2025-04-08 12:47:31 +01:00

308 lines
15 KiB
Ruby

require 'rspec'
RSpec.describe 'kerberos keytab' do
include_context 'Msf::UIDriver'
include_context 'Msf::DBManager'
include_context 'Msf::Simple::Framework#modules loading'
let(:subject) do
load_and_create_module(
module_type: 'auxiliary',
reference_name: 'admin/kerberos/keytab'
)
end
=begin
Generated with heimdal ktutil; which has two additional bytes at the end for a 32-bit kvno and flags which are
not present in the mit format
rm -f heimdal.keytab
ktutil --keytab=./heimdal.keytab --verbose add --password=password --principal=Administrator@DOMAIN.LOCAL --enctype=aes256-cts-hmac-sha1-96 --kvno=1
ktutil --keytab=./heimdal.keytab --verbose add --password=password --principal=Administrator@DOMAIN.LOCAL --enctype=aes128-cts-hmac-sha1-96 --kvno=1
ktutil --keytab=./heimdal.keytab --verbose add --password=password --principal=Administrator@DOMAIN.LOCAL --enctype=arcfour-hmac-md5 --kvno=1
ktutil --keytab=./heimdal.keytab --verbose list
ruby -r 'active_support/core_ext/array' -e 'puts File.binread("./heimdal.keytab").bytes.map { |x| "\\x#{x.to_s(16).rjust(2, "0")}" }.in_groups_of(16).map { |row| "\"#{row.join("")}\"" }.join(" \\ \n")'
=end
let(:valid_keytab) do
"\x05\x02\x00\x00\x00\x54\x00\x01\x00\x0c\x44\x4f\x4d\x41\x49\x4e" \
"\x2e\x4c\x4f\x43\x41\x4c\x00\x0d\x41\x64\x6d\x69\x6e\x69\x73\x74" \
"\x72\x61\x74\x6f\x72\x00\x00\x00\x01\x63\x38\x7e\x21\x01\x00\x12" \
"\x00\x20\xc4\xa3\xf3\x1d\x64\xaf\xa6\x48\xa6\xd0\x8d\x07\x76\x56" \
"\x3e\x12\x38\xb9\x76\xd0\xb9\x0f\x79\xea\x07\x21\x94\x36\x82\x94" \
"\xe9\x29\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x44\x00\x01" \
"\x00\x0c\x44\x4f\x4d\x41\x49\x4e\x2e\x4c\x4f\x43\x41\x4c\x00\x0d" \
"\x41\x64\x6d\x69\x6e\x69\x73\x74\x72\x61\x74\x6f\x72\x00\x00\x00" \
"\x01\x63\x38\x7e\x21\x01\x00\x11\x00\x10\xba\xba\x43\xa8\xb9\x7b" \
"\xac\xa1\x53\xbd\x54\xb2\xf0\x77\x4a\xd7\x00\x00\x00\x01\x00\x00" \
"\x00\x00\x00\x00\x00\x44\x00\x01\x00\x0c\x44\x4f\x4d\x41\x49\x4e" \
"\x2e\x4c\x4f\x43\x41\x4c\x00\x0d\x41\x64\x6d\x69\x6e\x69\x73\x74" \
"\x72\x61\x74\x6f\x72\x00\x00\x00\x01\x63\x38\x7e\x21\x01\x00\x17" \
"\x00\x10\x88\x46\xf7\xea\xee\x8f\xb1\x17\xad\x06\xbd\xd8\x30\xb7" \
"\x58\x6c\x00\x00\x00\x01\x00\x00\x00\x00"
end
let(:keytab_file) { Tempfile.new('keytab') }
before(:each) do
Timecop.freeze(Time.parse('Jul 15, 2022 12:33:40.000000000 GMT'))
subject.datastore['VERBOSE'] = false
allow(driver).to receive(:input).and_return(driver_input)
allow(driver).to receive(:output).and_return(driver_output)
subject.init_ui(driver_input, driver_output)
end
after(:each) do
Timecop.return
end
describe '#add_keytab_entry' do
context 'when the keytab file does not exist' do
before(:each) do
File.delete(keytab_file.path)
subject.datastore['KEYTAB_FILE'] = keytab_file.path
end
context 'when supplying a key with aes126 encryption type' do
it 'creates a new keytab' do
subject.datastore['PRINCIPAL'] = 'Administrator'
subject.datastore['REALM'] = 'DOMAIN.LOCAL'
subject.datastore['KVNO'] = 1
subject.datastore['ENCTYPE'] = 'AES256'
subject.datastore['KEY'] = 'c4a3f31d64afa648a6d08d0776563e1238b976d0b90f79ea072194368294e929'
subject.add_keytab_entry
subject.list_keytab_entries
expect(@combined_output.join("\n")).to match_table <<~TABLE
keytab saved to #{keytab_file.path}
Keytab entries
==============
kvno type principal hash date
---- ---- --------- ---- ----
1 18 (AES256) Administrator@DOMAIN.LOCAL c4a3f31d64afa648a6d08d0776563e1238b976d0b90f79ea072194368294e929 #{Time.parse('1970-01-01 00:00:00 +0000').localtime}
TABLE
end
end
context 'when supplying a password with the ALL encryption type specified' do
it 'creates a new keytab' do
subject.datastore['PRINCIPAL'] = 'Administrator'
subject.datastore['REALM'] = 'DOMAIN.LOCAL'
subject.datastore['KVNO'] = 1
subject.datastore['ENCTYPE'] = 'ALL'
subject.datastore['PASSWORD'] = 'password'
subject.add_keytab_entry
subject.list_keytab_entries
expect(@combined_output.join("\n")).to match_table <<~TABLE
keytab saved to #{keytab_file.path}
Keytab entries
==============
kvno type principal hash date
---- ---- --------- ---- ----
1 3 (DES_CBC_MD5) Administrator@DOMAIN.LOCAL 89d3b923d6a7195e #{Time.parse('1970-01-01 00:00:00 +0000').localtime}
1 16 (DES3_CBC_SHA1) Administrator@DOMAIN.LOCAL 341994e0ba5b1a20d640911cda23c137b637d51a6416d6cb #{Time.parse('1970-01-01 00:00:00 +0000').localtime}
1 23 (RC4_HMAC) Administrator@DOMAIN.LOCAL 8846f7eaee8fb117ad06bdd830b7586c #{Time.parse('1970-01-01 00:00:00 +0000').localtime}
1 17 (AES128) Administrator@DOMAIN.LOCAL baba43a8b97baca153bd54b2f0774ad7 #{Time.parse('1970-01-01 00:00:00 +0000').localtime}
1 18 (AES256) Administrator@DOMAIN.LOCAL c4a3f31d64afa648a6d08d0776563e1238b976d0b90f79ea072194368294e929 #{Time.parse('1970-01-01 00:00:00 +0000').localtime}
TABLE
end
end
context 'when supplying a password with aes256 encryption type' do
it 'creates a new keytab' do
subject.datastore['PRINCIPAL'] = 'Administrator'
subject.datastore['REALM'] = 'DOMAIN.LOCAL'
subject.datastore['KVNO'] = 1
subject.datastore['ENCTYPE'] = 'AES256'
subject.datastore['PASSWORD'] = 'password'
subject.add_keytab_entry
subject.list_keytab_entries
expect(@combined_output.join("\n")).to match_table <<~TABLE
keytab saved to #{keytab_file.path}
Keytab entries
==============
kvno type principal hash date
---- ---- --------- ---- ----
1 18 (AES256) Administrator@DOMAIN.LOCAL c4a3f31d64afa648a6d08d0776563e1238b976d0b90f79ea072194368294e929 #{Time.parse('1970-01-01 00:00:00 +0000').localtime}
TABLE
end
end
end
context 'when the keytab file exists' do
before(:each) do
File.binwrite(keytab_file.path, valid_keytab)
subject.datastore['KEYTAB_FILE'] = keytab_file.path
end
context 'when supplying a password with aes256 encryption type' do
it 'updates the existing keytab' do
subject.datastore['PRINCIPAL'] = 'Administrator'
subject.datastore['REALM'] = 'DOMAIN.LOCAL'
subject.datastore['KVNO'] = 1
subject.datastore['ENCTYPE'] = 'AES256'
subject.datastore['PASSWORD'] = 'password'
subject.add_keytab_entry
subject.list_keytab_entries
expect(@combined_output.join("\n")).to match_table <<~TABLE
keytab saved to #{keytab_file.path}
Keytab entries
==============
kvno type principal hash date
---- ---- --------- ---- ----
1 18 (AES256) Administrator@DOMAIN.LOCAL c4a3f31d64afa648a6d08d0776563e1238b976d0b90f79ea072194368294e929 #{Time.parse('2022-10-01 17:51:29 +0000').localtime}
1 17 (AES128) Administrator@DOMAIN.LOCAL baba43a8b97baca153bd54b2f0774ad7 #{Time.parse('2022-10-01 17:51:29 +0000').localtime}
1 23 (RC4_HMAC) Administrator@DOMAIN.LOCAL 8846f7eaee8fb117ad06bdd830b7586c #{Time.parse('2022-10-01 17:51:29 +0000').localtime}
1 18 (AES256) Administrator@DOMAIN.LOCAL c4a3f31d64afa648a6d08d0776563e1238b976d0b90f79ea072194368294e929 #{Time.parse('1970-01-01 00:00:00 +0000').localtime}
TABLE
end
end
end
end
describe '#list_keytab_entries' do
context 'when the keytab file does not exist' do
it 'raises a config error' do
expect { subject.list_keytab_entries }.to raise_error Msf::Auxiliary::Failed, /Invalid key tab file/
end
end
context 'when the keytab file exists' do
before(:each) do
File.binwrite(keytab_file.path, valid_keytab)
subject.datastore['KEYTAB_FILE'] = keytab_file.path
end
it 'lists the available keytab entries' do
subject.list_keytab_entries
expect(@combined_output.join("\n")).to match_table <<~TABLE
Keytab entries
==============
kvno type principal hash date
---- ---- --------- ---- ----
1 18 (AES256) Administrator@DOMAIN.LOCAL c4a3f31d64afa648a6d08d0776563e1238b976d0b90f79ea072194368294e929 #{Time.parse('2022-10-01 17:51:29 +0000').localtime}
1 17 (AES128) Administrator@DOMAIN.LOCAL baba43a8b97baca153bd54b2f0774ad7 #{Time.parse('2022-10-01 17:51:29 +0000').localtime}
1 23 (RC4_HMAC) Administrator@DOMAIN.LOCAL 8846f7eaee8fb117ad06bdd830b7586c #{Time.parse('2022-10-01 17:51:29 +0000').localtime}
TABLE
end
end
end
describe '#export_keytab_entries' do
context 'when the keytab file does not exist' do
before(:each) do
File.delete(keytab_file.path)
subject.datastore['KEYTAB_FILE'] = keytab_file.path
framework.db.delete_credentials(ids: (framework.db.creds || []).map(&:id))
end
after(:each) do
framework.db.delete_credentials(ids: (framework.db.creds || []).map(&:id))
end
context 'when there is no database active' do
before(:each) do
allow(subject.framework.db).to receive(:active).and_return(false)
end
it 'notifies the user that there is no database active' do
subject.export_keytab_entries
expect(@combined_output.join("\n")).to match_table <<~TABLE
export not available, because the database is not active.
TABLE
end
end
context 'when there are no kerberos or ntlm creds present in the database' do
it 'notifies the user that there are no entries to export' do
subject.export_keytab_entries
expect(@combined_output.join("\n")).to match_table <<~TABLE
No entries to export
keytab saved to #{keytab_file.path}
TABLE
end
end
context 'when there are kerberos and ntlm creds present in the database' do
def report_creds(
user, hash, type: :ntlm_hash, jtr_format: '', realm_key: nil, realm_value: nil,
rhost: '192.0.2.2', rport: '445', myworkspace_id: nil, module_fullname: nil
)
service_data = {
address: rhost,
port: rport,
service_name: 'smb',
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
before(:each) do
report_creds(
'user_without_realm', 'aad3b435b51404eeaad3b435b51404ee:e02bc503339d51f71d913c245d35b50b',
type: :ntlm_hash, module_fullname: subject.fullname, myworkspace_id: framework.db.default_workspace.id
)
report_creds(
'user_with_realm', 'aad3b435b51404eeaad3b435b51404ee:32ede47af254546a82b1743953cc4950',
type: :ntlm_hash, module_fullname: subject.fullname, myworkspace_id: framework.db.default_workspace.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_with_krbkey".b,
key: 'c4a3f31d64afa648a6d08d0776563e1238b976d0b90f79ea072194368294e929'
}
report_creds(
'user_with_krbkey', Metasploit::Credential::KrbEncKey.build_data(**krb_key),
type: :krb_enc_key, module_fullname: subject.fullname, myworkspace_id: framework.db.default_workspace.id,
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, realm_value: 'demo.local'
)
end
it 'exports the creds' do
subject.export_keytab_entries
subject.list_keytab_entries
expect(@combined_output.join("\n")).to match_table <<~TABLE
keytab saved to #{keytab_file.path}
Keytab entries
==============
kvno type principal hash date
---- ---- --------- ---- ----
1 23 (RC4_HMAC) user_without_realm@ e02bc503339d51f71d913c245d35b50b #{Time.parse('1970-01-01 01:00:00 +0100').localtime}
1 23 (RC4_HMAC) user_with_realm@example.local 32ede47af254546a82b1743953cc4950 #{Time.parse('1970-01-01 01:00:00 +0100').localtime}
1 18 (AES256) user_with_krbkey@demo.local 63346133663331643634616661363438613664303864303737363536336531323338623937366430623930663739656130373231393433363832393465393239 #{Time.parse('1970-01-01 01:00:00 +0100').localtime}
TABLE
end
end
end
end
end