PS: Manage AdminSDHolder Accounts

The well-known problem in historical grown AD DS environments where lots of users had a huge amount of high privileges is the adminSDHolder object within the configuration partition. Users that become member of a "protected" group get a new ACL (the one from the AdminSDHolder object) and in addition the inherit permission from parent objects flag gets disabled. This situation often causes problems. Especially with Exchange operations like provisioning new ActiveSync devices to this users.

The problem is only protection gets enabled there is no disable protection implemented in the directory services. That's the point where orphaned protected objects cause problems. To find out which users are protected you can use the PowerShell command: Get-ADUser -Filter "adminCount -eq 1" Then you can verify account by account to remove the protection on accounts that do no longer require the protection.

You can read more about it a article by John Policelli (>> AdminSDHolder, Protected Groups and SDPROP)

This script walks through all users within an active directory domain and checks whether the user is or was direct or indirect member of a protected group within AD DS. Afterwards it verifies if the user is still in this protected state or if the flag is orphaned.
With this analysis you have to options to just enable inherited permissions for all protected user or you can disable protection for users that have an orphaned protection flag set.

The script can handle accounts of a single domain within a forest. At this time it can not handle a complete forest.


Requirements

Active Directory Module for Power Shell >> KB10102


Usage

Execute the script with an elevated PowerShell and follow up screen instruction if they appear.


Version History

  • 1.0.0: Initial release
  • 1.0.1: Some typo fixes and the new possibility to export account lists
  • 1.0.2: Bug fixes (Users with only privileged groups were not displayed)
  • 1.0.3: Bug fixes

Script code:

	
##------------------------------------------------------------------------------------------------
##
##  Manage-AdminSDHolderAccounts.ps1
##
##   Version 1.0.3
##
##   (c) 2016 Martin Mueller
##		www.sh-soft.com
##
##  Licence: Feel free to use and redistribute this script!
##
##------------------------------------------------------------------------------------------------

<#
.SYNOPSIS
This script walks through all users within an active directory domain and checks whether the user is or was direct or indirect member of a protected group within AD DS. Afterwards it verifies if the user is still in this protected state or if the flag is orphaned.
With this analysis you have to options to just enable inherited permissions for all protected user or you can disable protection for users that have an orphaned protection flag set.
#>

#------------------------------------------------------------------------------------------------
# Import ADDS module
#------------------------------------------------------------------------------------------------
Import-Module ActiveDirectory

##------------------------------------------------------------------------------------------------
## Variables
##------------------------------------------------------------------------------------------------
#Groups and SIDs for the adminSDHolder relevant section, can be extended in the future
# DOMAIN will be replaced by the current domain SID
# FOREST will be replaced by the forest root domain SID
$WellKnownGroups = @{"AccountOperators" = "S-1-5-32-548" 
						"Administrators" = "S-1-5-32-544" 
						"BackupOperators" = "S-1-5-32-551" 
						"DomainAdmins" = "DOMAIN-512" 
						"DomainControllers" = "DOMAIN-516" 
						"EnterpriseAdmins" = "FOREST-519" 
						"PrintOperators" = "S-1-5-32-550" 
						"ReadOnlyDomainControllers" = "DOMAIN-521" 
						"Replicators" = "S-1-5-32-552" 
						"SchemaAdmins" = "FOREST-518" 
						"ServerOperators" = "S-1-5-32-549"}
#Temp Group List
$global:GroupList = @()
$LocalWellKnownGroupNames = @{}

#------------------------------------------------------------------------------------------------
# retrieve ADDS forest and domain information
$ForestName = Get-ADForest | Select-Object -ExpandProperty Name
$ForestRootDomainName = Get-ADForest | Select-Object -ExpandProperty RootDomain
$ForestRootSID = (Get-ADDomain $ForestRootDomainName | Select-Object -ExpandProperty DomainSID).Value
$CurrentDomainName = Get-ADDomain | Select-Object -ExpandProperty DNSRoot
$CurrentDomainSID = (Get-ADDomain | Select-Object -ExpandProperty DomainSID).Value

#------------------------------------------------------------------------------------------------
# Replace DOMAIN and FOREST by the actual values from ADDS
$WellKnownGroups.DomainAdmins = $WellKnownGroups.DomainAdmins.Replace("DOMAIN", $CurrentDomainSID)
$WellKnownGroups.DomainControllers = $WellKnownGroups.DomainControllers.Replace("DOMAIN", $CurrentDomainSID)
$WellKnownGroups.EnterpriseAdmins = $WellKnownGroups.EnterpriseAdmins.Replace("FOREST", $ForestRootSID)
$WellKnownGroups.ReadOnlyDomainControllers = $WellKnownGroups.ReadOnlyDomainControllers.Replace("DOMAIN", $CurrentDomainSID)
$WellKnownGroups.SchemaAdmins = $WellKnownGroups.SchemaAdmins.Replace("FOREST", $ForestRootSID)

#------------------------------------------------------------------------------------------------
# Get local group names for the well known SIDs
foreach ($WellKnownGroup in $WellKnownGroups.Values) {
	$LocalWellKnownGroupNames.Add($WellKnownGroup, (Get-ADGroup -Identity "$WellKnownGroup" | Select-Object -ExpandProperty Name))
}

##------------------------------------------------------------------------------------------------
## Internal functions
##------------------------------------------------------------------------------------------------
# Check whether group is already in the list to prevent recursion loop
Function Test-NewGroup ($DN) {
	if ($global:GroupList.Count -gt 0) {
		foreach ($Group in $global:GroupList) {
			if ($Group -eq $DN) {
				return $false
			}
			return $true
		}
	}
	else {
		return $true
	}
}
# Get nested groups in recursion
Function Get-IndirectGoups ($GroupDN) {
	Get-ADGroup -Identity "$GroupDN" -Properties MemberOf | `
		Select-Object -ExpandProperty MemberOf | `
		ForEach-Object {
			$CurrentDN = $_
			if (Test-NewGroup -DN "$CurrentDN") {
				$global:GroupList += "$CurrentDN"
				Get-IndirectGoups -GroupDN "$CurrentDN"
			}
		}
} 
# Get nested groups from direct membership groups
Function Get-CompleteGroupMembership ($DirectGroups) {
	$global:GroupList = @()
	foreach ($Group in $DirectGroups) {
		if (Test-NewGroup -DN "$Group") {
			$global:GroupList += "$Group"
			Get-IndirectGoups -GroupDN $Group
		}
	}
	return $global:GroupList
}
# Test if a group is in the list of well known groups
Function Test-AdminSDHolder ($GroupDN) {
	$CurrentGroupSID = (Get-ADGroup -Identity "$GroupDN" | Select-Object -ExpandProperty SID).Value
	foreach ($WellKnownGroup in $WellKnownGroups.Values) {
		if ($CurrentGroupSID -eq $WellKnownGroup) {
			return $true, $WellKnownGroup
		}
	}
	return $false
}
#Enable inheritance on given user DN
Function Enable-Inheritance ($UserDN) {
	#Get acl object
	$acl = Get-ACL -Path "AD:\$UserDN"
	#Test if acl is protected
	if ($acl.AreAccessRulesProtected){
		$acl.SetAccessRuleProtection($False, $True)
		try {
			#Set-ACL -AclObject $acl -path "AD:\$UserDN"
			Write-Host -ForegroundColor Green "[SUCCESS] while enabling permission on user: $UserDN"
		}
		catch {
			Write-Host -ForegroundColor Red "[ERROR] while enabling permission on user: $UserDN"
		}
	}
}


##------------------------------------------------------------------------------------------------
##  main block...
##------------------------------------------------------------------------------------------------

#------------------------------------------------------------------------------------------------
# Get relevant accounts an collect data
#------------------------------------------------------------------------------------------------

# Retrieve user accounts with the value adminCount enabled
$AdminUsers = Get-ADUser -Filter "adminCount -eq 1" -Properties MemberOf | `
				Select-Object -Property ObjectGUID, Name, distinguishedName, IndirectMemberOf, MemberOf, `
				TotalMembershipCounter, DirectMembershipCounter, AdminSDGroupsCounter, AdminSDGroupMembership
$UserCount = $AdminUsers.Count
$counter = 0
foreach ($AdminUser in $AdminUsers) {
	## UI
	Write-Progress -Activity "Getting users group membership..." -PercentComplete ($counter/$UserCount*100) -Status ("Processing user: "+$AdminUser.Name)
	$MemberOf = $AdminUser | Select-Object -ExpandProperty MemberOf
	if (($MemberOf | Measure-Object | Select-Object -ExpandProperty Count) -gt 0) {
		# Retrieve nested group membership
		$AdminUser.IndirectMemberOf = Get-CompleteGroupMembership -DirectGroups $MemberOf
		# Set some counters...
		$AdminUser.TotalMembershipCounter = $AdminUser.IndirectMemberOf.Count
		$AdminUser.DirectMembershipCounter = $MemberOf.Count
		$AdminUser.AdminSDGroupsCounter = 0
		$adminUser.AdminSDGroupMembership = @()
		$IndirectMemberOf = $AdminUser | Select-Object -ExpandProperty MemberOf
		if (($MemberOf | Measure-Object | Select-Object -ExpandProperty Count) -gt 0) { 
			foreach ($Group in $IndirectMemberOf) {
				# Test if the current group would issue adminCount to 1
				$TestResult = Test-AdminSDHolder -GroupDN $Group
				if ($TestResult) {
					$AdminUser.AdminSDGroupsCounter++
					$AdminUser.AdminSDGroupMembership+=$LocalWellKnownGroupNames.($TestResult[1])
				}
			}
		}
	}
	$counter++
}
Write-Progress -Completed -Activity "Getting users group membership..." -Status "...done"

#------------------------------------------------------------------------------------------------
# Display results
#------------------------------------------------------------------------------------------------
$UserCountRemain = ($AdminUsers | Where-Object {$_.AdminSDGroupsCounter -gt 0}).Count

Write-Host -ForegroundColor Green "Users found: " -NoNewline
Write-Host $UserCount
Write-Host -ForegroundColor Green "Users found that still require adminCount set to 1: " -NoNewline
Write-Host $UserCountRemain
Write-Host ""
Write-Host ""
Write-Host -ForegroundColor Green "Users that still require adminCount set to 1:"
$StillRequiredUsers = $AdminUsers | Where-Object {$_.AdminSDGroupsCounter -gt 0}
foreach ($User in $StillRequiredUsers) {
	Write-Host -ForegroundColor Yellow ($user.Name+ " (") -NoNewline
	Write-Host $User.distinguishedName -NoNewline
	Write-Host -ForegroundColor Yellow ")"
	Write-Host (" Direct / indirect member of groups: "+$User.TotalMembershipCounter)
	Write-Host (" Member of relevant groups: "+$User.AdminSDGroupsCounter)
	$User.AdminSDGroupMembership | ForEach-Object {Write-Host ("  - "+$_)}
}
Write-Host ""
Write-Host -ForegroundColor Green "Users that do NOT require adminCount set to 1:"
$NotRequiredUsers = $AdminUsers | Where-Object {$_.AdminSDGroupsCounter -eq 0 -or -not $_.AdminSDGroupsCounter}
foreach ($User in $NotRequiredUsers) {
	Write-Host -ForegroundColor Yellow ($user.Name+ " (") -NoNewline
	Write-Host $User.distinguishedName -NoNewline
	Write-Host -ForegroundColor Yellow ")"
	Write-Host (" Direct / indirect member of groups: "+$User.TotalMembershipCounter)
}

#------------------------------------------------------------------------------------------------
# Ask to reset permission and no longer required adminCount
#------------------------------------------------------------------------------------------------
Write-Host ""
Write-Host ""
Write-Host -ForegroundColor Red "Do you want to reset the permissions applied to the no longer required users and / or adminCount?"
Write-Host ""
Write-Host -ForegroundColor Magenta "[1] - " -NoNewline
Write-Host "Enabled inheritance for the users that still require adminCount"
Write-Host -ForegroundColor Magenta "[2] - " -NoNewline
Write-Host "Enabled inheritance for the users that do NOT require adminCount"
Write-Host -ForegroundColor Magenta "[3] - " -NoNewline
Write-Host "Reset adminCount for no longer required accounts"
Write-Host -ForegroundColor Magenta "[4] - " -NoNewline
Write-Host "Reset adminCount for no longer required accounts and enable inheritance for those accounts"
# Reports
Write-Host -ForegroundColor Magenta "[5] - " -NoNewline
Write-Host "Export a list with DNs of accounts that still require adminCount"
Write-Host -ForegroundColor Magenta "[6] - " -NoNewline
Write-Host "Export a list with DNs of accounts that do NOT require adminCount"
Write-Host ""
$Selection = Read-Host -Prompt "your choice"
while ($Selection -lt 1 -or $Selection -gt 6) {
	$Selection = Read-Host -Prompt "your choice"
}
#Do what was requested
switch ($Selection) {
	1 {
		$StillRequiredUsers | Select-Object -ExpandProperty distinguishedName | `
								ForEach-Object {
									Enable-Inheritance -UserDN "$_"
								}
		break
	}
	2 {
		$NotRequiredUsers | Select-Object -ExpandProperty distinguishedName | `
								ForEach-Object {
									Enable-Inheritance -UserDN "$_"
								}
		break
	}
	3 {
		$NotRequiredUsers | Select-Object -ExpandProperty distinguishedName | `
								ForEach-Object {
									try {
										Set-ADObject "$_" -Clear {adminCount}
										Write-Host -ForegroundColor Green "[SUCCESS] Disabling adminCount: $UserDN"
									}
									catch {
										Write-Host -ForegroundColor Red "[ERROR] Disabling adminCount: $UserDN"
									}
								}
		break
	}
	4 {
		$NotRequiredUsers | Select-Object -ExpandProperty distinguishedName | `
								ForEach-Object {
									try {
										Set-ADObject "$_" -Clear {adminCount}
										Write-Host -ForegroundColor Green "[SUCCESS] Disabling adminCount: $UserDN"
									}
									catch {
										Write-Host -ForegroundColor Red "[ERROR] Disabling adminCount: $UserDN"
									}
									Enable-Inheritance -UserDN "$_"
								}
		break
	}
	5 {
		Write-Host -ForegroundColor Yellow "Enter path and filename: " -NoNewline
		$FileName = Read-Host
		try {
			$StillRequiredUsers | Select-Object -ExpandProperty distinguishedName | Out-File -FilePath $FileName -Encoding utf8
		}
		catch {
			Write-Error "Failed to write to file: $FileName"
		}
	}
	6{
		Write-Host -ForegroundColor Yellow "Enter path and filename: " -NoNewline
		$FileName = Read-Host 
		try {
			$NotRequiredUsers | Select-Object -ExpandProperty distinguishedName | Out-File -FilePath $FileName -Encoding utf8
		}
		catch {
			Write-Error "Failed to write to file: $FileName"
		} 
	}
}
Write-Host ""
Write-Host ""
Write-Host -ForegroundColor Green "...Done"
	

Download the script:

>> Version 1.0.3 (current)
(MD5: 3338718e175a3fe8c1c2029ba228880d)
(SHA1: 5b61027bf2d892b38a6cbfa5b33fe6b995a93ef4)


>> syntax highlighting powered by highlight.js