Public/Get-SPNReport.ps1
|
function Get-SPNReport { <# .SYNOPSIS Audits Service Principal Names (SPNs) for Kerberoasting risk. .DESCRIPTION Identifies user accounts with Service Principal Names registered in Active Directory. User accounts with SPNs are vulnerable to Kerberoasting attacks - any authenticated domain user can request a TGS ticket encrypted with the service account's password hash, then crack it offline. This function flags such accounts and evaluates their risk based on encryption types and password age. .PARAMETER SearchBase The AD distinguished name to scope the search. Defaults to the current domain root. .PARAMETER IncludeComputers Include computer accounts in the report. By default, only user accounts are returned since computer accounts change their passwords automatically every 30 days. .EXAMPLE Get-SPNReport .EXAMPLE Get-SPNReport -SearchBase "DC=contoso,DC=com" -IncludeComputers .NOTES Kerberoasting context: - Any authenticated user can request a service ticket for any SPN - The ticket is encrypted with the service account's NTLM hash - Attackers crack these tickets offline (no lockout, no detection) - Only user accounts with SPNs are practical targets (computer passwords are random 120+ chars) #> [CmdletBinding()] param( [Parameter()] [string]$SearchBase, [Parameter()] [switch]$IncludeComputers ) begin { Write-Verbose "Starting SPN / Kerberoasting audit" $ADProperties = @( 'SAMAccountName' 'DisplayName' 'DistinguishedName' 'Enabled' 'PasswordLastSet' 'PasswordNeverExpires' 'ServicePrincipalName' 'msDS-SupportedEncryptionTypes' 'LastLogonDate' 'Description' 'MemberOf' 'ObjectClass' ) $Results = [System.Collections.Generic.List[PSObject]]::new() $Now = Get-Date # Encryption type flags (from MS-KILE) # These map to the msDS-SupportedEncryptionTypes attribute $EncTypeMap = @{ 0x1 = 'DES-CBC-CRC' 0x2 = 'DES-CBC-MD5' 0x4 = 'RC4-HMAC' 0x8 = 'AES128-CTS-HMAC-SHA1' 0x10 = 'AES256-CTS-HMAC-SHA1' } $WeakEncryption = @('DES-CBC-CRC', 'DES-CBC-MD5', 'RC4-HMAC') } process { # Query for user accounts with SPNs Write-Verbose "Searching for user accounts with SPNs" $UserParams = @{ Filter = "ServicePrincipalName -like '*'" Properties = $ADProperties } if ($SearchBase) { $UserParams['SearchBase'] = $SearchBase } try { $UserAccounts = @(Get-ADUser @UserParams -ErrorAction SilentlyContinue) Write-Verbose "Found $($UserAccounts.Count) user accounts with SPNs" } catch { Write-Warning "User SPN search failed: $_" $UserAccounts = @() } # Query for computer accounts with SPNs (if requested) $ComputerAccounts = @() if ($IncludeComputers) { Write-Verbose "Searching for computer accounts with SPNs" $CompParams = @{ Filter = "ServicePrincipalName -like '*'" Properties = $ADProperties } if ($SearchBase) { $CompParams['SearchBase'] = $SearchBase } try { $ComputerAccounts = @(Get-ADComputer @CompParams -ErrorAction SilentlyContinue) Write-Verbose "Found $($ComputerAccounts.Count) computer accounts with SPNs" } catch { Write-Warning "Computer SPN search failed: $_" $ComputerAccounts = @() } } $AllAccounts = @($UserAccounts) + @($ComputerAccounts) foreach ($Account in $AllAccounts) { if (-not $Account.ServicePrincipalName -or $Account.ServicePrincipalName.Count -eq 0) { continue } # Calculate password age $PasswordAge = if ($Account.PasswordLastSet) { [math]::Round(($Now - $Account.PasswordLastSet).TotalDays) } else { -1 } # Decode supported encryption types $EncValue = $Account.'msDS-SupportedEncryptionTypes' $SupportedEncryption = [System.Collections.Generic.List[string]]::new() if ($null -ne $EncValue -and $EncValue -ne 0) { foreach ($Flag in $EncTypeMap.Keys) { if ($EncValue -band $Flag) { $SupportedEncryption.Add($EncTypeMap[$Flag]) } } } else { # No encryption types set - defaults to RC4 $SupportedEncryption.Add('RC4-HMAC (default)') } $EncryptionDisplay = $SupportedEncryption -join ', ' # Check for weak encryption $HasWeakEncryption = $false foreach ($EncType in $SupportedEncryption) { foreach ($Weak in $WeakEncryption) { if ($EncType -like "$Weak*") { $HasWeakEncryption = $true break } } if ($HasWeakEncryption) { break } } # Determine if this is a user or computer account $IsUserAccount = ($Account.ObjectClass -ne 'computer') # Process each SPN on the account foreach ($SPN in $Account.ServicePrincipalName) { $Findings = [System.Collections.Generic.List[string]]::new() if ($IsUserAccount) { $Findings.Add('KERBEROASTABLE') } if ($HasWeakEncryption) { $Findings.Add('WEAK ENCRYPTION') } if ($IsUserAccount -and $PasswordAge -gt 365) { $Findings.Add('OLD PASSWORD + SPN') } if ($Account.PasswordNeverExpires -and $IsUserAccount) { $Findings.Add('PASSWORD NEVER EXPIRES') } if (-not $Account.Enabled) { $Findings.Add('ACCOUNT DISABLED') } $Result = [PSCustomObject]@{ SAMAccountName = $Account.SAMAccountName SPN = $SPN PasswordLastSet = $Account.PasswordLastSet PasswordAge = $PasswordAge Enabled = $Account.Enabled EncryptionTypes = $EncryptionDisplay IsUserAccount = $IsUserAccount Finding = ($Findings -join '; ') } $Result.PSObject.TypeNames.Insert(0, 'ServiceAccountAudit.SPN') $Results.Add($Result) } } } end { Write-Verbose "SPN audit complete. $($Results.Count) SPN entries analyzed." $Results | Sort-Object -Property @{Expression = 'IsUserAccount'; Descending = $true }, PasswordAge -Descending } } |