SEKurity GmbH Logo
adPEAS

adPEAS v2 Episode 8: PAC Deep-Dive & Ticket Forging — Was im Ticket steckt

Deep Dive in die PAC-Struktur, NDR-Serialisierung, PAC-Checksums und wie adPEAS v2 Golden, Silver und Diamond Tickets Schritt für Schritt fälscht.

Alexander Sturz

Gründer & Red Team Lead

24 Min. Lesezeit
Teilen:

Einleitung

In Episode 7 haben wir gesehen wie Kerberos-Nachrichten aufgebaut sind, wie Verschlüsselung funktioniert, und was im EncTicketPart steckt. Dabei haben wir ein Feld am Ende entdeckt: authorization-data mit ad-type 128 — der PAC. Und genau da wird es jetzt richtig interessant.

Der PAC (Privilege Attribute Certificate) ist der Grund warum Golden, Silver und Diamond Tickets funktionieren. Er enthält die Identitätsinformationen des Users — Gruppen, SIDs, Rechte. Und er ist der Teil den wir bei Ticket Forging manipulieren.

Vorwissen: Du solltest Episode 7 gelesen haben. Du brauchst ein Verständnis von ASN.1 DER, der EncTicketPart-Struktur, und wie AES-CTS/RC4-HMAC Verschlüsselung funktioniert.


Teil 1: PAC Architektur

Was ist der PAC?

Der PAC ist eine Microsoft-Erweiterung des Kerberos-Protokolls (MS-PAC). Reines MIT-Kerberos hat keinen PAC. In Active Directory enthält der PAC die Windows-spezifischen Autorisierungsdaten eines Users — alles was der Zielservice braucht um Access Control Decisions zu treffen.

EncTicketPart
  +-- authorization-data [10]
      +-- AD-IF-RELEVANT (type 1)
          +-- AD-WIN2K-PAC (type 128)
              +-- PACTYPE (der eigentliche 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 Struktur

Der PAC ist KEIN ASN.1 — er ist ein proprietäres Microsoft-Format:

PACTYPE Header:
+----------------------------------------------------+
|  cBuffers (4 Bytes) : Anzahl der PAC-Buffer        |
|  Version  (4 Bytes) : 0x00000000                   |
+----------------------------------------------------+
|  PAC_INFO_BUFFER[0]:                               |
|    ulType  (4 Bytes) : Buffer-Typ (1, 6, 7, etc.)  |
|    cbBufferSize (4 Bytes) : Größe des Buffers      |
|    Offset  (8 Bytes) : Offset im PAC-Blob          |
|                                                    |
|  PAC_INFO_BUFFER[1]:                               |
|    ulType  (4 Bytes) : ...                         |
|    cbBufferSize (4 Bytes) : ...                    |
|    Offset  (8 Bytes) : ...                         |
|                                                    |
|  ... (wiederholt für jeden Buffer)                 |
+----------------------------------------------------+
|  Buffer-Daten (ab den jeweiligen Offsets):         |
|    [LOGON_INFO Bytes]                              |
|    [CLIENT_INFO Bytes]                             |
|    [UPN_DNS_INFO Bytes]                            |
|    [PAC_ATTRIBUTES Bytes]                          |
|    [PAC_REQUESTOR Bytes]                           |
|    [SERVER_CHECKSUM Bytes]                         |
|    [KDC_CHECKSUM Bytes]                            |
+----------------------------------------------------+

Jeder Buffer wird auf 8 Bytes aligned. Die Reihenfolge der Buffer in der PAC_INFO_BUFFER-Tabelle kann variieren, die Offsets zeigen auf die richtigen Daten.

Die 7 erforderlichen PAC-Buffer

Seit den CVE-2021-42287 Patches erzwingt Windows Server 2022+ genau diese Buffer:

TypeNameBeschreibungGröße
1LOGON_INFOUser-Identität, Gruppen, Domain-SID~500-2000 Bytes
6SERVER_CHECKSUMHMAC-Signatur über den gesamten PAC20-28 Bytes
7KDC_CHECKSUMHMAC-Signatur über den Server-Checksum20-28 Bytes
10CLIENT_INFOClient-Name + Auth-Zeitstempel~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

Ein weiterer Buffer der bei PKINIT-Authentifizierung vom KDC erzeugt wird:

TypeNameBeschreibungGröße
2CREDENTIAL_INFOVerschlüsselte NTLM-Credentials (nur bei PKINIT)~100-200 Bytes

Dieser Buffer enthält den NT-Hash des Benutzers, verschlüsselt mit dem DH-derived AS-REP Reply Key (KeyUsage 16). Er wird nur bei PKINIT-Authentifizierung eingefügt, weil der Client dort keinen symmetrischen Key besitzt. adPEAS nutzt das für UnPAC-the-Hash — die automatische NT-Hash-Recovery nach PKINIT (siehe Episode 3).

Zwei zusätzliche Buffer die der KDC normalerweise erzeugt, bei Ticket Forging aber nicht enthalten sein dürfen:

TypeNameWarum weglassen?
16TICKET_CHECKSUMNur vom KDC berechenbar (benötigt krbtgt-Key des jeweiligen TGS)
19FULL_PAC_CHECKSUMNur vom KDC berechenbar

adPEAS’ Build-PAC schließt diese Buffer gar nicht erst ein. Wenn sie vorhanden wären (z.B. bei In-Place-PAC-Patching), würde der KDC das Ticket ablehnen weil er die Checksums nicht validieren kann.


Teil 2: LOGON_INFO — Die User-Identität (NDR Format)

Warum NDR?

Der LOGON_INFO Buffer enthält eine KERB_VALIDATION_INFO Struktur. Diese ist im NDR (Network Data Representation) Format serialisiert — nicht ASN.1, nicht JSON, nicht Protocol Buffers, sondern das Serialisierungsformat von DCE/RPC aus den 90er Jahren.

NDR ist… speziell. Es hat:

  • Referent-IDs für Pointer
  • Padding-Regeln die von der Position im Buffer abhängen
  • Strings als RPC_UNICODE_STRING mit separaten Referents
  • Konforme Arrays mit vorgestellter MaxCount

NDR Type Serialization Header

Jeder NDR-serialisierte Typ in einem PAC-Buffer beginnt mit einem 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) : Länge des Payloads |
|    Filler      (4 Bytes) : 0x00000000                |
|                                                      |
|  Top-Level Referent ID (4 Bytes):                    |
|    0x00020000                                        |
+------------------------------------------------------+

KERB_VALIDATION_INFO Felder

Die eigentliche Payload nach dem NDR-Header:

KERB_VALIDATION_INFO (vereinfacht):
+--------------------------------------------------------+
| Fixed-Size Teil (ca. 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 (z.B. 500)        |
|  PrimaryGroupId      (4 Bytes) : RID (z.B. 513)        |
|  GroupCount          (4 Bytes) : Anzahl Gruppen        |
|  GroupIds            (4 Bytes) : Pointer -> Array      |
|  UserFlags           (4 Bytes) : Flags                 |
|  UserSessionKey     (16 Bytes) : Nullen                |
|  LogonServer         (8 Bytes) : RPC_UNICODE_STRING*   |
|  LogonDomainName     (8 Bytes) : RPC_UNICODE_STRING*   |
|  LogonDomainId       (4 Bytes) : Pointer -> SID        |
|  ...                                                   |
|  SidCount            (4 Bytes) : Anzahl ExtraSIDs      |
|  ExtraSids           (4 Bytes) : Pointer -> Array      |
|  ...                                                   |
|  ResourceGroupCount  (4 Bytes) : Anzahl ResourceGroups |
|  ResourceGroupIds    (4 Bytes) : Pointer -> Array      |
|                                                        |
+--------------------------------------------------------+
| Referent-Daten (variable Größe):                       |
|                                                        |
|  (Referents erscheinen SEQUENZIELL in der              |
|   Reihenfolge ihrer Pointer-Deklaration!)              |
|                                                        |
|  Referent 0x00020004: EffectiveName String             |
|    MaxCount(4) + Offset(4) + ActualCount(4)            |
|    + UTF-16LE Bytes + Padding auf 4 Bytes              |
|                                                        |
|  Referent 0x00020008: FullName String                  |
|    ... (gleiche Struktur)                              |
|                                                        |
|  Referent 0x0002000C: LogonScript String               |
|    ... (leerer String: MaxCount=0, Offset=0, Count=0)  |
|                                                        |
|  ... (weitere String-Referents)                        |
|                                                        |
|  GroupIds Array:                                       |
|    MaxCount(4): Anzahl Gruppen                         |
|    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 (wenn SidCount > 0):                  |
|    MaxCount(4): Anzahl ExtraSIDs                       |
|    KERB_SID_AND_ATTRIBUTES[0]:                         |
|      SidPointer(4): Referent-ID                        |
|      Attributes(4): 0x07                               |
|    SidPointer -> SID Referent:                         |
|      MaxCount(4) + SID Bytes                           |
|                                                        |
+--------------------------------------------------------+

Die goldene Regel bei NDR

Referents erscheinen SEQUENZIELL in der Reihenfolge ihrer Pointer-Deklaration!

Das bedeutet: Du kannst NICHT nach Referent-IDs suchen oder Pattern-Matching machen. Du musst die Struktur vom Anfang an linear durchparsen und jeden Referent in exakt der Reihenfolge lesen, in der die Pointer im Fixed-Size-Teil deklariert wurden.

Pointer IDs beginnen bei 0x00020004:
  EffectiveName  -> 0x00020004 (erster Pointer nach Top-Level)
  FullName       -> 0x00020008
  LogonScript    -> 0x0002000C
  ProfilePath    -> 0x00020010
  ...
  GroupIds       -> 0x00020024
  LogonDomainId  -> 0x00020028
  ExtraSids      -> 0x0002002C (wenn vorhanden)

RPC_UNICODE_STRING Referent

Strings in NDR sind… aufwändig:

Im Fixed-Size Teil (8 Bytes):
  Length       (2 Bytes) : Byte-Länge des Strings (ohne Null-Terminator)
  MaximumLength(2 Bytes) : Buffer-Größe (= Length oder Length + 2)
  Pointer      (4 Bytes) : Referent-ID (oder 0x00000000 wenn NULL)

Im Referent-Teil (variable):
  MaxCount     (4 Bytes) : MaximumLength / 2 (Char-Count)
  Offset       (4 Bytes) : 0 (immer)
  ActualCount  (4 Bytes) : Length / 2 (tatsächliche Chars)
  Data         (n Bytes) : UTF-16LE encoded
  Padding      (0-3 Bytes): Alignment auf 4 Bytes

Beispiel: Der 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)

Teil 3: Die anderen PAC-Buffer

PAC_CREDENTIAL_INFO (Type 2)

Dieser Buffer ist besonders — er existiert nur bei PKINIT-Authentifizierung:

PAC_CREDENTIAL_INFO:
+---------------------------------------------+
|  Version        (4 Bytes) : 0x00000000      |
|  EncryptionType (4 Bytes) : z.B. 18 (AES256)|
|  SerializedData (rest)    : Verschlüsselt   |
+---------------------------------------------+

Die SerializedData ist mit dem AS-REP Reply Key (DH-derived) verschlüsselt (KeyUsage 16). Nach der Entschlüsselung enthält sie eine NDR-serialisierte PAC_CREDENTIAL_DATA:

PAC_CREDENTIAL_DATA (nach Entschlüsselung):
+-------------------------------------------------+
|  NDR Type Serialization 1 Header (16 Bytes)     |
|  Top-Level Referent (4 Bytes)                   |
|  CredentialCount (4 Bytes) : Anzahl Credentials |
|  Conformant Array MaxCount (4 Bytes)            |
|                                                 |
|  SECPKG_SUPPLEMENTAL_CRED[0]:                   |
|    PackageName  (RPC_UNICODE_STRING) : "NTLM"   |
|    CredentialSize (4 Bytes)                     |
|    Credentials (Pointer -> Daten)               |
|                                                 |
|  Referent-Daten:                                |
|    PackageName String: "NTLM" (UTF-16LE)        |
|    NTLM_SUPPLEMENTAL_CREDENTIAL:                |
|      Version    (4 Bytes) : 0                   |
|      Flags      (4 Bytes) : 3 (LM+NT)           |
|      LmPassword (16 Bytes)                      |
|      NtPassword (16 Bytes) <- DER NT-HASH       |
+-------------------------------------------------+

Warum nur bei PKINIT? Bei symmetrischer Auth (Password/Hash/Key) besitzt der Client bereits einen Key der vom Passwort abgeleitet ist. Bei PKINIT authentifiziert sich der Client asymmetrisch — er hat keinen symmetrischen Key. Der KDC liefert deshalb die NTLM-Credentials als “Bonus” mit, verschlüsselt mit dem DH-Key, den nur Client und KDC kennen.

adPEAS nutzt das für die automatische NT-Hash-Recovery über einen U2U (User-to-User) TGS-REQ an sich selbst. Details dazu in Episode 3 (Authentication Deep-Dive, Abschnitt “UnPAC-the-Hash”).

CLIENT_INFO (Type 10)

Der einfachste Buffer — und der tückischste:

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

Tückisch weil: ClientId MUSS exakt mit dem authtime aus dem EncTicketPart übereinstimmen. Kerberos verwendet GeneralizedTime mit Sekunden-Präzision (20250207120000Z), aber FILETIME hat 100-Nanosekunden-Präzision. Wenn du den FILETIME aus einem DateTime-Objekt mit Sub-Sekunden-Präzision baust, entsteht eine Abweichung -> der DC lehnt das Ticket ab mit “invalid credential”.

# adPEAS Fix: AuthTime auf Sekunden-Präzision truncaten
$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 (enthält SamName + SID)      |
|                                                 |
|  (Wenn Flags & 0x02):                           |
|  SamNameLength     (2 Bytes)                    |
|  SamNameOffset     (2 Bytes)                    |
|  SidLength         (2 Bytes)                    |
|  SidOffset         (2 Bytes)                    |
|                                                 |
|  String-Daten (ab den Offsets):                 |
|  UPN:        "admin@contoso.com" (UTF-16LE)     |
|  DnsDomain:  "contoso.com" (UTF-16LE)           |
|  SamName:    "admin" (UTF-16LE) (wenn EXTENDED) |
|  SID:        Raw SID bytes (wenn EXTENDED)      |
+-------------------------------------------------+

adPEAS und Rubeus setzen Flags auf 0x01 (UPN_CONSTRUCTED) - das einfachere Format ohne SamName/SID-Felder. Moderne DCs verwenden intern 0x02 (EXTENDED) mit zusätzlichen SamName- und SID-Feldern, aber das 0x01-Format wird von KDCs problemlos akzeptiert.

PAC_ATTRIBUTES (Type 17)

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

Nur 8 Bytes. Sagt dem KDC dass der PAC explizit angefordert wurde.

PAC_REQUESTOR (Type 18)

PAC_REQUESTOR:
+--------------------------------------------+
|  Raw SID (keine Längen-Prefix!):           |
|  Revision      (1 Byte)  : 1               |
|  SubAuthCount  (1 Byte)  : 5               |
|  Authority     (6 Bytes) : NT Authority    |
|  SubAuth[0]    (4 Bytes) : 21              |
|  SubAuth[1]    (4 Bytes) : Domain-Teil 1   |
|  SubAuth[2]    (4 Bytes) : Domain-Teil 2   |
|  SubAuth[3]    (4 Bytes) : Domain-Teil 3   |
|  SubAuth[4]    (4 Bytes) : RID (z.B. 500)  |
+--------------------------------------------+

Dieser Buffer wurde mit CVE-2021-42287 eingeführt. Der DC prüft ob die SID im PAC_REQUESTOR mit dem User übereinstimmt der das Ticket angefordert hat. Ohne diesen Buffer wird das Ticket auf gepatchten DCs abgelehnt.

Wichtig bei Diamond Tickets: Wenn du einen anderen User impersonierst, muss die RID im PAC_REQUESTOR zum Ziel-User passen (nicht zum Base-User).


Teil 4: PAC Signaturen — Der Vertrauensanker

Wie PAC-Checksums funktionieren

Der PAC hat zwei Signaturen die sicherstellen, dass er nicht manipuliert wurde:

+---------------------------------------------------------+
|                PAC Signatur-Verfahren                   |
+---------------------------------------------------------+
|                                                         |
|  Schritt 1: SERVER_CHECKSUM berechnen                   |
|  ----------------------------------------               |
|  1a. Server- und KDC-Checksum-Felder auf NULL setzen    |
|  1b. HMAC über den GESAMTEN PAC (mit Nullen)            |
|  1c. Ergebnis in SERVER_CHECKSUM schreiben              |
|                                                         |
|  Schritt 2: KDC_CHECKSUM berechnen                      |
|  ------------------------------------                   |
|  2a. HMAC über die SERVER_CHECKSUM Bytes                |
|      (NICHT über den ganzen PAC!)                       |
|  2b. Ergebnis in KDC_CHECKSUM schreiben                 |
|                                                         |
|  Key Usage für beide: 17                                |
|                                                         |
|  Bei Golden Ticket:                                     |
|    Server-Key = krbtgt-Key                              |
|    KDC-Key    = krbtgt-Key                              |
|                                                         |
|  Bei Silver Ticket:                                     |
|    Server-Key = Service-Account-Key                     |
|    KDC-Key    = Service-Account-Key  <- Beide gleich!   |
|                                                         |
|  Bei legitimem 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)    |
|                                            |
|  KEIN RODCIdentifier!                      |
|  (nur bei Read-Only DC relevant)           |
+--------------------------------------------+

Die Key-Derivation für PAC-Checksums

Hier wird es subtil. RFC 3961 definiert drei abgeleitete Keys:

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

PAC-Checksums verwenden Kc (Suffix 0x99), NICHT Ki (Suffix 0x55).

Warum ist das wichtig? Ki wird für Integritätsprüfungen innerhalb von verschlüsselten Daten verwendet (der HMAC am Ende eines AES-CTS-Ciphertexts). Kc wird für standalone Checksums verwendet — und genau das ist eine PAC-Signatur.

Diesen Unterschied zu übersehen führt zu einem frustrierenden Bug: Die Kryptographie ist technisch korrekt (gültiger HMAC), aber der KDC erwartet einen anderen Key -> STATUS_WRONG_PASSWORD (0x56).

# adPEAS nutzt deshalb Windows Native Crypto:
$checksum = Get-KerberosChecksumNative `
    -EncryptionType 18 `   # AES256 (mapped intern zu Checksum-Typ 16)
    -Key $krbtgtKey `
    -Data $pacBytes `
    -KeyUsage 17           # PAC Checksum usage

Bei Silver Tickets: Beide Checksums gleich

Normalerweise ist der Server-Checksum mit dem Service-Account-Key und der KDC-Checksum mit dem krbtgt-Key signiert. Aber bei Silver Tickets kennen wir den krbtgt-Key nicht — deshalb signieren wir beide mit dem Service-Account-Key.

Das funktioniert weil der Zielservice den KDC-Checksum normalerweise nicht an den KDC zur Validierung weiterleitet. Er prüft nur den Server-Checksum mit seinem eigenen Key.


Teil 5: Golden Ticket — Schritt für Schritt

Was brauchen wir?

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

Der Build-Prozess in adPEAS

+------------------------------------------------------------+
|                 New-GoldenTicket Flow                      |
+------------------------------------------------------------+
|                                                            |
|  1. Random Session Key generieren                          |
|     $sessionKey = [byte[]]::new(32)                        |
|     [Security.Cryptography.RNGCryptoServiceProvider]::     |
|       Create().GetBytes($sessionKey)                       |
|                                                            |
|  2. Timestamps setzen                                      |
|     $authTime = [DateTime]::UtcNow (auf Sekunde trunc.)    |
|     $endTime = $authTime.AddHours(10)                      |
|     $renewTill = $authTime.AddDays($ValidityDays)          |
|                                                            |
|  3. PAC bauen (Build-PAC)                                  |
|     |-- Build-KerbValidationInfo (NDR)                     |
|     |   +-- User-Info, Gruppen, 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. PAC-Checksums berechnen (Complete-PACSignatures)       |
|     |-- Beide Checksum-Felder auf NULL setzen              |
|     |-- Server-Checksum = HMAC(krbtgt, gesamter PAC)       |
|     |-- Server-Checksum eintragen                          |
|     +-- KDC-Checksum = HMAC(krbtgt, Server-Checksum)       |
|                                                            |
|  5. EncTicketPart bauen (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. EncTicketPart verschlüsseln                            |
|     $cipher = Protect-KerberosNative(                      |
|       EncryptionType=18, Key=$krbtgtKey,                   |
|       Data=$encTicketPart, KeyUsage=2                      |
|     )                                                      |
|                                                            |
|  7. Ticket (APPLICATION 1) bauen                           |
|     |-- tkt-vno: 5                                         |
|     |-- realm: CONTOSO.COM                                 |
|     |-- sname: krbtgt/CONTOSO.COM                          |
|     +-- enc-part: $cipher (etype 18, kvno 2)               |
|                                                            |
|  8. KRB-CRED bauen (Build-KRBCred)                         |
|     |-- tickets: [Ticket]                                  |
|     +-- enc-part: EncKrbCredPart (unverschlüsselt)         |
|         +-- key: $sessionKey                               |
|                                                            |
|  9. Optional: PTT via Import-KerberosTicket                |
|     +-- LSA API: LsaCallAuthenticationPackage              |
|                                                            |
+------------------------------------------------------------+
# adPEAS Aufruf:
Invoke-TicketForge -Mode Golden `
    -Domain "contoso.com" `
    -DomainSID "S-1-5-21-1234-5678-9012" `
    -AES256Key "52a4126c7ab14fe..." `
    -PTT

Was der KDC sieht (und was nicht)

Golden Ticket OPSEC-Profil:
+-----------------------------------------------------+
|  Event ID 4768 (TGT Request):      NICHT vorhanden  |
|    -> Kein AS-REQ wurde gesendet!                   |
|    -> Verdächtig: TGT existiert ohne AS-REQ         |
|                                                     |
|  Event ID 4769 (TGS Request):      Vorhanden        |
|    -> Wenn du das Golden Ticket für TGS-REQ nutzt   |
|    -> Sieht "normal" aus                            |
|                                                     |
|  Event ID 4770 (TGT Renew):        Möglich          |
|    -> Wenn ValidityDays sehr lang -> verdächtig     |
|                                                     |
|  Erkennung:                                         |
|  - Kein korrespondierender 4768-Event zum TGT       |
|  - Ungewöhnlich lange Ticket-Lebenszeit             |
|  - Gruppen die der User normalerweise nicht hat     |
|  - RC4-Verschlüsselung in AES-Umgebung              |
+-----------------------------------------------------+

Teil 6: Silver Ticket — Direkt zum Service

Die Unterschiede zum Golden Ticket

Golden Ticket:
  Ticket.sname = krbtgt/CONTOSO.COM
  Encrypted mit = krbtgt Key
  Ticket Flags: Initial = YES
  Nutzung: TGS-REQ -> Service Ticket -> Service

Silver Ticket:
  Ticket.sname = cifs/fileserver.contoso.com (oder anderer SPN)
  Encrypted mit = Service-Account Key
  Ticket Flags: Initial = NO
  Nutzung: Direkt zum Service (AP-REQ) -- kein KDC-Kontakt!

Der entscheidende Punkt: Ein Silver Ticket überspringt den KDC komplett. Du schickst das gefälschte Service Ticket direkt an den Zielservice. Der Service prüft:

  1. Kann ich den enc-part mit meinem Key entschlüsseln? -> Ja
  2. Ist der EncTicketPart gültig (Timestamps, Flags)? -> Ja
  3. Welche Gruppen hat der User (laut PAC)? -> Was auch immer du reingeschrieben hast

PAC-Signatur bei Silver Tickets

+-----------------------------------------------------+
|  Normales Service Ticket (vom KDC):                 |
|    Server-Checksum: HMAC(Service-Key, PAC)          |
|    KDC-Checksum:    HMAC(krbtgt-Key, Server-CS)     |
|                                                     |
|  Silver Ticket (gefälscht):                         |
|    Server-Checksum: HMAC(Service-Key, PAC)          |
|    KDC-Checksum:    HMAC(Service-Key, Server-CS)    |
|                     ^^^^^^^^^^^^^^^^                |
|                     Gleicher Key!                   |
|                                                     |
|  Funktioniert weil:                                 |
|    Der Service validiert nur den Server-Checksum    |
|    KDC-Checksum wird nur geprüft wenn der Service   |
|    PAC-Validation beim KDC anfordert (selten)       |
+-----------------------------------------------------+
# Silver Ticket für 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

Unterstützte Service-Typen

ServiceTypeSPN wird zuTypischer Key-Besitzer
CIFScifs/host.domain.comComputer Account (DC01$)
LDAPldap/host.domain.comComputer Account (DC01$)
HTTPhttp/host.domain.comComputer Account oder IIS-Account
HOSThost/host.domain.comComputer Account
MSSQLMSSQLSvc/host.domain.comSQL Service Account
WSMANwsman/host.domain.comComputer Account

Teil 7: Diamond Ticket — Der Hybrid-Ansatz

Warum Diamond statt Golden?

Golden Ticket Problem:
  DC-Logs: [4769: TGS-REQ von "admin"]
  DC-Logs: [kein 4768 für "admin"!]  <- Verdächtig!
  SOC: "Woher kommt dieses TGT? Es gibt keinen Login!"

Diamond Ticket:
  DC-Logs: [4768: AS-REQ von "lowpriv"]  <- Normaler Login
  DC-Logs: [4769: TGS-REQ von "lowpriv"]  <- Normaler Service-Zugriff
  SOC: "Sieht normal aus."
  Realität: lowpriv's TGT hat Domain Admins im PAC!

Der Hybrid-Ansatz im Detail

Das Diamond Ticket in adPEAS verwendet einen “Hybrid”-Ansatz: Es kombiniert ein echtes TGT mit der bewährten Golden-Ticket-Logik zum Neuaufbau des PAC.

Warum nicht einfach den PAC im Original-TGT patchen?

Weil NDR-Serialisierung nicht deterministisch ist. Wenn du den LOGON_INFO Buffer neu serialisierst (z.B. mit neuen Gruppen), erzeugt Build-KerbValidationInfo einen NDR-Blob der byte-für-byte anders ist als der originale. Auch wenn die logischen Daten gleich sind. Der KDC validiert den PAC-Checksum — und der stimmt nicht mehr.

Wir haben diesen Ansatz getestet. Ergebnis: KRB_AP_ERR_MODIFIED (Error 41) bei jedem TGS-REQ.

Die Lösung: Nimm das echte TGT, extrahiere die relevanten Metadaten (Timestamps, Session Key), und baue alles komplett neu mit der Golden-Ticket-Logik:

+----------------------------------------------------------+
|                 Diamond Ticket Hybrid Flow               |
+----------------------------------------------------------+
|                                                          |
|  Phase 1: Echtes TGT beschaffen                          |
|  -------------------------                               |
|  Input: BaseUser-Credentials (Password/Hash/Key/.kirbi)  |
|  -> AS-REQ an KDC (erzeugt Event 4768 in DC-Logs!)       |
|  -> AS-REP enthält echtes TGT + Session Key               |
|                                                          |
|  Phase 2: TGT entschlüsseln und parsen                   |
|  ----------------------------------                      |
|  1. KRB-CRED parsen (ASN.1)                              |
|  2. Ticket.enc-part mit krbtgt-Key entschlüsseln         |
|  3. EncTicketPart parsen:                                |
|     - Session Key extrahieren  <- KRITISCH: aufbewahren! |
|     - authtime extrahieren                               |
|     - PAC parsen -> UserName, Domain, SID, RID, Gruppen  |
|                                                          |
|  Phase 3: Neues Ticket mit Golden-Logik bauen            |
|  ------------------------------------------              |
|  1. Build-PAC mit gewünschten GroupRIDs                  |
|     (z.B. 512=DA, 519=EA -- statt der echten Gruppen)    |
|  2. Complete-PACSignatures mit krbtgt-Key                |
|  3. New-EncTicketPart mit:                               |
|     - ORIGINAL Session Key (nicht neu generieren!)       |
|     - ORIGINAL authtime (muss zu CLIENT_INFO passen)     |
|     - Neue Gruppen im PAC                                |
|  4. Encrypt mit krbtgt-Key (KeyUsage 2)                  |
|  5. Build Ticket + KRB-CRED mit ORIGINAL Session Key     |
|                                                          |
|  Warum Original Session Key?                             |
|  -----------------------------                           |
|  Der Client kennt den Session Key aus der AS-REP.        |
|  Der TGS-REQ Authenticator wird damit verschlüsselt.     |
|  Wenn wir einen neuen Session Key generieren, passt der  |
|  Authenticator nicht mehr -> TGS-REQ schlägt fehl.       |
|                                                          |
|  Bei Golden Tickets ist das kein Problem, weil wir       |
|  sowohl den Session Key im Ticket als auch im KRB-CRED   |
|  kontrollieren. Bei Diamond Tickets hat der Client       |
|  bereits den Original-Key.                               |
|                                                          |
+----------------------------------------------------------+

OPSEC-Modi

Modus 1: Stealthy (ohne -UserName)
----------------------------------
  BaseUser = lowpriv
  Ticket-User = lowpriv  <- Gleich wie AS-REQ!
  Gruppen = Domain Admins, Enterprise Admins

  DC-Logs: "lowpriv hat AS-REQ gemacht" ✓
  Ticket:  "lowpriv ist Domain Admin"
  Detection: Nur PAC-Inhalt-Analyse (sehr selten)

Modus 2: Impersonation (mit -UserName)
---------------------------------------
  BaseUser = lowpriv
  Ticket-User = Administrator  <- Anders als AS-REQ!
  Gruppen = Domain Admins, Enterprise Admins

  DC-Logs: "lowpriv hat AS-REQ gemacht" ✓
  Ticket:  "Administrator ist Domain Admin"
  Detection: Username-Mismatch zwischen 4768 und 4769

Encryption Type Constraint

Diamond Ticket Constraint:
  Base TGT etype MUSS zum krbtgt Key passen!

  Szenario:
    DC verschlüsselt TGT mit AES256 (etype 18)
    Du hast nur krbtgt NT-Hash (etype 23)
    -> FEHLER: Kann TGT nicht entschlüsseln!

  Lösung A: AES256-Key des krbtgt beschaffen (DCSync mit /all)
  Lösung B: Golden Ticket statt Diamond verwenden

Teil 8: PAC Buffer Konsistenz

Das Konsistenz-Problem

Alle PAC-Buffer müssen den gleichen User beschreiben. Bei einem Diamond Ticket mit Impersonation muss man alle Buffer aktualisieren:

Buffer                      Muss aktualisiert werden?
------------------------    ------------------------
LOGON_INFO (type 1)         Ja -- UserName, RID, Gruppen
CLIENT_INFO (type 10)       Ja -- Client Name
UPN_DNS_INFO (type 12)      Ja -- UPN, SamName, SID-RID
PAC_ATTRIBUTES (type 17)    Nein -- statisch (0x01)
PAC_REQUESTOR (type 18)     Ja -- letzte SubAuth (RID)
SERVER_CHECKSUM (type 6)    Ja -- nach allen Änderungen neu berechnen
KDC_CHECKSUM (type 7)       Ja -- nach Server-Checksum neu berechnen

Wenn LOGON_INFO sagt “RID 500 (Administrator)” aber PAC_REQUESTOR sagt “RID 1103 (lowpriv)” wird das Ticket abgelehnt. Rubeus’ ModifyTicket aktualisiert deshalb alle relevanten Buffer konsistent.

Was passiert bei Inkonsistenz?

InkonsistenzErgebnis
LOGON_INFO RID ≠ PAC_REQUESTOR RIDLogon-Fehler (Ticket abgelehnt)
CLIENT_INFO Name ≠ EncTicketPart cname”invalid credential”
CLIENT_INFO Timestamp ≠ EncTicketPart authtime”invalid credential”
PAC_REQUESTOR fehltLogon-Fehler (auf gepatchten DCs)
TICKET_CHECKSUM vorhandenLogon-Fehler (KDC kann es nicht validieren)
Server-Checksum falschKRB_AP_ERR_MODIFIED (Error 41)

Teil 9: Detection und Countermeasures

Was Defender sehen können

Event ID 4768 (TGT Request):

  • Golden Ticket: Kein Event -> verdächtig
  • Diamond Ticket: Normales Event -> stealthy

Event ID 4769 (TGS Request):

  • Encryption Type 0x17 (RC4) in AES-Umgebung -> verdächtig
  • Unbekannte Gruppen für den User -> nur mit PAC-Analyse

PAC-basierte Detection:

  • Microsoft Defender for Identity kann PAC-Inhalte analysieren
  • Vergleich: “Hat User X wirklich Domain Admins Mitgliedschaft?”
  • LOGON_INFO-Gruppen vs. tatsächliche AD-Gruppenmitgliedschaft

Ticket-Lifetime Anomalien:

  • Golden Ticket mit 10 Jahren Gültigkeit -> offensichtlich gefälscht
  • Diamond Ticket übernimmt Domain-Policy -> unauffällig

Countermeasures

MaßnahmeGegenEffektivität
krbtgt Key RotationGolden + DiamondHoch (invalidiert alle TGTs)
Credential GuardHash-Dumps für Key-MaterialHoch
PAC Validation (KDC)Silver TicketsMittel (Performance-Impact)
AES-Only PolicyRC4-basierte TicketsMittel (reduziert Angriffsfläche)
Protected UsersDiverseMittel (keine Delegation, kein RC4)
MDI/ATAAnomalie-DetectionHoch (PAC-Analyse, TGT-Anomalien)
Frequent krbtgt RotationPersistence via GoldenHoch (empfohlen: alle 180 Tage)

krbtgt Key Rotation

Der effektivste Schutz gegen Golden und Diamond Tickets:

Rotation 1: Neuer krbtgt Key (kvno 3)
  -> Alte Tickets (kvno 2) funktionieren noch!
  -> Kerberos erlaubt N und N-1

Rotation 2 (nach Replikation): Neuer krbtgt Key (kvno 4)
  -> Jetzt funktioniert kvno 2 nicht mehr
  -> Nur kvno 3 und 4 sind gültig
  -> Alle Golden/Diamond Tickets mit dem alten Key sind ungültig

Empfehlung: Zwei Rotationen mit 12-24h Abstand

Zusammenfassung

Die Ticket-Forging-Hierarchie

+------------------------------------------------------+
|                                                      |
|  Golden Ticket                                       |
|  |-- Benötigt: krbtgt Key                            |
|  |-- OPSEC: Schlecht (kein AS-REQ in Logs)           |
|  |-- Scope: Gesamte Domain                           |
|  +-- Komplexität: Am einfachsten                     |
|                                                      |
|  Silver Ticket                                       |
|  |-- Benötigt: Service-Account Key                   |
|  |-- OPSEC: Mittel (kein TGS-REQ in Logs)            |
|  |-- Scope: Ein bestimmter Service                   |
|  +-- Komplexität: Mittel (SPN muss bekannt sein)     |
|                                                      |
|  Diamond Ticket                                      |
|  |-- Benötigt: krbtgt Key + User Credentials         |
|  |-- OPSEC: Gut (echter AS-REQ in Logs)              |
|  |-- Scope: Gesamte Domain                           |
|  +-- Komplexität: Am höchsten (Hybrid-Ansatz)        |
|                                                      |
+------------------------------------------------------+

Was du jetzt verstehen solltest

  1. Der PAC ist eine Microsoft-Erweiterung mit User-Identität und Gruppen im Kerberos-Ticket
  2. NDR-Serialisierung ist das Encoding für LOGON_INFO — mit Referents, Alignment, und RPC_UNICODE_STRING
  3. 7 PAC-Buffer sind auf modernen DCs erforderlich (CVE-2021-42287)
  4. PAC-Checksums verwenden Kc (0x99), nicht Ki (0x55) — ein subtiler aber kritischer Unterschied
  5. Golden Tickets bauen alles von Grund auf — maximale Kontrolle, schlechte OPSEC
  6. Silver Tickets überspringen den KDC komplett — limitierter Scope, gute OPSEC
  7. Diamond Tickets modifizieren echte TGTs — bester OPSEC-Kompromiss, höchste Komplexität
  8. Session Key Preservation ist bei Diamond Tickets kritisch — sonst scheitert der TGS-REQ
  9. Buffer-Konsistenz zwischen LOGON_INFO, CLIENT_INFO, UPN_DNS_INFO und PAC_REQUESTOR ist Pflicht
  10. krbtgt Key Rotation (2x) ist die effektivste Gegenmaßnahme

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

Über den Autor

Alexander Sturz

Gründer & Red Team Lead

Active Directory Ninja und Experte für offensive Sicherheit mit Spezialisierung auf Kompromittierung von Unternehmensinfrastrukturen und Post-Exploitation-Techniken.

Verwandte Artikel