From 295d5f84fe151034d1484734786a3689652dbf17 Mon Sep 17 00:00:00 2001 From: Sascha Feig Date: Thu, 18 Jun 2026 14:25:34 +0200 Subject: [PATCH] Resolve well-known principals by SID for localized DCs On non-English DCs, ADeleg writes well-known principals with their localized CN (e.g. "Domaenen-Admins" instead of "Domain Admins"), so the hardcoded English patterns in $UnsafeTrustees / $Tier0Resources never match and insecure delegations are silently missed ("Eureka!"). Resolve each well-known principal's CN by its SID straight from the directory -- the same source ADeleg reads -- and append it to the patterns. On English domains this re-adds the English names (a harmless no-op); on localized domains it adds the localized CNs. Universal SIDs (Everyone, Authenticated Users) are always emitted in English by ADeleg, so they stay as the static patterns. Read-only Domain Controllers, which was missing from the Tier-0 list even on English AD, is now covered too. Falls back to the English patterns with a warning if the domain SID cannot be resolved. Co-Authored-By: Claude Opus 4.8 (1M context) --- Invoke-ADeleginator.ps1 | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/Invoke-ADeleginator.ps1 b/Invoke-ADeleginator.ps1 index 7aeaa4b..60a7aad 100644 --- a/Invoke-ADeleginator.ps1 +++ b/Invoke-ADeleginator.ps1 @@ -29,6 +29,28 @@ function Invoke-ADeleginator { ([ADSISearcher]"samaccountname=$($env:USERNAME)").Findone().Properties.memberof -replace '^CN=([^,]+).+$','$1' } + # Resolve the directory CN of a well-known principal by its SID. On non-English + # DCs these objects carry a localized CN (e.g. "Domaenen-Admins"); reading the CN + # straight from the directory -- the same source ADeleg reads -- returns the exact + # name ADeleg writes to its report, regardless of the OS or domain UI language. + function Get-LocalizedWellKnownName { + [CmdletBinding()] + Param( + [System.Security.Principal.WellKnownSidType]$WellKnownSid, + [System.Security.Principal.SecurityIdentifier]$DomainSid + ) + + try { + $Sid = [System.Security.Principal.SecurityIdentifier]::new($WellKnownSid, $DomainSid) + $Searcher = [ADSISearcher]"(objectSid=$($Sid.Value))" + $Searcher.PropertiesToLoad.Add('cn') > $null + $Result = $Searcher.FindOne() + if ($Result) { $Result.Properties['cn'][0] } + } catch { + Write-Verbose "Could not resolve well-known SID '$WellKnownSid': $_" + } + } + # Create ADeleg csv or json report in the current directory function Create-ADelegReport{ [CmdletBinding()] @@ -101,6 +123,43 @@ function Invoke-ADeleginator { $Tier0Resources = 'Account Operators|Administrator|Administrators|AdminSDHolder|Backup Operators|Cryptographic Operators|Distributed COM Users|Domain Admins|Domain Controllers|Domain Controllers (OU)|Domain root object|DnsAdmins|Enterprise Admins|GPO linked to Tier Zero container|krbtgt|Print Operators|RODC computer object|Schema Admins|Server Operators|Users (container)' $UnsafeDelegations = 'owns|write all properties|create child objects|delete child objects|Change the owner|add/delete delegations|delete' + # The patterns above are English. On a localized DC, ADeleg writes these + # principals with their localized CN (e.g. "Domaenen-Admins" for Domain Admins), + # so the English patterns never match and insecure delegations are silently + # missed ("Eureka!"). Resolve each well-known principal's CN by its SID and add + # it to the patterns. On English domains this re-adds the English names (a + # harmless no-op). Universal SIDs (Everyone, Authenticated Users) are always + # written in English by ADeleg, so they stay as the static patterns above. + try { + $DomainSid = [System.Security.Principal.SecurityIdentifier]::new([byte[]]([ADSI]'').objectSid.Value, 0).AccountDomainSid + $WKS = [System.Security.Principal.WellKnownSidType] + + $DomainUsersName = Get-LocalizedWellKnownName -WellKnownSid $WKS::AccountDomainUsersSid -DomainSid $DomainSid + if ($DomainUsersName) { $UnsafeTrustees += '|' + [regex]::Escape($DomainUsersName) } + + $Tier0WellKnownSids = @( + $WKS::BuiltinAdministratorsSid + $WKS::BuiltinAccountOperatorsSid + $WKS::BuiltinSystemOperatorsSid + $WKS::BuiltinPrintOperatorsSid + $WKS::BuiltinBackupOperatorsSid + $WKS::AccountDomainAdminsSid + $WKS::AccountEnterpriseAdminsSid + $WKS::AccountSchemaAdminsSid + $WKS::AccountControllersSid + $WKS::WinAccountReadonlyControllersSid + ) + foreach ($WellKnownSid in $Tier0WellKnownSids) { + $Name = Get-LocalizedWellKnownName -WellKnownSid $WellKnownSid -DomainSid $DomainSid + if ($Name) { $Tier0Resources += '|' + [regex]::Escape($Name) } + } + } catch { + Write-Warning "Could not resolve localized well-known names ($_). Detection falls back to English names only." + } + + Write-Verbose "UnsafeTrustees pattern: $UnsafeTrustees" + Write-Verbose "Tier0Resources pattern: $Tier0Resources" + $CurrentUserGroups = Get-CurrentUserGroups if ($CurrentUserGroups -notmatch $Tier0Resources) { $UnsafeTrustees += "|" + $CurrentUserGroups