Use PowerShell to search for accounts in Active Directory that have gone stale!

MVP

 

Dear Microsoft and Active Directory Friends,

 

In a company/organization, employees come and go. This is completely normal and nothing out of the ordinary. But often, from a technical point of view, the Active Directory is somewhat forgotten and account cleanup is rarely or never done. This means there are outdated or no longer current accounts (ghost accounts). This is not a great situation from either a security or administrative perspective. In this article I would like to show you a possible way. But what exactly does "stale" accounts mean? Let me explain my definition to you:

 

1. Haven't logged in for X days
2. Hasn't logged in
3. Created at least X days ago

 

We can start first with the CMDLET "Search-ADAccount". This gives once a first result. The #tags are comments.

#Using Search-ADAccount
Search-ADAccount -AccountInactive -TimeSpan '90.00:00:00' -UsersOnly

_AD_01.jpg

 

How many accounts would there be?

(Search-ADAccount -AccountInactive -TimeSpan '90.00:00:00' -UsersOnly).count

_AD_02.jpg

 

But there are a lot of them. Now let's work with filters. We explicitly examine the "LastLogonTimeStamp" property. For an account that has never been logged in, no value is displayed.

 

#Using a filter
Get-ADUser "Freida Lazarus" -Properties LastLogonTimeStamp | Select-Object Name,LastLogonTimeStamp

_AD_03.jpg

 

An account that has been used for a login will show a value. This number represents a date, this is in FileTime format.

_AD_04.jpg

 

We can generate such value. For example, one hour in the past.

$LogonDate = (Get-Date).AddHours(-1).ToFileTime()
$LogonDate

_AD_05.jpg

 

So, for example, we can check who has not logged in during the last hour. Of course we would not work with only one hour. But I'm going to keep working with this one hour so I can illustrate it to you better.

 

#If it is older than $LogonDate
$LogonDate = (Get-Date).AddHours(-1).ToFileTime()
Get-ADUser -Filter {LastLogonTimeStamp -lt $LogonDate}

 

_AD_06.jpg

 

Or in numbers it would be so many accounts.

_AD_07.jpg

 

Now we search for all accounts that do not have this value.

 

#If it doesn't have value
Get-ADUser -Filter {LastLogonTimeStamp -notlike "*"} -Properties LastLogonTimeStamp | Select-Object Name,LastLogonTimeStamp

_AD_08.jpg

 

But what if the user has not yet logged in because the account has just been created? Let's take a closer look at this as well.

 

#And if the account was created before $createdDate
$createdDate = (Get-Date).AddDays(-14)
Get-ADUser -Filter {Created -lt $createdDate} -Properties Created |
Select-Object Name,Created

_AD_09.jpg

 

Bringing all this together, we can determine what exactly "stale" means to us.

 

#Add them all together:
$filter = {
((LastLogonTimeStamp -lt $logonDate) -or (LastLogonTimeStamp -notlike "*"))
-and (Created -lt $createdDate)
}
Get-ADuser -Filter $filter | Select-Object SamAccountName

_AD_10.jpg

 

We can create a function from it.

 

Function Get-ADStaleUsers {
[cmdletbinding()]
Param (
[datetime]$NoLogonSince = (Get-Date).AddDays(-90),
[datetime]$CreatedBefore = (Get-Date).AddDays(-14)
)
$NoLogonString = $NoLogonSince.ToFileTime()
$filter = {
((LastLogonTimeStamp -lt $NoLogonString) -or (LastLogonTimeStamp -notlike "*"))
-and (Created -lt $createdBefore)
}
Write-Host $filter
Get-ADuser -Filter $filter
}

 

# Usage
Get-ADStaleUsers

_AD_11.jpg

 

This way we can work with our own values. Moreover, we can delete the accounts directly afterwards or at least check with -WhatIf what exactly would be done!

 

Get-ADStaleUsers -NoLogonSince (Get-Date).AddDays(-30) -CreatedBefore (Get-Date).AddDays(-7) | Remove-ADUser -WhatIf

_AD_12.jpg

 

Annotation:
In the search result from above two Built-In accounts (e.g.: krbtgt) are displayed among others (this is how it turned out in the search). Such Built-In accounts should never be deactivated or even worse deleted, this has very negative effects on the function of the Active Directory. You should never delete user accounts directly. Instead, the accounts should be disabled for some time first. If the accounts were used for other purposes (unknown to you). You should also avoid setting user accounts as service accounts. This is not according to Microsoft "Best Practice". For service accounts, use the so-called group managed service accounts.

 

I realize that this was not necessarily spectacular. It was simply important for me to share my experience with you. Nevertheless, I hope that this article was helpful. Thank you for taking the time to read the article.


Best regards, Tom Wechsler

 

P.S. All scripts (#PowerShell, Azure CLI, #Terraform, #ARM) that I use can be found on github! https://github.com/tomwechsler

2 Replies

For anyone reviewing this and with a need to audit an AD environment not only for stale accounts, but also for stale passwords, stale computers, unsupported operating systems, and other such reports - please review my own project at https://github.com/ziesemer/ad-privileged-audit/ , if it may provide any benefit to you or your organization. It is built upon many of the same principles and PowerShell cmdlets that Tom just covered here, but extends this into a suite of production-ready reports. (I don't mean to spam this article with my own solution, but only mean to share as I'm trying to help everyone I can get ahead of the potential security incidents otherwise waiting to happen within many environments due to such stale or orphaned accounts.)

Hello,

Nice script, thanks !
I would add two exceptions to your script, one for "krbtgt", another for default AD administrator account ("administrator" in En-US) - if people start to disable/delete those because they think they are stale, it will backfire.