Private/AD/Core/Get-ADKerberosConfig.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Get-ADKerberosConfig {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Connection,

        [switch]$Quiet
    )

    $result = @{
        KerberoastableAccounts    = @()
        ASREPRoastableAccounts    = @()
        UnconstrainedDelegation   = @()
        ConstrainedDelegation     = @()
        RBCD                      = @()
        ProtocolTransition        = @()
        EncryptionTypes           = @{}
        KerberosPolicy            = @{}
    }

    $searchRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $Connection.DomainDN

    # ── Kerberoastable Accounts ───────────────────────────────────────────────
    # User accounts (not computers, not disabled) with SPNs set
    Write-Verbose 'Querying Kerberoastable accounts (users with SPNs)...'
    try {
        $kerberoastable = Invoke-LdapQuery -SearchRoot $searchRoot `
            -Filter '(&(objectCategory=person)(objectClass=user)(servicePrincipalName=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' `
            -Properties @('samaccountname', 'distinguishedname', 'serviceprincipalname', 'pwdlastset', 'admincount', 'useraccountcontrol', 'msds-supportedencryptiontypes')

        $kerbList = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($acct in $kerberoastable) {
            $spns = $acct['serviceprincipalname']
            if ($spns -isnot [array]) { $spns = @($spns) }

            $kerbList.Add(@{
                SamAccountName      = $acct['samaccountname'] ?? ''
                DN                  = $acct['distinguishedname'] ?? ''
                SPNs                = @($spns)
                PwdLastSet          = $acct['pwdlastset']
                AdminCount          = [int]($acct['admincount'] ?? 0)
                UserAccountControl  = [int]($acct['useraccountcontrol'] ?? 0)
                EncryptionTypes     = [int]($acct['msds-supportedencryptiontypes'] ?? 0)
            })
        }

        $result.KerberoastableAccounts = @($kerbList)
        Write-Verbose "Found $($kerbList.Count) Kerberoastable account(s)."
    } catch {
        Write-Warning "Failed to query Kerberoastable accounts: $_"
    }

    # ── AS-REP Roastable Accounts ─────────────────────────────────────────────
    # Users with DONT_REQ_PREAUTH (0x400000 = 4194304)
    Write-Verbose 'Querying AS-REP roastable accounts (DONT_REQ_PREAUTH)...'
    try {
        $asrepAccounts = Invoke-LdapQuery -SearchRoot $searchRoot `
            -Filter '(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))' `
            -Properties @('samaccountname', 'distinguishedname', 'useraccountcontrol', 'pwdlastset', 'admincount')

        $asrepList = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($acct in $asrepAccounts) {
            $asrepList.Add(@{
                SamAccountName     = $acct['samaccountname'] ?? ''
                DN                 = $acct['distinguishedname'] ?? ''
                UserAccountControl = [int]($acct['useraccountcontrol'] ?? 0)
                PwdLastSet         = $acct['pwdlastset']
                AdminCount         = [int]($acct['admincount'] ?? 0)
            })
        }

        $result.ASREPRoastableAccounts = @($asrepList)
        Write-Verbose "Found $($asrepList.Count) AS-REP roastable account(s)."
    } catch {
        Write-Warning "Failed to query AS-REP roastable accounts: $_"
    }

    # ── Unconstrained Delegation ──────────────────────────────────────────────
    # TRUSTED_FOR_DELEGATION (0x80000 = 524288), excluding DCs (SERVER_TRUST_ACCOUNT 0x2000)
    Write-Verbose 'Querying unconstrained delegation objects...'
    try {
        $unconstrainedAll = Invoke-LdapQuery -SearchRoot $searchRoot `
            -Filter '(&(userAccountControl:1.2.840.113556.1.4.803:=524288)(!(userAccountControl:1.2.840.113556.1.4.803:=8192)))' `
            -Properties @('samaccountname', 'distinguishedname', 'objectclass', 'useraccountcontrol', 'dnshostname')

        $unconstrainedList = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($obj in $unconstrainedAll) {
            $objClasses = $obj['objectclass']
            if ($objClasses -isnot [array]) { $objClasses = @($objClasses) }

            $unconstrainedList.Add(@{
                SamAccountName     = $obj['samaccountname'] ?? ''
                DN                 = $obj['distinguishedname'] ?? ''
                ObjectClass        = $objClasses
                UserAccountControl = [int]($obj['useraccountcontrol'] ?? 0)
                DnsHostName        = $obj['dnshostname'] ?? ''
            })
        }

        $result.UnconstrainedDelegation = @($unconstrainedList)
        Write-Verbose "Found $($unconstrainedList.Count) unconstrained delegation object(s) (excluding DCs)."
    } catch {
        Write-Warning "Failed to query unconstrained delegation: $_"
    }

    # ── Constrained Delegation ────────────────────────────────────────────────
    # Objects with msDS-AllowedToDelegateTo populated
    Write-Verbose 'Querying constrained delegation objects...'
    try {
        $constrained = Invoke-LdapQuery -SearchRoot $searchRoot `
            -Filter '(msDS-AllowedToDelegateTo=*)' `
            -Properties @('samaccountname', 'distinguishedname', 'objectclass', 'useraccountcontrol', 'msds-allowedtodelegateto')

        $constrainedList = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($obj in $constrained) {
            $allowedTo = $obj['msds-allowedtodelegateto']
            if ($allowedTo -isnot [array]) { $allowedTo = @($allowedTo) }
            $objClasses = $obj['objectclass']
            if ($objClasses -isnot [array]) { $objClasses = @($objClasses) }

            $constrainedList.Add(@{
                SamAccountName         = $obj['samaccountname'] ?? ''
                DN                     = $obj['distinguishedname'] ?? ''
                ObjectClass            = $objClasses
                UserAccountControl     = [int]($obj['useraccountcontrol'] ?? 0)
                AllowedToDelegateTo    = @($allowedTo)
            })
        }

        $result.ConstrainedDelegation = @($constrainedList)
        Write-Verbose "Found $($constrainedList.Count) constrained delegation object(s)."
    } catch {
        Write-Warning "Failed to query constrained delegation: $_"
    }

    # ── Resource-Based Constrained Delegation (RBCD) ──────────────────────────
    # Objects with msDS-AllowedToActOnBehalfOfOtherIdentity populated
    Write-Verbose 'Querying resource-based constrained delegation objects...'
    try {
        $rbcd = Invoke-LdapQuery -SearchRoot $searchRoot `
            -Filter '(msDS-AllowedToActOnBehalfOfOtherIdentity=*)' `
            -Properties @('samaccountname', 'distinguishedname', 'objectclass', 'msds-allowedtoactonbehalfofotheridentity')

        $rbcdList = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($obj in $rbcd) {
            $rawSD = $obj['msds-allowedtoactonbehalfofotheridentity']
            $allowedPrincipals = @()

            # Parse the security descriptor to extract allowed principals
            if ($rawSD -is [byte[]]) {
                try {
                    $sd = New-Object System.DirectoryServices.ActiveDirectorySecurity
                    $sd.SetSecurityDescriptorBinaryForm($rawSD)
                    $rules = $sd.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
                    $allowedPrincipals = @(foreach ($rule in $rules) {
                        $sidStr = $rule.IdentityReference.Value
                        $resolved = Resolve-ADSid -SidString $sidStr -SearchRoot $searchRoot
                        @{
                            SID      = $sidStr
                            Identity = $resolved
                            Rights   = $rule.ActiveDirectoryRights.ToString()
                        }
                    })
                } catch {
                    Write-Verbose "Could not parse RBCD descriptor for $($obj['distinguishedname']): $_"
                }
            }

            $objClasses = $obj['objectclass']
            if ($objClasses -isnot [array]) { $objClasses = @($objClasses) }

            $rbcdList.Add(@{
                SamAccountName    = $obj['samaccountname'] ?? ''
                DN                = $obj['distinguishedname'] ?? ''
                ObjectClass       = $objClasses
                AllowedPrincipals = $allowedPrincipals
            })
        }

        $result.RBCD = @($rbcdList)
        Write-Verbose "Found $($rbcdList.Count) RBCD object(s)."
    } catch {
        Write-Warning "Failed to query RBCD: $_"
    }

    # ── Protocol Transition ───────────────────────────────────────────────────
    # TRUSTED_TO_AUTH_FOR_DELEGATION (0x1000000 = 16777216)
    Write-Verbose 'Querying protocol transition (T2A4D) objects...'
    try {
        $t2a4d = Invoke-LdapQuery -SearchRoot $searchRoot `
            -Filter '(userAccountControl:1.2.840.113556.1.4.803:=16777216)' `
            -Properties @('samaccountname', 'distinguishedname', 'objectclass', 'useraccountcontrol', 'msds-allowedtodelegateto')

        $t2a4dList = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($obj in $t2a4d) {
            $allowedTo = $obj['msds-allowedtodelegateto']
            if ($null -ne $allowedTo -and $allowedTo -isnot [array]) { $allowedTo = @($allowedTo) }
            $objClasses = $obj['objectclass']
            if ($objClasses -isnot [array]) { $objClasses = @($objClasses) }

            $t2a4dList.Add(@{
                SamAccountName      = $obj['samaccountname'] ?? ''
                DN                  = $obj['distinguishedname'] ?? ''
                ObjectClass         = $objClasses
                UserAccountControl  = [int]($obj['useraccountcontrol'] ?? 0)
                AllowedToDelegateTo = @($allowedTo ?? @())
            })
        }

        $result.ProtocolTransition = @($t2a4dList)
        Write-Verbose "Found $($t2a4dList.Count) protocol transition object(s)."
    } catch {
        Write-Warning "Failed to query protocol transition objects: $_"
    }

    # ── Encryption Types on Domain Controllers ────────────────────────────────
    Write-Verbose 'Analyzing msDS-SupportedEncryptionTypes across domain controllers...'
    try {
        $dcResults = Invoke-LdapQuery -SearchRoot $searchRoot `
            -Filter '(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))' `
            -Properties @('samaccountname', 'distinguishedname', 'msds-supportedencryptiontypes', 'operatingsystem')

        $encTypeFlags = @{
            1  = 'DES-CBC-CRC'
            2  = 'DES-CBC-MD5'
            4  = 'RC4-HMAC'
            8  = 'AES128-CTS-HMAC-SHA1'
            16 = 'AES256-CTS-HMAC-SHA1'
        }

        $dcEncTypes = [System.Collections.Generic.List[hashtable]]::new()
        foreach ($dc in $dcResults) {
            $encVal = [int]($dc['msds-supportedencryptiontypes'] ?? 0)
            $supported = [System.Collections.Generic.List[string]]::new()

            foreach ($flag in $encTypeFlags.GetEnumerator()) {
                if ($encVal -band $flag.Key) {
                    $supported.Add($flag.Value)
                }
            }

            $dcEncTypes.Add(@{
                SamAccountName     = $dc['samaccountname'] ?? ''
                DN                 = $dc['distinguishedname'] ?? ''
                OperatingSystem    = $dc['operatingsystem'] ?? ''
                EncryptionTypeValue = $encVal
                SupportedTypes     = @($supported)
                HasDES             = ($encVal -band 3) -ne 0
                HasRC4             = ($encVal -band 4) -ne 0
                HasAES             = ($encVal -band 24) -ne 0
            })
        }

        # Build summary
        $desCount  = @($dcEncTypes | Where-Object { $_.HasDES }).Count
        $rc4Count  = @($dcEncTypes | Where-Object { $_.HasRC4 }).Count
        $aesCount  = @($dcEncTypes | Where-Object { $_.HasAES }).Count
        $totalDCs  = $dcEncTypes.Count

        $result.EncryptionTypes = @{
            DomainControllers = @($dcEncTypes)
            Summary           = @{
                TotalDCs       = $totalDCs
                DESEnabled     = $desCount
                RC4Enabled     = $rc4Count
                AESEnabled     = $aesCount
            }
        }

        Write-Verbose "DC encryption analysis: $totalDCs DCs, DES=$desCount, RC4=$rc4Count, AES=$aesCount."
    } catch {
        Write-Warning "Failed to analyze DC encryption types: $_"
    }

    # ── Kerberos Policy from Default Domain Policy SYSVOL ─────────────────────
    Write-Verbose 'Attempting to read Kerberos policy from domain GPO SYSVOL...'
    try {
        # The Default Domain Policy is always {31B2F340-016D-11D2-945F-00C04FB984F9}
        $defaultGPOGuid = '{31B2F340-016D-11D2-945F-00C04FB984F9}'

        # Derive the FQDN from the DomainDN
        $domainFqdn = ($Connection.DomainDN -replace ',DC=', '.' -replace '^DC=', '')

        $kerbInfPath = "\\$domainFqdn\SYSVOL\$domainFqdn\Policies\$defaultGPOGuid\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf"

        if (Test-Path -LiteralPath $kerbInfPath -ErrorAction SilentlyContinue) {
            $infContent = Get-Content -LiteralPath $kerbInfPath -ErrorAction Stop
            $inKerberosSection = $false
            $kerbPolicy = @{}

            foreach ($line in $infContent) {
                $trimmed = $line.Trim()
                if ($trimmed -match '^\[(.+)\]$') {
                    $inKerberosSection = ($Matches[1] -eq 'Kerberos Policy')
                    continue
                }
                if ($inKerberosSection -and $trimmed -match '^(.+?)\s*=\s*(.+)$') {
                    $key = $Matches[1].Trim()
                    $val = $Matches[2].Trim()
                    switch ($key) {
                        'MaxTicketAge'  { $kerbPolicy['MaxTicketAge']  = [int]$val }
                        'MaxRenewAge'   { $kerbPolicy['MaxRenewAge']   = [int]$val }
                        'MaxServiceAge' { $kerbPolicy['MaxServiceAge'] = [int]$val }
                        'MaxClockSkew'  { $kerbPolicy['MaxClockSkew']  = [int]$val }
                        'TicketValidateClient' { $kerbPolicy['TicketValidateClient'] = [int]$val }
                    }
                }
            }

            $result.KerberosPolicy = $kerbPolicy
            Write-Verbose "Kerberos policy loaded: MaxTicketAge=$($kerbPolicy['MaxTicketAge'] ?? 'N/A')h, MaxClockSkew=$($kerbPolicy['MaxClockSkew'] ?? 'N/A')min."
        } else {
            Write-Verbose "SYSVOL Kerberos policy file not accessible: $kerbInfPath"
            $result.KerberosPolicy = @{ Error = 'SYSVOL not accessible' }
        }
    } catch {
        Write-Verbose "Failed to read Kerberos policy from SYSVOL: $_"
        $result.KerberosPolicy = @{ Error = "Failed: $_" }
    }

    return $result
}