Private/AD/Core/Get-ADObjectACLs.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-ADObjectACLs { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Connection, [switch]$Quiet ) $result = @{ CriticalObjectACLs = @{} DangerousACEs = @() MachineAccountQuota = 0 DomainRootOwner = '' GPOPermissions = @{} OUDelegation = @() } $searchRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $Connection.DomainDN # ── Well-known GUIDs for dangerous extended rights and properties ────────── $dangerousGuids = @{ '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2' = 'DS-Replication-Get-Changes' '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' = 'DS-Replication-Get-Changes-All' '89e95b76-444d-4c62-991a-0facbeda640c' = 'DS-Replication-Get-Changes-In-Filtered-Set' '00299570-246d-11d0-a768-00aa006e0529' = 'User-Force-Change-Password' 'bf9679c0-0de6-11d0-a285-00aa003049e2' = 'Self-Membership' 'f3a64788-5306-11d1-a9c5-0000f80367c1' = 'Validated-SPN' } # Dangerous rights to flag $dangerousRights = @( 'GenericAll', 'GenericWrite', 'WriteDacl', 'WriteOwner' ) # Default groups to ignore in ACL analysis $defaultIgnorePatterns = @( 'Domain Admins', 'Enterprise Admins', 'SYSTEM', 'BUILTIN\Administrators', 'Administrators', 'S-1-5-18' # SYSTEM SID ) # ── Critical objects to audit ───────────────────────────────────────────── $criticalObjects = @{ 'Domain Root' = $Connection.DomainDN 'AdminSDHolder' = "CN=AdminSDHolder,CN=System,$($Connection.DomainDN)" 'Domain Controllers OU' = "OU=Domain Controllers,$($Connection.DomainDN)" 'Schema Container' = $Connection.SchemaDN 'Configuration Container' = $Connection.ConfigDN 'GPO Container' = "CN=Policies,CN=System,$($Connection.DomainDN)" } $allDangerousACEs = [System.Collections.Generic.List[hashtable]]::new() foreach ($objName in $criticalObjects.Keys) { $objDN = $criticalObjects[$objName] Write-Verbose "Analyzing ACLs on critical object: $objName ($objDN)..." try { $entry = New-LdapSearchRoot -Connection $Connection -SearchBase $objDN $sd = $entry.ObjectSecurity if ($null -eq $sd) { Write-Verbose "Could not read security descriptor for $objDN" $result.CriticalObjectACLs[$objName] = @{ ObjectDN = $objDN Error = 'Could not read security descriptor' ACEs = @() } continue } $accessRules = $sd.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]) $aceList = [System.Collections.Generic.List[hashtable]]::new() foreach ($rule in $accessRules) { $sidStr = $rule.IdentityReference.Value $resolved = Resolve-ADSid -SidString $sidStr -SearchRoot $searchRoot $objectTypeGuid = if ($rule.ObjectType -and $rule.ObjectType.ToString() -ne '00000000-0000-0000-0000-000000000000') { $rule.ObjectType.ToString() } else { $null } $objectTypeName = if ($objectTypeGuid -and $dangerousGuids.ContainsKey($objectTypeGuid)) { $dangerousGuids[$objectTypeGuid] } elseif ($objectTypeGuid) { $objectTypeGuid } else { $null } $inheritedTypeGuid = if ($rule.InheritedObjectType -and $rule.InheritedObjectType.ToString() -ne '00000000-0000-0000-0000-000000000000') { $rule.InheritedObjectType.ToString() } else { $null } $ace = @{ IdentityReference = $resolved IdentitySID = $sidStr ActiveDirectoryRights = $rule.ActiveDirectoryRights.ToString() AccessControlType = $rule.AccessControlType.ToString() ObjectType = $objectTypeName ObjectTypeGUID = $objectTypeGuid InheritedObjectType = $inheritedTypeGuid IsInherited = $rule.IsInherited ObjectDN = $objDN ObjectName = $objName } $aceList.Add($ace) # ── Check if this is a dangerous non-default ACE ────────── if ($rule.AccessControlType.ToString() -ne 'Allow') { continue } # Skip default/expected groups $isDefault = $false foreach ($pattern in $defaultIgnorePatterns) { if ($resolved -eq $pattern -or $resolved -like "*\$pattern" -or $sidStr -eq $pattern) { $isDefault = $true break } } # Also skip if it ends with the well-known domain admin/EA RIDs if (-not $isDefault -and $sidStr -match '-512$|-519$') { $isDefault = $true } if ($isDefault) { continue } $rights = $rule.ActiveDirectoryRights.ToString() $isDangerous = $false # Check for dangerous broad rights foreach ($dr in $dangerousRights) { if ($rights -match $dr) { $isDangerous = $true break } } # Check for dangerous WriteProperty on specific attributes if (-not $isDangerous -and $rights -match 'WriteProperty' -and $objectTypeGuid) { if ($dangerousGuids.ContainsKey($objectTypeGuid)) { $isDangerous = $true } } # Check for dangerous ExtendedRight (DCSync, password reset, etc.) if (-not $isDangerous -and $rights -match 'ExtendedRight') { if ($objectTypeGuid -and $dangerousGuids.ContainsKey($objectTypeGuid)) { $isDangerous = $true } elseif (-not $objectTypeGuid) { # All extended rights granted (no specific GUID means all) $isDangerous = $true } } if ($isDangerous) { $allDangerousACEs.Add($ace) } } # Store the owner for domain root if ($objName -eq 'Domain Root') { try { $ownerSid = $sd.GetOwner([System.Security.Principal.SecurityIdentifier]) $result.DomainRootOwner = Resolve-ADSid -SidString $ownerSid.Value -SearchRoot $searchRoot } catch { Write-Verbose "Could not resolve domain root owner: $_" $result.DomainRootOwner = 'Unknown' } } $result.CriticalObjectACLs[$objName] = @{ ObjectDN = $objDN ACECount = $aceList.Count ACEs = @($aceList) } Write-Verbose " $objName`: $($aceList.Count) ACEs analyzed." } catch { Write-Warning "Failed to read ACLs for $objName ($objDN): $_" $result.CriticalObjectACLs[$objName] = @{ ObjectDN = $objDN Error = "Failed: $_" ACEs = @() } } } $result.DangerousACEs = @($allDangerousACEs) Write-Verbose "Total dangerous non-default ACEs found: $($allDangerousACEs.Count)." # ── ms-DS-MachineAccountQuota ───────────────────────────────────────────── Write-Verbose 'Reading ms-DS-MachineAccountQuota from domain root...' try { $domainRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $Connection.DomainDN $maqResults = @(Invoke-LdapQuery -SearchRoot $domainRoot ` -Filter '(objectClass=domainDNS)' ` -Properties @('ms-DS-MachineAccountQuota') ` -Scope Base) if ($maqResults.Count -gt 0) { $result.MachineAccountQuota = [int]($maqResults[0]['ms-ds-machineaccountquota'] ?? 10) Write-Verbose "MachineAccountQuota: $($result.MachineAccountQuota)" } } catch { Write-Warning "Failed to read MachineAccountQuota: $_" } # ── GPO Permissions ─────────────────────────────────────────────────────── Write-Verbose 'Analyzing GPO permissions...' try { $gpoPoliciesDN = "CN=Policies,CN=System,$($Connection.DomainDN)" $gpoRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $gpoPoliciesDN $gpoObjects = Invoke-LdapQuery -SearchRoot $gpoRoot ` -Filter '(objectClass=groupPolicyContainer)' ` -Properties @('displayname', 'distinguishedname', 'name') $gpoPerms = @{} foreach ($gpo in $gpoObjects) { $gpoDN = $gpo['distinguishedname'] ?? '' $gpoDisplayName = $gpo['displayname'] ?? $gpo['name'] ?? $gpoDN try { $gpoEntry = New-LdapSearchRoot -Connection $Connection -SearchBase $gpoDN $gpoSd = $gpoEntry.ObjectSecurity if ($null -eq $gpoSd) { continue } $rules = $gpoSd.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) $editPrincipals = [System.Collections.Generic.List[string]]::new() $applyPrincipals = [System.Collections.Generic.List[string]]::new() foreach ($rule in $rules) { if ($rule.AccessControlType.ToString() -ne 'Allow') { continue } $sidStr = $rule.IdentityReference.Value $resolved = Resolve-ADSid -SidString $sidStr -SearchRoot $searchRoot $rights = $rule.ActiveDirectoryRights.ToString() # Edit = WriteDacl, WriteOwner, WriteProperty, GenericWrite, GenericAll if ($rights -match 'GenericAll|GenericWrite|WriteDacl|WriteOwner|WriteProperty') { if (-not $editPrincipals.Contains($resolved)) { $editPrincipals.Add($resolved) } } # Apply = GenericRead + GenericExecute (roughly) if ($rights -match 'GenericRead|GenericExecute|ReadProperty') { if (-not $applyPrincipals.Contains($resolved)) { $applyPrincipals.Add($resolved) } } } $gpoPerms[$gpoDisplayName] = @{ DN = $gpoDN CanEdit = @($editPrincipals) AppliesTo = @($applyPrincipals) } } catch { Write-Verbose "Failed to read ACL for GPO $gpoDisplayName`: $_" } } $result.GPOPermissions = $gpoPerms Write-Verbose "Analyzed permissions on $($gpoPerms.Count) GPO(s)." } catch { Write-Warning "Failed to analyze GPO permissions: $_" } # ── OU Delegation (object creation/deletion) ────────────────────────────── Write-Verbose 'Scanning OUs for delegated creation/deletion ACEs...' try { $ouResults = Invoke-LdapQuery -SearchRoot $searchRoot ` -Filter '(objectClass=organizationalUnit)' ` -Properties @('distinguishedname') $ouDelegations = [System.Collections.Generic.List[hashtable]]::new() foreach ($ou in $ouResults) { $ouDN = $ou['distinguishedname'] ?? '' if (-not $ouDN) { continue } try { $ouEntry = New-LdapSearchRoot -Connection $Connection -SearchBase $ouDN $ouSd = $ouEntry.ObjectSecurity if ($null -eq $ouSd) { continue } $rules = $ouSd.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) foreach ($rule in $rules) { if ($rule.AccessControlType.ToString() -ne 'Allow') { continue } $rights = $rule.ActiveDirectoryRights.ToString() # CreateChild or DeleteChild indicate delegation if ($rights -notmatch 'CreateChild|DeleteChild|GenericAll') { continue } $sidStr = $rule.IdentityReference.Value $resolved = Resolve-ADSid -SidString $sidStr -SearchRoot $searchRoot # Skip default groups $isDefault = $false foreach ($pattern in $defaultIgnorePatterns) { if ($resolved -eq $pattern -or $resolved -like "*\$pattern" -or $sidStr -eq $pattern) { $isDefault = $true break } } if (-not $isDefault -and $sidStr -match '-512$|-519$') { $isDefault = $true } if ($isDefault) { continue } $inheritedTypeGuid = if ($rule.InheritedObjectType -and $rule.InheritedObjectType.ToString() -ne '00000000-0000-0000-0000-000000000000') { $rule.InheritedObjectType.ToString() } else { $null } $objectTypeGuid = if ($rule.ObjectType -and $rule.ObjectType.ToString() -ne '00000000-0000-0000-0000-000000000000') { $rule.ObjectType.ToString() } else { $null } $ouDelegations.Add(@{ OUDN = $ouDN IdentityReference = $resolved IdentitySID = $sidStr ActiveDirectoryRights = $rights ObjectType = $objectTypeGuid InheritedObjectType = $inheritedTypeGuid IsInherited = $rule.IsInherited }) } } catch { Write-Verbose "Failed to read ACL for OU $ouDN`: $_" } } $result.OUDelegation = @($ouDelegations) Write-Verbose "Found $($ouDelegations.Count) OU delegation ACE(s)." } catch { Write-Warning "Failed to scan OU delegations: $_" } return $result } |