Private/Get-KrbProtocolCatalog.ps1
|
#Requires -Version 7.4 function Get-KrbProtocolCatalog { <# .SYNOPSIS Returns reference tables for Kerberos result codes, pre-authentication types and ticket options .DESCRIPTION Supplies the non-cipher lookup tables the event decoder needs: - KdcStatus: RFC 4120 result codes as written into the Status field of events 4768, 4769 and 4771. The one this module cares most about is 0x0E, KDC_ERR_ETYPE_NOTSUPP, which is the exact error a client receives when it offers no encryption type the KDC will accept. After a hardening change, a rise in 4771 events carrying 0x0E is the breakage, observed directly. - PreAuthType: RFC 4120 and MS-KILE padata types from the PreAuthType field. Relevant here because type 0 - no pre-authentication - and type 2 - encrypted timestamp - have different exposure to etype removal from the PKINIT and FAST types. - TicketOption: KDC option bit flags from the TicketOptions field. Included so a report can distinguish a delegation or renewal request from an ordinary one when explaining why a particular principal appears more often than expected. Cached per session in the same manner as Get-KrbEtypeCatalog, and equally free of any AD or event-log dependency. .EXAMPLE PS> (Get-KrbProtocolCatalog).KdcStatus[0x0E] DESCRIPTION: Resolves the result code that indicates encryption type negotiation failure OUTPUT: A record naming KDC_ERR_ETYPE_NOTSUPP USE CASE: Detecting post-hardening breakage in 4771 events .OUTPUTS System.Collections.Hashtable .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ TROUBLESHOOTING: - Interpreting KDC result codes: .\Troubleshooting\Common\KDC-Status-Codes.md .LINK https://www.rfc-editor.org/rfc/rfc4120#section-7.5.9 #> [CmdletBinding()] [OutputType([hashtable])] param() if ($script:KrbProtocolCatalog) { return $script:KrbProtocolCatalog } Write-Verbose 'Building Kerberos protocol catalog (first call this session)' # Row builders, so each entry stays inside the 115-character line limit. The alternative # is a table 200 characters wide that the quality gate rejects and that cannot be read in # a side-by-side diff. $status = { param([string]$Name, [string]$Description, [bool]$EtypeRelated = $false) @{ Name = $Name; Description = $Description; IsEtypeRelated = $EtypeRelated } } $padata = { param([string]$Name, [string]$Description) @{ Name = $Name; Description = $Description } } # IsEtypeRelated marks the codes that an encryption-type change can plausibly cause. # It is what lets a post-change report separate "these failures are our doing" from the # constant background of expired passwords and clock skew that every domain produces. $kdcStatus = @{ 0x00 = & $status 'KDC_ERR_NONE' 'No error - request succeeded' 0x01 = & $status 'KDC_ERR_NAME_EXP' 'Client principal entry has expired' 0x02 = & $status 'KDC_ERR_SERVICE_EXP' 'Server principal entry has expired' 0x06 = & $status 'KDC_ERR_C_PRINCIPAL_UNKNOWN' 'Client principal not found in the database' 0x07 = & $status 'KDC_ERR_S_PRINCIPAL_UNKNOWN' ( 'Service principal not found - usually a missing or duplicate SPN') 0x08 = & $status 'KDC_ERR_PRINCIPAL_NOT_UNIQUE' ( 'Multiple principal entries match - duplicate SPN') # NULL_KEY is etype-related in a way that surprises people: it is what a KDC returns # when the account has no key of a usable type, which is exactly the state an account # reaches when it is set to AES-only but its password has not been changed since, so # no AES key was ever derived. 0x09 = & $status 'KDC_ERR_NULL_KEY' ( 'Account has no key of a usable type - commonly an AES-only account whose ' + 'password predates the change') $true 0x0A = & $status 'KDC_ERR_CANNOT_POSTDATE' 'Ticket is not eligible for postdating' 0x0C = & $status 'KDC_ERR_POLICY' ( 'Rejected by policy - logon hours, workstation restriction, or authentication policy') 0x0D = & $status 'KDC_ERR_BADOPTION' ( 'Requested option is not honoured - often unconstrained delegation being refused') # The headline code for this module. 0x0E = & $status 'KDC_ERR_ETYPE_NOTSUPP' ( 'KDC has no encryption type in common with the client - the direct signature ' + 'of etype hardening breakage') $true 0x0F = & $status 'KDC_ERR_SUMTYPE_NOSUPP' ( 'KDC has no checksum type in common with the client') $true 0x10 = & $status 'KDC_ERR_PADATA_TYPE_NOSUPP' ( 'KDC does not support the pre-authentication type offered - seen when PKINIT ' + 'or FAST is unavailable') $true 0x11 = & $status 'KDC_ERR_TRTYPE_NOSUPP' 'KDC does not support the transited type' 0x12 = & $status 'KDC_ERR_CLIENT_REVOKED' 'Account is disabled, locked out, or expired' 0x13 = & $status 'KDC_ERR_SERVICE_REVOKED' 'Service account credentials have been revoked' 0x17 = & $status 'KDC_ERR_KEY_EXPIRED' 'Password has expired' 0x18 = & $status 'KDC_ERR_PREAUTH_FAILED' ( 'Pre-authentication failed - normally a wrong password') 0x19 = & $status 'KDC_ERR_PREAUTH_REQUIRED' ( 'Additional pre-authentication required - a normal first-leg response, not a failure') 0x1A = & $status 'KDC_ERR_SERVER_NOMATCH' 'KDC does not know about the requested server' 0x1B = & $status 'KDC_ERR_MUST_USE_USER2USER' ( 'Server principal requires user-to-user Kerberos') 0x1F = & $status 'KRB_AP_ERR_BAD_INTEGRITY' ( 'Integrity check on the decrypted field failed - can follow a key mismatch ' + 'after an etype change') $true 0x20 = & $status 'KRB_AP_ERR_TKT_EXPIRED' 'Ticket has expired' 0x21 = & $status 'KRB_AP_ERR_TKT_NYV' 'Ticket is not yet valid' 0x22 = & $status 'KRB_AP_ERR_REPEAT' 'Request is a replay' 0x25 = & $status 'KRB_AP_ERR_SKEW' 'Clock skew between client and KDC is too great' 0x29 = & $status 'KRB_AP_ERR_MODIFIED' ( 'Message stream modified - frequently a key mismatch, including one caused ' + 'by an etype change') $true 0x34 = & $status 'KRB_ERR_RESPONSE_TOO_BIG' ( 'Response too large for UDP - the client should retry over TCP') 0x3C = & $status 'KRB_ERR_GENERIC' 'Generic error' } # The Status field does not only carry RFC 4120 Kerberos result codes. Windows also writes # NTSTATUS values into it, and nothing in the field distinguishes the two - they simply do # not collide, because Kerberos codes are small positive integers and NTSTATUS failure # values all begin 0xC0000000, which reads as a large negative int32. # # This was found by a live cross-forest lab. Four 4769 events carried 0xC000019B, the # catalog had no entry above 0x3C, and StatusName came back null - caught by the suite's # own assertion that every status from a real KDC must resolve to a name. Until a trust # existed in the domain, no status in this range had ever been produced. # # Keys are the signed int32 the converter yields, which is what PowerShell's own 8-digit # hex literals evaluate to, so 0xC000019B here and the decoded -1073741413 are the same key. $ntStatus = @{ 0xC0000064 = & $status 'STATUS_NO_SUCH_USER' 'The named user does not exist' 0xC000006A = & $status 'STATUS_WRONG_PASSWORD' 'Incorrect password' 0xC000006D = & $status 'STATUS_LOGON_FAILURE' 'Logon failure - bad user name or password' 0xC000006E = & $status 'STATUS_ACCOUNT_RESTRICTION' ( 'Account restriction prevented logon - logon hours, workstation, or policy') 0xC000006F = & $status 'STATUS_INVALID_LOGON_HOURS' 'Logon outside permitted hours' 0xC0000070 = & $status 'STATUS_INVALID_WORKSTATION' 'Logon from a workstation not permitted' 0xC0000071 = & $status 'STATUS_PASSWORD_EXPIRED' 'Password has expired' 0xC0000072 = & $status 'STATUS_ACCOUNT_DISABLED' 'Account is disabled' 0xC00000DC = & $status 'STATUS_INVALID_SERVER_STATE' ( 'Server is not in a valid state to service the request') 0xC0000133 = & $status 'STATUS_TIME_DIFFERENCE_AT_DC' 'Clock skew between the client and the controller' 0xC000018B = & $status 'STATUS_NO_TRUST_SAM_ACCOUNT' ( 'The trust account for this domain does not exist or is unusable') # Marked encryption-type related deliberately. A trust whose supported encryption # types do not intersect with the requesting realm's fails here, which is exactly the # cross-realm RC4 condition KRB014 exists to warn about - so this code needs to reach # the post-change failure view rather than being filtered out as background noise. 0xC000019B = & $status 'STATUS_TRUSTED_DOMAIN_FAILURE' ( 'The trust relationship with the target domain failed - can follow a trust whose ' + 'supported encryption types do not intersect with the requesting realm') $true 0xC000019C = & $status 'STATUS_TRUSTED_RELATIONSHIP_FAILURE' ( 'The trust relationship between this workstation and the domain failed') 0xC0000192 = & $status 'STATUS_NETLOGON_NOT_STARTED' 'The Netlogon service was not started' 0xC0000193 = & $status 'STATUS_ACCOUNT_EXPIRED' 'Account has expired' 0xC0000224 = & $status 'STATUS_PASSWORD_MUST_CHANGE' 'The password must be changed at next logon' 0xC0000234 = & $status 'STATUS_ACCOUNT_LOCKED_OUT' 'Account is locked out' } foreach ($code in $ntStatus.Keys) { $kdcStatus[$code] = $ntStatus[$code] } $preAuthType = @{ 0 = & $padata 'None' ( 'No pre-authentication - the account has "Do not require Kerberos ' + 'preauthentication" set, which is also the AS-REP roasting exposure') 2 = & $padata 'PA-ENC-TIMESTAMP' ( 'Encrypted timestamp - the standard pre-authentication method, and the one ' + 'whose encryption type this module tracks') 11 = & $padata 'PA-ETYPE-INFO' 'Encryption type information supplied by the KDC' 15 = & $padata 'PA-PK-AS-REP_OLD' 'Smart card logon, legacy PKINIT' 16 = & $padata 'PA-PK-AS-REQ' 'Smart card logon, PKINIT request' 17 = & $padata 'PA-PK-AS-REP' 'Smart card logon, PKINIT reply' 19 = & $padata 'PA-ETYPE-INFO2' ( 'Encryption type information, version 2 - carries salt and s2kparams') 20 = & $padata 'PA-SVR-REFERRAL-INFO' 'Cross-realm referral information' 138 = & $padata 'PA-ENCRYPTED-CHALLENGE' 'FAST armoured pre-authentication' } # KDC option bits, as carried in the TicketOptions field. Only the ones worth naming in a # report are listed; the field is reported in full as hex regardless. $ticketOption = @{ 0x40000000 = 'Forwardable' 0x20000000 = 'Forwarded' 0x10000000 = 'Proxiable' 0x08000000 = 'Proxy' 0x04000000 = 'Allow-Postdate' 0x02000000 = 'Postdated' 0x00800000 = 'Renewable' 0x00010000 = 'Canonicalize' 0x00008000 = 'Request-Anonymous' 0x00000010 = 'Renewable-OK' 0x00000008 = 'Enc-Tkt-in-Skey' 0x00000002 = 'Renew' 0x00000001 = 'Validate' } $script:KrbProtocolCatalog = @{ KdcStatus = $kdcStatus PreAuthType = $preAuthType TicketOption = $ticketOption # Descending order, precomputed. Sorting thirteen keys is trivial in isolation, but # the ticket options are decoded on every event, and a Sort-Object pipeline per event # is a measurable share of a multi-hundred-thousand-event collection. SortedTicketOptionKeys = [int[]]@($ticketOption.Keys | Sort-Object -Descending) } return $script:KrbProtocolCatalog } |