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.
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:
| Type | Name | Beschreibung | Größe |
|---|---|---|---|
| 1 | LOGON_INFO | User-Identität, Gruppen, Domain-SID | ~500-2000 Bytes |
| 6 | SERVER_CHECKSUM | HMAC-Signatur über den gesamten PAC | 20-28 Bytes |
| 7 | KDC_CHECKSUM | HMAC-Signatur über den Server-Checksum | 20-28 Bytes |
| 10 | CLIENT_INFO | Client-Name + Auth-Zeitstempel | ~30-60 Bytes |
| 12 | UPN_DNS_INFO | UPN, DNS-Domain, SamName, SID | ~100-200 Bytes |
| 17 | PAC_ATTRIBUTES | Flags (PAC_WAS_REQUESTED) | 8 Bytes |
| 18 | PAC_REQUESTOR | User-SID (raw) | ~28 Bytes |
Ein weiterer Buffer der bei PKINIT-Authentifizierung vom KDC erzeugt wird:
| Type | Name | Beschreibung | Größe |
|---|---|---|---|
| 2 | CREDENTIAL_INFO | Verschlü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:
| Type | Name | Warum weglassen? |
|---|---|---|
| 16 | TICKET_CHECKSUM | Nur vom KDC berechenbar (benötigt krbtgt-Key des jeweiligen TGS) |
| 19 | FULL_PAC_CHECKSUM | Nur 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?
| Zutat | Woher? | Beispiel |
|---|---|---|
| krbtgt Key | DCSync, NTDS.dit | AES256: 52a4126c7ab14fe... |
| Domain SID | Get-DomainObject | S-1-5-21-1234-5678-9012 |
| Domain Name | bekannt | CONTOSO.COM |
| Domain NetBIOS | Get-DomainObject | CONTOSO |
| User RID | optional (Default: 500) | 500 (Administrator) |
| Group RIDs | optional (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:
- Kann ich den enc-part mit meinem Key entschlüsseln? -> Ja
- Ist der EncTicketPart gültig (Timestamps, Flags)? -> Ja
- 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
| ServiceType | SPN wird zu | Typischer Key-Besitzer |
|---|---|---|
| CIFS | cifs/host.domain.com | Computer Account (DC01$) |
| LDAP | ldap/host.domain.com | Computer Account (DC01$) |
| HTTP | http/host.domain.com | Computer Account oder IIS-Account |
| HOST | host/host.domain.com | Computer Account |
| MSSQL | MSSQLSvc/host.domain.com | SQL Service Account |
| WSMAN | wsman/host.domain.com | Computer 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?
| Inkonsistenz | Ergebnis |
|---|---|
| LOGON_INFO RID ≠ PAC_REQUESTOR RID | Logon-Fehler (Ticket abgelehnt) |
| CLIENT_INFO Name ≠ EncTicketPart cname | ”invalid credential” |
| CLIENT_INFO Timestamp ≠ EncTicketPart authtime | ”invalid credential” |
| PAC_REQUESTOR fehlt | Logon-Fehler (auf gepatchten DCs) |
| TICKET_CHECKSUM vorhanden | Logon-Fehler (KDC kann es nicht validieren) |
| Server-Checksum falsch | KRB_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ßnahme | Gegen | Effektivität |
|---|---|---|
| krbtgt Key Rotation | Golden + Diamond | Hoch (invalidiert alle TGTs) |
| Credential Guard | Hash-Dumps für Key-Material | Hoch |
| PAC Validation (KDC) | Silver Tickets | Mittel (Performance-Impact) |
| AES-Only Policy | RC4-basierte Tickets | Mittel (reduziert Angriffsfläche) |
| Protected Users | Diverse | Mittel (keine Delegation, kein RC4) |
| MDI/ATA | Anomalie-Detection | Hoch (PAC-Analyse, TGT-Anomalien) |
| Frequent krbtgt Rotation | Persistence via Golden | Hoch (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
- Der PAC ist eine Microsoft-Erweiterung mit User-Identität und Gruppen im Kerberos-Ticket
- NDR-Serialisierung ist das Encoding für LOGON_INFO — mit Referents, Alignment, und RPC_UNICODE_STRING
- 7 PAC-Buffer sind auf modernen DCs erforderlich (CVE-2021-42287)
- PAC-Checksums verwenden Kc (0x99), nicht Ki (0x55) — ein subtiler aber kritischer Unterschied
- Golden Tickets bauen alles von Grund auf — maximale Kontrolle, schlechte OPSEC
- Silver Tickets überspringen den KDC komplett — limitierter Scope, gute OPSEC
- Diamond Tickets modifizieren echte TGTs — bester OPSEC-Kompromiss, höchste Komplexität
- Session Key Preservation ist bei Diamond Tickets kritisch — sonst scheitert der TGS-REQ
- Buffer-Konsistenz zwischen LOGON_INFO, CLIENT_INFO, UPN_DNS_INFO und PAC_REQUESTOR ist Pflicht
- krbtgt Key Rotation (2x) ist die effektivste Gegenmaßnahme
← Episode 7: Kerberos Internals | Episode 9: Tips & Tricks — coming soon
Schlagwörter
Über den Autor
Verwandte Artikel
adPEAS v2 Episode 6: Offensive Operations - AD manipulieren mit adPEAS
Praxis-Guide für die offensiven Funktionen von adPEAS v2: Privilege Escalation, Persistence, Lateral Movement, GPO Abuse, ADCS-Exploitation und Kerberos Ticket Forging.
adPEAS v2 Episode 2: Unter der Haube - Anatomie eines Scans
Was passiert, wenn adPEAS ein Active Directory scannt? Von Authentifizierung und LDAP-Abfragen bis hin zu kontextabhängigen Severity-Bewertungen und Caching — ein Blick unter die Haube.
adPEAS v2 Blog-Serie: Active Directory Sicherheitsanalyse mit adPEAS
Einführung in adPEAS v2 — eine komplette Neuentwicklung des PowerShell-basierten Active Directory Analyse-Tools mit nativem Kerberos-Support, null Abhängigkeiten und über 40 Security-Checks.