SEKurity GmbH Logo
adPEAS

adPEAS v2 Episode 8: PAC Deep-Dive & Ticket Forging - What's Inside the Ticket

Deep dive into the PAC structure, NDR serialization, PAC checksums, and how adPEAS v2 forges Golden, Silver, and Diamond Tickets step by step.

Alexander Sturz

Founder & Red Team Lead

25 min read
Share:

Introduction

In Episode 7, we looked at how Kerberos messages are structured, how encryption works, and what lives inside the EncTicketPart. Along the way, we discovered a field at the end: authorization-data with ad-type 128 — the PAC. And that’s where things get really interesting.

The PAC (Privilege Attribute Certificate) is the reason Golden, Silver, and Diamond Tickets work. It contains the user’s identity information — groups, SIDs, privileges. And it’s the part we manipulate during Ticket Forging.

Prerequisites: You should have read Episode 7. You’ll need an understanding of ASN.1 DER, the EncTicketPart structure, and how AES-CTS/RC4-HMAC encryption works.


Part 1: PAC Architecture

What is the PAC?

The PAC is a Microsoft extension to the Kerberos protocol (MS-PAC). Pure MIT Kerberos has no PAC. In Active Directory, the PAC contains the Windows-specific authorization data for a user — everything the target service needs to make access control decisions.

EncTicketPart
  +-- authorization-data [10]
      +-- AD-IF-RELEVANT (type 1)
          +-- AD-WIN2K-PAC (type 128)
              +-- PACTYPE (the actual PAC)
                  |-- Buffer 1: LOGON_INFO
                  |-- Buffer 2: CLIENT_INFO
                  |-- Buffer 3: UPN_DNS_INFO
                  |-- Buffer 4: PAC_ATTRIBUTES
                  |-- Buffer 5: PAC_REQUESTOR
                  |-- Buffer 6: SERVER_CHECKSUM
                  +-- Buffer 7: KDC_CHECKSUM

PACTYPE Structure

The PAC is NOT ASN.1 — it’s a proprietary Microsoft format:

PACTYPE Header:
+----------------------------------------------------+
|  cBuffers (4 Bytes) : Number of PAC buffers        |
|  Version  (4 Bytes) : 0x00000000                   |
+----------------------------------------------------+
|  PAC_INFO_BUFFER[0]:                               |
|    ulType  (4 Bytes) : Buffer type (1, 6, 7, etc.) |
|    cbBufferSize (4 Bytes) : Size of the buffer     |
|    Offset  (8 Bytes) : Offset in the PAC blob      |
|                                                    |
|  PAC_INFO_BUFFER[1]:                               |
|    ulType  (4 Bytes) : ...                         |
|    cbBufferSize (4 Bytes) : ...                    |
|    Offset  (8 Bytes) : ...                         |
|                                                    |
|  ... (repeated for each buffer)                    |
+----------------------------------------------------+
|  Buffer data (at the respective offsets):          |
|    [LOGON_INFO Bytes]                              |
|    [CLIENT_INFO Bytes]                             |
|    [UPN_DNS_INFO Bytes]                            |
|    [PAC_ATTRIBUTES Bytes]                          |
|    [PAC_REQUESTOR Bytes]                           |
|    [SERVER_CHECKSUM Bytes]                         |
|    [KDC_CHECKSUM Bytes]                            |
+----------------------------------------------------+

Each buffer is aligned to 8 bytes. The order of buffers in the PAC_INFO_BUFFER table can vary; the offsets point to the correct data.

The 7 Required PAC Buffers

Since the CVE-2021-42287 patches, Windows Server 2022+ enforces exactly these buffers:

TypeNameDescriptionSize
1LOGON_INFOUser identity, groups, domain SID~500-2000 Bytes
6SERVER_CHECKSUMHMAC signature over the entire PAC20-28 Bytes
7KDC_CHECKSUMHMAC signature over the server checksum20-28 Bytes
10CLIENT_INFOClient name + auth timestamp~30-60 Bytes
12UPN_DNS_INFOUPN, DNS domain, SamName, SID~100-200 Bytes
17PAC_ATTRIBUTESFlags (PAC_WAS_REQUESTED)8 Bytes
18PAC_REQUESTORUser SID (raw)~28 Bytes

An additional buffer generated by the KDC during PKINIT authentication:

TypeNameDescriptionSize
2CREDENTIAL_INFOEncrypted NTLM credentials (PKINIT only)~100-200 Bytes

This buffer contains the user’s NT hash, encrypted with the DH-derived AS-REP Reply Key (KeyUsage 16). It is only included during PKINIT authentication because the client does not possess a symmetric key. adPEAS uses this for UnPAC-the-Hash — automatic NT hash recovery after PKINIT (see Episode 3).

Two additional buffers that the KDC normally generates, but which must not be included during Ticket Forging:

TypeNameWhy leave them out?
16TICKET_CHECKSUMOnly computable by the KDC (requires the krbtgt key of the respective TGS)
19FULL_PAC_CHECKSUMOnly computable by the KDC

adPEAS’ Build-PAC doesn’t include these buffers at all. If they were present (e.g., during in-place PAC patching), the KDC would reject the ticket because it cannot validate the checksums.


Part 2: LOGON_INFO — The User Identity (NDR Format)

Why NDR?

The LOGON_INFO buffer contains a KERB_VALIDATION_INFO structure. This is serialized in the NDR (Network Data Representation) format — not ASN.1, not JSON, not Protocol Buffers, but the serialization format from DCE/RPC dating back to the 90s.

NDR is… special. It has:

  • Referent IDs for pointers
  • Padding rules that depend on the position within the buffer
  • Strings as RPC_UNICODE_STRING with separate referents
  • Conformant arrays with a prepended MaxCount

NDR Type Serialization Header

Every NDR-serialized type in a PAC buffer starts with a header:

NDR Header (20 Bytes):
+------------------------------------------------------+
|  Common Header (8 Bytes):                            |
|    Version     (1 Byte)  : 0x01                      |
|    Endianness  (1 Byte)  : 0x10 (Little-Endian)      |
|    CommonHeaderLength (2 Bytes) : 0x0008             |
|    Filler      (4 Bytes) : 0xCCCCCCCC                |
|                                                      |
|  Private Header (8 Bytes):                           |
|    ObjectBufferLength (4 Bytes) : Length of payload  |
|    Filler      (4 Bytes) : 0x00000000                |
|                                                      |
|  Top-Level Referent ID (4 Bytes):                    |
|    0x00020000                                        |
+------------------------------------------------------+

KERB_VALIDATION_INFO Fields

The actual payload after the NDR header:

KERB_VALIDATION_INFO (simplified):
+--------------------------------------------------------+
| Fixed-Size Part (~160 Bytes):                          |
|                                                        |
|  LogonTime           (8 Bytes) : FILETIME              |
|  LogoffTime          (8 Bytes) : FILETIME              |
|  KickOffTime         (8 Bytes) : FILETIME              |
|  PasswordLastSet     (8 Bytes) : FILETIME              |
|  PasswordCanChange   (8 Bytes) : FILETIME              |
|  PasswordMustChange  (8 Bytes) : FILETIME              |
|  EffectiveName       (8 Bytes) : RPC_UNICODE_STRING*   |
|  FullName            (8 Bytes) : RPC_UNICODE_STRING*   |
|  LogonScript         (8 Bytes) : RPC_UNICODE_STRING*   |
|  ProfilePath         (8 Bytes) : RPC_UNICODE_STRING*   |
|  HomeDirectory       (8 Bytes) : RPC_UNICODE_STRING*   |
|  HomeDirectoryDrive  (8 Bytes) : RPC_UNICODE_STRING*   |
|  LogonCount          (2 Bytes) : USHORT                |
|  BadPasswordCount    (2 Bytes) : USHORT                |
|  UserId              (4 Bytes) : RID (e.g. 500)        |
|  PrimaryGroupId      (4 Bytes) : RID (e.g. 513)        |
|  GroupCount          (4 Bytes) : Number of groups      |
|  GroupIds            (4 Bytes) : Pointer -> Array      |
|  UserFlags           (4 Bytes) : Flags                 |
|  UserSessionKey     (16 Bytes) : Zeroes                |
|  LogonServer         (8 Bytes) : RPC_UNICODE_STRING*   |
|  LogonDomainName     (8 Bytes) : RPC_UNICODE_STRING*   |
|  LogonDomainId       (4 Bytes) : Pointer -> SID        |
|  ...                                                   |
|  SidCount            (4 Bytes) : Number of ExtraSIDs   |
|  ExtraSids           (4 Bytes) : Pointer -> Array      |
|  ...                                                   |
|  ResourceGroupCount  (4 Bytes) : Number of ResGroups   |
|  ResourceGroupIds    (4 Bytes) : Pointer -> Array      |
|                                                        |
+--------------------------------------------------------+
| Referent Data (variable size):                         |
|                                                        |
|  (Referents appear SEQUENTIALLY in the order           |
|   their pointers were declared!)                       |
|                                                        |
|  Referent 0x00020004: EffectiveName String             |
|    MaxCount(4) + Offset(4) + ActualCount(4)            |
|    + UTF-16LE Bytes + Padding to 4 Bytes               |
|                                                        |
|  Referent 0x00020008: FullName String                  |
|    ... (same structure)                                |
|                                                        |
|  Referent 0x0002000C: LogonScript String               |
|    ... (empty string: MaxCount=0, Offset=0, Count=0)   |
|                                                        |
|  ... (more string referents)                           |
|                                                        |
|  GroupIds Array:                                       |
|    MaxCount(4): Number of groups                       |
|    GROUP_MEMBERSHIP[0]:                                |
|      RelativeId(4): 512 (Domain Admins)                |
|      Attributes(4): 0x07 (Mandatory|Enabled|Default)   |
|    GROUP_MEMBERSHIP[1]:                                |
|      RelativeId(4): 513 (Domain Users)                 |
|      Attributes(4): 0x07                               |
|    ...                                                 |
|                                                        |
|  LogonDomainId SID:                                    |
|    MaxCount(4): SubAuthCount                           |
|    Revision(1): 1                                      |
|    SubAuthCount(1): 4                                  |
|    Authority(6): 0x000000000005 (NT Authority)         |
|    SubAuth[0..3]: Domain SID RIDs                      |
|                                                        |
|  ExtraSids Array (if SidCount > 0):                    |
|    MaxCount(4): Number of ExtraSIDs                    |
|    KERB_SID_AND_ATTRIBUTES[0]:                         |
|      SidPointer(4): Referent ID                        |
|      Attributes(4): 0x07                               |
|    SidPointer -> SID Referent:                         |
|      MaxCount(4) + SID Bytes                           |
|                                                        |
+--------------------------------------------------------+

The Golden Rule of NDR

Referents appear SEQUENTIALLY in the order their pointers were declared!

This means: You CANNOT search for referent IDs or do pattern matching. You must parse the structure linearly from the beginning and read each referent in exactly the order the pointers were declared in the fixed-size part.

Pointer IDs start at 0x00020004:
  EffectiveName  -> 0x00020004 (first pointer after top-level)
  FullName       -> 0x00020008
  LogonScript    -> 0x0002000C
  ProfilePath    -> 0x00020010
  ...
  GroupIds       -> 0x00020024
  LogonDomainId  -> 0x00020028
  ExtraSids      -> 0x0002002C (if present)

RPC_UNICODE_STRING Referent

Strings in NDR are… involved:

In the Fixed-Size Part (8 Bytes):
  Length       (2 Bytes) : Byte length of the string (without null terminator)
  MaximumLength(2 Bytes) : Buffer size (= Length or Length + 2)
  Pointer      (4 Bytes) : Referent ID (or 0x00000000 if NULL)

In the Referent Part (variable):
  MaxCount     (4 Bytes) : MaximumLength / 2 (char count)
  Offset       (4 Bytes) : 0 (always)
  ActualCount  (4 Bytes) : Length / 2 (actual chars)
  Data         (n Bytes) : UTF-16LE encoded
  Padding      (0-3 Bytes): Alignment to 4 bytes

Example: The string “Administrator” (26 UTF-16LE bytes):

Fixed:    1A 00 1C 00 04 00 02 00
          ^^^^       ^^^^
          26 Bytes   Referent ID

Referent: 0E 00 00 00  <- MaxCount = 14 chars
          00 00 00 00  <- Offset = 0
          0D 00 00 00  <- ActualCount = 13 chars
          41 00 64 00 6D 00 69 00 6E 00 69 00  <- "Admini"
          73 00 74 00 72 00 61 00 74 00 6F 00  <- "strato"
          72 00                                 <- "r"
          00 00                                 <- Padding (2 bytes -> 4 aligned)

Part 3: The Other PAC Buffers

PAC_CREDENTIAL_INFO (Type 2)

This buffer is special — it only exists during PKINIT authentication:

PAC_CREDENTIAL_INFO:
+----------------------------------------------+
|  Version        (4 Bytes) : 0x00000000       |
|  EncryptionType (4 Bytes) : e.g. 18 (AES256) |
|  SerializedData (rest)    : Encrypted        |
+----------------------------------------------+

The SerializedData is encrypted with the AS-REP Reply Key (DH-derived) using KeyUsage 16. After decryption, it contains an NDR-serialized PAC_CREDENTIAL_DATA:

PAC_CREDENTIAL_DATA (after decryption):
+-------------------------------------------------+
|  NDR Type Serialization 1 Header (16 Bytes)     |
|  Top-Level Referent (4 Bytes)                   |
|  CredentialCount (4 Bytes) : Number of creds    |
|  Conformant Array MaxCount (4 Bytes)            |
|                                                 |
|  SECPKG_SUPPLEMENTAL_CRED[0]:                   |
|    PackageName  (RPC_UNICODE_STRING) : "NTLM"   |
|    CredentialSize (4 Bytes)                     |
|    Credentials (Pointer -> data)                |
|                                                 |
|  Referent data:                                 |
|    PackageName String: "NTLM" (UTF-16LE)        |
|    NTLM_SUPPLEMENTAL_CREDENTIAL:                |
|      Version    (4 Bytes) : 0                   |
|      Flags      (4 Bytes) : 3 (LM+NT)           |
|      LmPassword (16 Bytes)                      |
|      NtPassword (16 Bytes) <- THE NT HASH       |
+-------------------------------------------------+

Why only with PKINIT? With symmetric auth (password/hash/key), the client already possesses a key derived from the password. With PKINIT, the client authenticates asymmetrically — it has no symmetric key. The KDC therefore delivers the NTLM credentials as a “bonus”, encrypted with the DH key that only the client and KDC know.

adPEAS uses this for automatic NT hash recovery via a U2U (User-to-User) TGS-REQ to itself. Details in Episode 3 (Authentication Deep-Dive, section “UnPAC-the-Hash”).

CLIENT_INFO (Type 10)

The simplest buffer — and the most treacherous:

CLIENT_INFO:
+-----------------------------------------+
|  ClientId   (8 Bytes) : FILETIME        |
|  NameLength (2 Bytes) : Byte length     |
|  Name       (n Bytes) : UTF-16LE        |
+-----------------------------------------+

Treacherous because: ClientId MUST match the authtime from the EncTicketPart exactly. Kerberos uses GeneralizedTime with second-level precision (20250207120000Z), but FILETIME has 100-nanosecond precision. If you construct the FILETIME from a DateTime object with sub-second precision, a mismatch occurs and the DC rejects the ticket with “invalid credential”.

# adPEAS fix: Truncate AuthTime to second-level precision
$AuthTime = [datetime]::new(
    $AuthTime.Year, $AuthTime.Month, $AuthTime.Day,
    $AuthTime.Hour, $AuthTime.Minute, $AuthTime.Second,
    [System.DateTimeKind]::Utc
)

UPN_DNS_INFO (Type 12)

UPN_DNS_INFO:
+-------------------------------------------------+
|  UpnLength         (2 Bytes)                    |
|  UpnOffset         (2 Bytes)                    |
|  DnsDomainLength   (2 Bytes)                    |
|  DnsDomainOffset   (2 Bytes)                    |
|  Flags             (4 Bytes)                    |
|    0x00 = Standard                              |
|    0x01 = UPN_CONSTRUCTED (S-Flag)              |
|    0x02 = EXTENDED (contains SamName + SID)     |
|                                                 |
|  (If Flags & 0x02):                             |
|  SamNameLength     (2 Bytes)                    |
|  SamNameOffset     (2 Bytes)                    |
|  SidLength         (2 Bytes)                    |
|  SidOffset         (2 Bytes)                    |
|                                                 |
|  String data (at the offsets):                  |
|  UPN:        "admin@contoso.com" (UTF-16LE)     |
|  DnsDomain:  "contoso.com" (UTF-16LE)           |
|  SamName:    "admin" (UTF-16LE) (if EXTENDED)   |
|  SID:        Raw SID bytes (if EXTENDED)        |
+-------------------------------------------------+

adPEAS and Rubeus set Flags to 0x01 (UPN_CONSTRUCTED) — the simpler format without SamName/SID fields. Modern DCs internally use 0x02 (EXTENDED) with additional SamName and SID fields, but the 0x01 format is accepted by KDCs without issues.

PAC_ATTRIBUTES (Type 17)

PAC_ATTRIBUTES:
+-------------------------------------------+
|  FlagsLength  (4 Bytes) : 2 (= 2 bits)    |
|  Flags        (4 Bytes) : 0x00000001      |
|    Bit 0: PAC_WAS_REQUESTED = 1           |
+-------------------------------------------+

Just 8 bytes. Tells the KDC that the PAC was explicitly requested.

PAC_REQUESTOR (Type 18)

PAC_REQUESTOR:
+--------------------------------------------+
|  Raw SID (no length prefix!):              |
|  Revision      (1 Byte)  : 1               |
|  SubAuthCount  (1 Byte)  : 5               |
|  Authority     (6 Bytes) : NT Authority    |
|  SubAuth[0]    (4 Bytes) : 21              |
|  SubAuth[1]    (4 Bytes) : Domain part 1   |
|  SubAuth[2]    (4 Bytes) : Domain part 2   |
|  SubAuth[3]    (4 Bytes) : Domain part 3   |
|  SubAuth[4]    (4 Bytes) : RID (e.g. 500)  |
+--------------------------------------------+

This buffer was introduced with CVE-2021-42287. The DC verifies that the SID in PAC_REQUESTOR matches the user who requested the ticket. Without this buffer, the ticket is rejected on patched DCs.

Important for Diamond Tickets: When impersonating a different user, the RID in PAC_REQUESTOR must match the target user (not the base user).


Part 4: PAC Signatures — The Trust Anchor

How PAC Checksums Work

The PAC has two signatures that ensure it hasn’t been tampered with:

+---------------------------------------------------------+
|                PAC Signature Procedure                  |
+---------------------------------------------------------+
|                                                         |
|  Step 1: Compute SERVER_CHECKSUM                        |
|  -----------------------------------------------        |
|  1a. Set server and KDC checksum fields to NULL         |
|  1b. HMAC over the ENTIRE PAC (with zeroes)             |
|  1c. Write result into SERVER_CHECKSUM                  |
|                                                         |
|  Step 2: Compute KDC_CHECKSUM                           |
|  ----------------------------------------               |
|  2a. HMAC over the SERVER_CHECKSUM bytes                |
|      (NOT over the entire PAC!)                         |
|  2b. Write result into KDC_CHECKSUM                     |
|                                                         |
|  Key Usage for both: 17                                 |
|                                                         |
|  For Golden Ticket:                                     |
|    Server Key = krbtgt key                              |
|    KDC Key    = krbtgt key                              |
|                                                         |
|  For Silver Ticket:                                     |
|    Server Key = Service account key                     |
|    KDC Key    = Service account key  <- Both the same!  |
|                                                         |
|  For legitimate TGT:                                    |
|    Server Key = krbtgt key                              |
|    KDC Key    = krbtgt key                              |
|                                                         |
+---------------------------------------------------------+

Checksum Format (PAC_SIGNATURE_DATA)

PAC_SIGNATURE_DATA:
+--------------------------------------------+
|  SignatureType  (4 Bytes, signed int32)    |
|    -138  = HMAC_MD5 (RC4)                  |
|    15    = HMAC_SHA1_96_AES128             |
|    16    = HMAC_SHA1_96_AES256             |
|                                            |
|  Signature      (variable)                 |
|    RC4:  16 Bytes (HMAC-MD5)               |
|    AES:  12 Bytes (truncated HMAC-SHA1)    |
|                                            |
|  NO RODCIdentifier!                        |
|  (only relevant for Read-Only DCs)         |
+--------------------------------------------+

Key Derivation for PAC Checksums

This is where it gets subtle. RFC 3961 defines three derived keys:

                        Base Key
                           |
            +--------------+-----------------+
            v              v                 v
     DK(usage||0xAA)   DK(usage||0x55)  DK(usage||0x99)
           Ke              Ki                Kc
      (Encryption)     (Integrity)       (Checksum)

PAC checksums use Kc (suffix 0x99), NOT Ki (suffix 0x55).

Why does this matter? Ki is used for integrity checks within encrypted data (the HMAC at the end of an AES-CTS ciphertext). Kc is used for standalone checksums — and that’s exactly what a PAC signature is.

Overlooking this difference leads to a frustrating bug: The cryptography is technically correct (valid HMAC), but the KDC expects a different key and you get STATUS_WRONG_PASSWORD (0x56).

# adPEAS uses Windows Native Crypto for this reason:
$checksum = Get-KerberosChecksumNative `
    -EncryptionType 18 `   # AES256 (internally mapped to checksum type 16)
    -Key $krbtgtKey `
    -Data $pacBytes `
    -KeyUsage 17           # PAC Checksum usage

Silver Tickets: Both Checksums Are the Same

Normally, the server checksum is signed with the service account key and the KDC checksum is signed with the krbtgt key. But with Silver Tickets, we don’t know the krbtgt key — so we sign both with the service account key.

This works because the target service normally doesn’t forward the KDC checksum to the KDC for validation. It only verifies the server checksum using its own key.


Part 5: Golden Ticket — Step by Step

What Do We Need?

IngredientSourceExample
krbtgt KeyDCSync, NTDS.ditAES256: 52a4126c7ab14fe...
Domain SIDGet-DomainObjectS-1-5-21-1234-5678-9012
Domain NameknownCONTOSO.COM
Domain NetBIOSGet-DomainObjectCONTOSO
User RIDoptional (Default: 500)500 (Administrator)
Group RIDsoptional (Default: DA+DU+SA+EA+GPCO)512, 513, 518, 519, 520

The Build Process in adPEAS

+------------------------------------------------------------+
|                 New-GoldenTicket Flow                      |
+------------------------------------------------------------+
|                                                            |
|  1. Generate random session key                            |
|     $sessionKey = [byte[]]::new(32)                        |
|     [Security.Cryptography.RNGCryptoServiceProvider]::     |
|       Create().GetBytes($sessionKey)                       |
|                                                            |
|  2. Set timestamps                                         |
|     $authTime = [DateTime]::UtcNow (truncated to seconds)  |
|     $endTime = $authTime.AddHours(10)                      |
|     $renewTill = $authTime.AddDays($ValidityDays)          |
|                                                            |
|  3. Build PAC (Build-PAC)                                  |
|     |-- Build-KerbValidationInfo (NDR)                     |
|     |   +-- User info, groups, domain SID, ExtraSIDs       |
|     |-- Build-PACClientInfo                                |
|     |   +-- authTime (FILETIME) + Username                 |
|     |-- Build-PACUpnDnsInfo (Flags: 0x01 UPN_CONSTRUCTED)  |
|     |   +-- UPN, DnsDomain, SamName, SID                   |
|     |-- Build-PACAttributesInfo (0x01)                     |
|     |-- Build-PACRequestor (User SID)                      |
|     |-- Build-PACSignature (Server, zeroed)                |
|     +-- Build-PACSignature (KDC, zeroed)                   |
|                                                            |
|  4. Compute PAC checksums (Complete-PACSignatures)         |
|     |-- Set both checksum fields to NULL                   |
|     |-- Server checksum = HMAC(krbtgt, entire PAC)         |
|     |-- Write server checksum                              |
|     +-- KDC checksum = HMAC(krbtgt, server checksum)       |
|                                                            |
|  5. Build EncTicketPart (New-EncTicketPart, ASN.1)         |
|     |-- flags: Forwardable, Renewable, Initial, PreAuth    |
|     |-- key: $sessionKey                                   |
|     |-- crealm, cname: User info                           |
|     |-- authtime, endtime, renew-till                      |
|     +-- authorization-data: AD-WIN2K-PAC(PAC)              |
|                                                            |
|  6. Encrypt EncTicketPart                                  |
|     $cipher = Protect-KerberosNative(                      |
|       EncryptionType=18, Key=$krbtgtKey,                   |
|       Data=$encTicketPart, KeyUsage=2                      |
|     )                                                      |
|                                                            |
|  7. Build Ticket (APPLICATION 1)                           |
|     |-- tkt-vno: 5                                         |
|     |-- realm: CONTOSO.COM                                 |
|     |-- sname: krbtgt/CONTOSO.COM                          |
|     +-- enc-part: $cipher (etype 18, kvno 2)               |
|                                                            |
|  8. Build KRB-CRED (Build-KRBCred)                         |
|     |-- tickets: [Ticket]                                  |
|     +-- enc-part: EncKrbCredPart (unencrypted)             |
|         +-- key: $sessionKey                               |
|                                                            |
|  9. Optional: PTT via Import-KerberosTicket                |
|     +-- LSA API: LsaCallAuthenticationPackage              |
|                                                            |
+------------------------------------------------------------+
# adPEAS invocation:
Invoke-TicketForge -Mode Golden `
    -Domain "contoso.com" `
    -DomainSID "S-1-5-21-1234-5678-9012" `
    -AES256Key "52a4126c7ab14fe..." `
    -PTT

What the KDC Sees (and What It Doesn’t)

Golden Ticket OPSEC Profile:
+-----------------------------------------------------+
|  Event ID 4768 (TGT Request):      NOT present      |
|    -> No AS-REQ was sent!                           |
|    -> Suspicious: TGT exists without AS-REQ         |
|                                                     |
|  Event ID 4769 (TGS Request):      Present          |
|    -> When you use the Golden Ticket for TGS-REQ    |
|    -> Looks "normal"                                |
|                                                     |
|  Event ID 4770 (TGT Renew):        Possible         |
|    -> If ValidityDays is very long -> suspicious    |
|                                                     |
|  Detection:                                         |
|  - No corresponding 4768 event for the TGT          |
|  - Unusually long ticket lifetime                   |
|  - Groups the user doesn't normally have            |
|  - RC4 encryption in an AES environment             |
+-----------------------------------------------------+

Part 6: Silver Ticket — Straight to the Service

Differences from the Golden Ticket

Golden Ticket:
  Ticket.sname = krbtgt/CONTOSO.COM
  Encrypted with = krbtgt key
  Ticket Flags: Initial = YES
  Usage: TGS-REQ -> Service Ticket -> Service

Silver Ticket:
  Ticket.sname = cifs/fileserver.contoso.com (or another SPN)
  Encrypted with = Service account key
  Ticket Flags: Initial = NO
  Usage: Directly to the service (AP-REQ) -- no KDC contact!

The key point: A Silver Ticket bypasses the KDC entirely. You send the forged service ticket directly to the target service. The service checks:

  1. Can I decrypt the enc-part with my key? -> Yes
  2. Is the EncTicketPart valid (timestamps, flags)? -> Yes
  3. What groups does the user have (according to the PAC)? -> Whatever you put in there

PAC Signatures for Silver Tickets

+-----------------------------------------------------+
|  Normal Service Ticket (from the KDC):              |
|    Server Checksum: HMAC(Service-Key, PAC)          |
|    KDC Checksum:    HMAC(krbtgt-Key, Server-CS)     |
|                                                     |
|  Silver Ticket (forged):                            |
|    Server Checksum: HMAC(Service-Key, PAC)          |
|    KDC Checksum:    HMAC(Service-Key, Server-CS)    |
|                     ^^^^^^^^^^^^^^^^                |
|                     Same key!                       |
|                                                     |
|  Works because:                                     |
|    The service only validates the Server Checksum   |
|    KDC Checksum is only checked when the service    |
|    requests PAC validation from the KDC (rare)      |
+-----------------------------------------------------+
# Silver Ticket for LDAP:
Invoke-TicketForge -Mode Silver `
    -Domain "contoso.com" `
    -DomainSID "S-1-5-21-1234-5678-9012" `
    -ServiceType LDAP -TargetComputer "dc01" `
    -AES256Key "DC01_COMPUTER_AES256KEY" `
    -PTT

Supported Service Types

ServiceTypeSPN becomesTypical key owner
CIFScifs/host.domain.comComputer Account (DC01$)
LDAPldap/host.domain.comComputer Account (DC01$)
HTTPhttp/host.domain.comComputer Account or IIS Account
HOSThost/host.domain.comComputer Account
MSSQLMSSQLSvc/host.domain.comSQL Service Account
WSMANwsman/host.domain.comComputer Account

Part 7: Diamond Ticket — The Hybrid Approach

Why Diamond Instead of Golden?

Golden Ticket Problem:
  DC Logs: [4769: TGS-REQ from "admin"]
  DC Logs: [no 4768 for "admin"!]  <- Suspicious!
  SOC: "Where did this TGT come from? There's no login!"

Diamond Ticket:
  DC Logs: [4768: AS-REQ from "lowpriv"]  <- Normal login
  DC Logs: [4769: TGS-REQ from "lowpriv"]  <- Normal service access
  SOC: "Looks normal."
  Reality: lowpriv's TGT has Domain Admins in the PAC!

The Hybrid Approach in Detail

The Diamond Ticket in adPEAS uses a “hybrid” approach: It combines a real TGT with the proven Golden Ticket logic for rebuilding the PAC.

Why not just patch the PAC in the original TGT?

Because NDR serialization is not deterministic. When you re-serialize the LOGON_INFO buffer (e.g., with new groups), Build-KerbValidationInfo produces an NDR blob that is byte-for-byte different from the original. Even if the logical data is the same. The KDC validates the PAC checksum — and it no longer matches.

We tested this approach. Result: KRB_AP_ERR_MODIFIED (Error 41) on every TGS-REQ.

The solution: Take the real TGT, extract the relevant metadata (timestamps, session key), and rebuild everything from scratch using the Golden Ticket logic:

+----------------------------------------------------------+
|                 Diamond Ticket Hybrid Flow               |
+----------------------------------------------------------+
|                                                          |
|  Phase 1: Obtain a real TGT                              |
|  -------------------------                               |
|  Input: BaseUser credentials (Password/Hash/Key/.kirbi)  |
|  -> AS-REQ to KDC (generates Event 4768 in DC logs!)     |
|  -> AS-REP contains real TGT + session key               |
|                                                          |
|  Phase 2: Decrypt and parse the TGT                      |
|  ----------------------------------                      |
|  1. Parse KRB-CRED (ASN.1)                               |
|  2. Decrypt Ticket.enc-part with krbtgt key              |
|  3. Parse EncTicketPart:                                 |
|     - Extract session key  <- CRITICAL: preserve it!     |
|     - Extract authtime                                   |
|     - Parse PAC -> UserName, Domain, SID, RID, Groups    |
|                                                          |
|  Phase 3: Build new ticket with Golden logic             |
|  ------------------------------------------              |
|  1. Build-PAC with desired GroupRIDs                     |
|     (e.g. 512=DA, 519=EA -- instead of real groups)      |
|  2. Complete-PACSignatures with krbtgt key               |
|  3. New-EncTicketPart with:                              |
|     - ORIGINAL session key (don't generate a new one!)   |
|     - ORIGINAL authtime (must match CLIENT_INFO)         |
|     - New groups in the PAC                              |
|  4. Encrypt with krbtgt key (KeyUsage 2)                 |
|  5. Build Ticket + KRB-CRED with ORIGINAL session key    |
|                                                          |
|  Why the original session key?                           |
|  -----------------------------                           |
|  The client knows the session key from the AS-REP.       |
|  The TGS-REQ Authenticator is encrypted with it.         |
|  If we generate a new session key, the Authenticator     |
|  won't match -> TGS-REQ fails.                           |
|                                                          |
|  With Golden Tickets this isn't a problem because we     |
|  control both the session key in the ticket and in the   |
|  KRB-CRED. With Diamond Tickets, the client already      |
|  has the original key.                                   |
|                                                          |
+----------------------------------------------------------+

OPSEC Modes

Mode 1: Stealthy (without -UserName)
----------------------------------
  BaseUser = lowpriv
  Ticket User = lowpriv  <- Same as AS-REQ!
  Groups = Domain Admins, Enterprise Admins

  DC Logs: "lowpriv made AS-REQ" ✓
  Ticket:  "lowpriv is Domain Admin"
  Detection: Only PAC content analysis (very rare)

Mode 2: Impersonation (with -UserName)
---------------------------------------
  BaseUser = lowpriv
  Ticket User = Administrator  <- Different from AS-REQ!
  Groups = Domain Admins, Enterprise Admins

  DC Logs: "lowpriv made AS-REQ" ✓
  Ticket:  "Administrator is Domain Admin"
  Detection: Username mismatch between 4768 and 4769

Encryption Type Constraint

Diamond Ticket Constraint:
  Base TGT etype MUST match the krbtgt key!

  Scenario:
    DC encrypts TGT with AES256 (etype 18)
    You only have the krbtgt NT hash (etype 23)
    -> ERROR: Cannot decrypt TGT!

  Solution A: Obtain the krbtgt AES256 key (DCSync with /all)
  Solution B: Use a Golden Ticket instead of Diamond

Part 8: PAC Buffer Consistency

The Consistency Problem

All PAC buffers must describe the same user. When creating a Diamond Ticket with impersonation, all buffers must be updated:

Buffer                      Needs to be updated?
------------------------    ------------------------
LOGON_INFO (type 1)         Yes -- UserName, RID, groups
CLIENT_INFO (type 10)       Yes -- Client Name
UPN_DNS_INFO (type 12)      Yes -- UPN, SamName, SID RID
PAC_ATTRIBUTES (type 17)    No -- static (0x01)
PAC_REQUESTOR (type 18)     Yes -- last SubAuth (RID)
SERVER_CHECKSUM (type 6)    Yes -- recompute after all changes
KDC_CHECKSUM (type 7)       Yes -- recompute after server checksum

If LOGON_INFO says “RID 500 (Administrator)” but PAC_REQUESTOR says “RID 1103 (lowpriv)”, the ticket will be rejected. Rubeus’ ModifyTicket therefore updates all relevant buffers consistently.

What Happens with Inconsistencies?

InconsistencyResult
LOGON_INFO RID ≠ PAC_REQUESTOR RIDLogon failure (ticket rejected)
CLIENT_INFO Name ≠ EncTicketPart cname”invalid credential”
CLIENT_INFO Timestamp ≠ EncTicketPart authtime”invalid credential”
PAC_REQUESTOR missingLogon failure (on patched DCs)
TICKET_CHECKSUM presentLogon failure (KDC can’t validate it)
Server checksum wrongKRB_AP_ERR_MODIFIED (Error 41)

Part 9: Detection and Countermeasures

What Defenders Can See

Event ID 4768 (TGT Request):

  • Golden Ticket: No event -> suspicious
  • Diamond Ticket: Normal event -> stealthy

Event ID 4769 (TGS Request):

  • Encryption Type 0x17 (RC4) in an AES environment -> suspicious
  • Unexpected groups for the user -> only with PAC analysis

PAC-based Detection:

  • Microsoft Defender for Identity can analyze PAC contents
  • Comparison: “Does user X actually have Domain Admins membership?”
  • LOGON_INFO groups vs. actual AD group membership

Ticket Lifetime Anomalies:

  • Golden Ticket with 10-year validity -> obviously forged
  • Diamond Ticket adopts domain policy -> inconspicuous

Countermeasures

MeasureAgainstEffectiveness
krbtgt Key RotationGolden + DiamondHigh (invalidates all TGTs)
Credential GuardHash dumps for key materialHigh
PAC Validation (KDC)Silver TicketsMedium (performance impact)
AES-Only PolicyRC4-based ticketsMedium (reduces attack surface)
Protected UsersVariousMedium (no delegation, no RC4)
MDI/ATAAnomaly detectionHigh (PAC analysis, TGT anomalies)
Frequent krbtgt RotationPersistence via GoldenHigh (recommended: every 180 days)

krbtgt Key Rotation

The most effective defense against Golden and Diamond Tickets:

Rotation 1: New krbtgt key (kvno 3)
  -> Old tickets (kvno 2) still work!
  -> Kerberos allows N and N-1

Rotation 2 (after replication): New krbtgt key (kvno 4)
  -> Now kvno 2 no longer works
  -> Only kvno 3 and 4 are valid
  -> All Golden/Diamond Tickets with the old key are invalidated

Recommendation: Two rotations with 12-24h interval

Summary

The Ticket Forging Hierarchy

+------------------------------------------------------+
|                                                      |
|  Golden Ticket                                       |
|  |-- Requires: krbtgt key                            |
|  |-- OPSEC: Poor (no AS-REQ in logs)                 |
|  |-- Scope: Entire domain                            |
|  +-- Complexity: Easiest                             |
|                                                      |
|  Silver Ticket                                       |
|  |-- Requires: Service account key                   |
|  |-- OPSEC: Medium (no TGS-REQ in logs)              |
|  |-- Scope: A single specific service                |
|  +-- Complexity: Medium (SPN must be known)          |
|                                                      |
|  Diamond Ticket                                      |
|  |-- Requires: krbtgt key + user credentials         |
|  |-- OPSEC: Good (real AS-REQ in logs)               |
|  |-- Scope: Entire domain                            |
|  +-- Complexity: Highest (hybrid approach)           |
|                                                      |
+------------------------------------------------------+

What You Should Understand Now

  1. The PAC is a Microsoft extension carrying user identity and group information inside the Kerberos ticket
  2. NDR serialization is the encoding for LOGON_INFO — with referents, alignment, and RPC_UNICODE_STRING
  3. 7 PAC buffers are required on modern DCs (CVE-2021-42287)
  4. PAC checksums use Kc (0x99), not Ki (0x55) — a subtle but critical distinction
  5. Golden Tickets build everything from scratch — maximum control, poor OPSEC
  6. Silver Tickets bypass the KDC entirely — limited scope, good OPSEC
  7. Diamond Tickets modify real TGTs — best OPSEC trade-off, highest complexity
  8. Session key preservation is critical for Diamond Tickets — otherwise the TGS-REQ fails
  9. Buffer consistency between LOGON_INFO, CLIENT_INFO, UPN_DNS_INFO, and PAC_REQUESTOR is mandatory
  10. krbtgt key rotation (2x) is the most effective countermeasure

← Episode 7: Kerberos Internals | Episode 9: Tips & Tricks — coming soon

About the Author

Alexander Sturz

Founder & Red Team Lead

Active Directory Ninja and offensive security expert specializing in enterprise infrastructure compromise and post-exploitation techniques.

Related Articles