Fix AD Lingering Objects with PowerShell

I briefly ran a blog before on wordpress.com, and most of the information there is outdated, or probably not relevant today, but there are a few posts that I’ve found little else on the internet to address.  These typically harken back to my AD/Exchange heavy days, but they’re still relevant today.  One of those posts is how to fix Active Directory lingering objects using PowerShell.

I ran into a problem in a large forest with multiple child domains and lots of domain controllers – 10 domains and 275 domain controllers!

To protect identities, let’s assume a forest consisted of domain.com, with two child domains – child1.domain.com and child2.domain.com.  Each domain has 2 global catalog servers (gc1, gc2), and one domain controller that is not a global catalog (dc1).

What are lingering objects anyway?

Remember that at least one domain controller in each domain must be a global catalog server.  GC’s have a copy of all objects in the forest, but only a subset of each object’s properties is found in AD.  For all objects in a GC that are not in that domain controller’s domain, the GC has a read-only copy.  You cannot manually go in and alter, create, or delete objects directly in the Global Catalog for objects that reside in another domain.

Lingering objects occur when through a variety of ways, a global catalog in one domain ends up with objects that no longer exist in another domain.  For example, let’s say a user exists in child2.domain.com and is deleted.  If somehow this doesn’t replicate to a GC in child1.domain.com or domain.com, the global catalogs in domain.com and child1.domain.com now have that user as a lingering object.  This can occur through a variety of ways, such as replication failures, or a global catalog server was disconnected for a long period of time.

Further info can be found here.

To find if you have lingering objects on a domain controller, you must run the following command:

repadmin /removelingeringobjects ServerName ServerGUID DirectoryPartition /advisory_mode

Simply remove the /advisory_mode switch to remove lingering objects.

ServerName is the fully qualified domain name of a global catalog that has lingering objects.  ServerGUID is a domain controller’s GUID from the domain that the lingering object is from, and you’d like to use it as a reference.  DirectoryPartition is the distinguished name of the GC partition with the lingering object.  Usually, lingering objects are computer or user account objects, so this would look like dc=domain,dc=com.
Finding the DC’s GUID can be done by looking in the forward lookup zone _msdcs.domain.com.

Lingering objects can cause problems with outdated or invalid group membership, problems with address book generation with Exchange, or basically problems with anything that depends upon valid info within the global catalog.  It can even cause replication failures depending upon your global catalog replication topology, and if you have strict replication enabled.

Scenario
Let’s say you suspect gc1.child1.domain.com has lingering objects from child2.domain.com.  You would first need a GUID of a DC in child2.domain.com that you believe has accurate domain information.  Let’s say you believe that dc is GC2.child2.domain.com.  Use the DNS MMC, connect to a DNS server hosting domain.com, look in the _msdcs.domain.com zone, and you will see all domain controllers in your forest.  Copy the GUID to your clipboard.  Let’s say GC2.child2.domain.com’s GUID is:

85d158d2-a006-4fff-b1e5-f9b6eaabab2b

You would then run:
repadmin /removelingeringobjects gc1.child1.domain.com 85d158d2-a006-4fff-b1e5-f9b6eaabab2b dc=child2,dc=domain,dc=com /advisory_mode

Note you need the Windows Support Tools installed.

This isn’t so tough.

However, if you suspected all your global catalogs had lingering objects for this domain, you’d need to run this command for each GC not in child2.domain.com.  Not terrible for this small of an environment.  To fix them, just chop off the advisory mode switch, and you’re done.

Think Big!

What if your environment was a 10 domain forest with over 100 domain controllers, and no predictable pattern of which domain controllers were global catalogs and which weren’t?!  Even if you knew which were global catalogs, who wants to issue that many commands?!

Wouldn’t it be nice is if we could issue this command to every global catalog not in child2.domain.com (since their GC’s have writable copies of the partition, theirs would be correct and would fix lingering objects on their own)?

That is what I faced.  I found replication wasn’t occuring for a domain partition in the global catalog because strict replication was enabled, and all global catalogs outside of a particular domain had lingering objects.  Talk about a pain in the butt!  Unless of course…
PowerShell to the rescue!

We can easily get all the global catalogs in the forest:
$forest = [system.directoryservices.activedirectory.Forest]::GetCurrentForest()
$forest.globalcatalogs | select-object name

You would receive output of the fully qualified domain names of all global catalogs.
But wait.  We only want GC’s that are NOT in child2.domain.com.  Simple enough with a where-object filter.

$forest.globalcatalogs | where-object {$_.name -notlike “*.child2.domain.com”} | select-object name

Now we just need to set this to a variable, so we add “$gcs = “ to the beginning of the second line.  This will allow us to have an array we can then perform an action or command on.  The last part is a bit tricky because we’re intermixing PowerShell with a standard command line.  Usually, you need to use the ‘ character around phrases.  Also, in this case, we’ve actually grabbed objects within the $gcs variable, so we want to make sure we’re not passing any other properties or code associated to objects.  We literally just want the name of each to be passed.  Remember, $_ means every object in the pipeline.  By adding .name, we’re saying don’t pass any other output related to each object in the array other than it’s name.  Without it, you get errors because PowerShell is putting extra characters in for each Global Catalog.

Final commands:

$forest = [system.directoryservices.activedirectory.Forest]::GetCurrentForest()
$gcs = $forest.globalcatalogs | where-object {$_.name -notlike "*.child2.domain.com"} | select-object name
$gcs | foreach-object {repadmin /removelingeringobjects $_.name 85d158d2-a006-4fff-b1e5-f