This PowerShell script will enumerate all user accounts in a Domain, calculate their UserAccountControl flags and create a report of the “interesting” flags in CSV format.
The interesting flags are those you are interested in reporting on as documented in Microsoft KB305144 such as:
- SCRIPT
- ACCOUNTDISABLE
- HOMEDIR_REQUIRED
- LOCKOUT
- PASSWD_NOTREQD
- PASSWD_CANT_CHANGE
- ENCRYPTED_TEXT_PWD_ALLOWED
- TEMP_DUPLICATE_ACCOUNT
- NORMAL_ACCOUNT
- INTERDOMAIN_TRUST_ACCOUNT
- WORKSTATION_TRUST_ACCOUNT
- SERVER_TRUST_ACCOUNT
- DONT_EXPIRE_PASSWORD
- MNS_LOGON_ACCOUNT
- SMARTCARD_REQUIRED
- TRUSTED_FOR_DELEGATION
- NOT_DELEGATED
- USE_DES_KEY_ONLY
- DONT_REQ_PREAUTH
- PASSWORD_EXPIRED
- TRUSTED_TO_AUTH_FOR_DELEGATION
- PARTIAL_SECRETS_ACCOUNT
Whilst this script can report on any of them, I am usually most interested in the following:
- PASSWD_NOTREQD – No password is required. This flag opens a whole can of worms which I’ll write up in a separate blog.
- TRUSTED_FOR_DELEGATION – When this flag is set, the service account (the user or computer account) under which a service runs is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service. It’s good to keep control of the number of these in use and be able to justify each one.
- TRUSTED_TO_AUTH_FOR_DELEGATION – This is a security-sensitive setting. Accounts that have this option enabled should be tightly controlled. This setting lets a service that runs under the account assume a client’s identity and authenticate as that user to other remote servers on the network. This flag is only set if you configure constrained delegation using any protocol. If you find accounts set with this flag, you should review the use of constrained delegation and change it to use Kerberos only, where possible, which will remove this flag from the account.
- USE_DES_KEY_ONLY – Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys. DES encryption types are not secure and are only used now days for backward compatibility when integrating older services and keytab files. As documented in the Best Practice Analyzer:
- DES is considered weak cryptography and is no longer enabled by default in Kerberos authentication in Windows 7 and Windows Server 2008 R2.
- User accounts and trusts configured for DES only will have authentication failures.
- User accounts and trusts should use Advanced Encryption Standard (AES) or RC4 Kerberos encryption keys.
- DONT_REQUIRE_PREAUTH – This account does not require Kerberos pre-authentication for logging on. Some systems/services may cause the Domain Controllers to log an Event ID 675 in the Security Event logs saying that pre-authentication has failed. However, this is not necessarily a bad thing and can often be ignored. Disabling Kerberos pre-authentication will further compromise security of Active Directory, because it means that any un-authenticated user can request the current time encrypted with the account’s password, allowing an offline attack. If a Vendor has suggested that Kerberos pre-authentication should be disabled, they should be challenged to justify this, including risk mitigation.
The script has some variables that can be set:
- Add the flags you want to report on to the $arrInterestingFlags variable array. You can certainly run the script multiple times with different combinations of flags.
- In large environments it can be difficult to know how long the script will take to complete, so I’ve implemented a progress bar. This allows you to monitor it and know where it’s up to. This is controlled by the $ProgressBar variable. Once you’re comfortable with the script, you could turn this off and run the script as a scheduled task on a regular basis as part of your administrative reporting tasks.
- When the script completes it will write a summary to the console. This is controlled by the $OutputSummary variable.
- I’ve added a $ProcessDisabledUsers variable that you can set if you really wanted to report on disabled user accounts.
The screen shots shown in this post are from a recent health check I completed in a large environment with 7950 enabled user accounts.
The screen shot below shows the progress bar. As mentioned, this output can be turned on and off by the $ProgressBar variable.
The screen show below shows the summary report produced when the script completes. As mentioned, this output can also be turned on and off by the $OutputSummary variable. You’ll notice that the ACCOUNTDISABLE flag is missing from the summary output simply because we do not report on disabled accounts by default.
The screen shot below shows the output of the csv file that has been imported into Excel. Note that I also get the LastLogonTimeStamp for each user account to ensure I’m not wasting time presenting information to the customer for discussion about user accounts that can simply be disabled.
The screen shot below shows the report I completed for the customer in a Word table format. I only include the necessary information.
Here is the Get-UserAccountControlReport.ps1 script:
<# This script will enumerate all user accounts in a Domain, calculate their UserAccountControl flags and create a report of the "interesting" flags in CSV format. Interesting flags are those you set in the $arrInterestingFlags array. References: - http://support.microsoft.com/kb/305144 - http://msdn.microsoft.com/en-us/library/ms680832(VS.85).aspx - http://bsonposh.com/archives/288 - http://gallery.technet.microsoft.com/scriptcenter/Convert-userAccountControl-629eed01 - http://jackstromberg.com/2013/01/useraccountcontrol-attributeflag-values/ Syntax examples: - To execute the script in the current Domain: Get-UserAccountControlReport.ps1 - To execute the script against a trusted Domain: Get-UserAccountControlReport.ps1 -TrustedDomain mydemosthatrock.com Script Name: Get-UserAccountControlReport.ps1 Release 1.1 Written by Jeremy Saunders (Jeremy@jhouseconsulting.com) 27/12/2013 Modified by Jeremy Saunders (Jeremy@jhouseconsulting.com) 02/11/2016 #> #------------------------------------------------------------- param([String]$TrustedDomain) # Set Powershell Compatibility Mode Set-StrictMode -Version 2.0 # Enable verbose, warning and error mode $VerbosePreference = 'Continue' $WarningPreference = 'Continue' $ErrorPreference = 'Continue' #------------------------------------------------------------- # Add the flags you want to report on in the CSV $arrInterestingFlags = @( "DONT_EXPIRE_PASSWORD",` "PASSWD_NOTREQD",` "TRUSTED_FOR_DELEGATION",` "USE_DES_KEY_ONLY",` "DONT_REQ_PREAUTH",` "TRUSTED_TO_AUTH_FOR_DELEGATION" ) # Set this to the OU structure where the you want to search to # start from. Do not add the Domain DN. If you leave it blank, # the script will start from the root of the domain. $OUStructureToProcess = "" # Set this to true to process disabled user accounts. $ProcessDisabledUsers = $False # Set this value to true if you want a summary output to the # console when the script has completed. $OutputSummary = $True # Set this to the delimiter for the CSV output $Delimiter = "," # Set this value to true if you want to see the progress bar. $ProgressBar = $True #------------------------------------------------------------- write-verbose "This script is running under PowerShell version $($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)" $invalidChars = [io.path]::GetInvalidFileNamechars() $datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-") # Get the script path $ScriptPath = {Split-Path $MyInvocation.ScriptName} $ReferenceFile = $(&$ScriptPath) + "\UserAccountControlReport-$($datestampforfilename).csv" if (Test-Path -path $ReferenceFile) { remove-item $ReferenceFile -force -confirm:$false } if ([String]::IsNullOrEmpty($TrustedDomain)) { # Get the Current Domain Information $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() } else { $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("domain",$TrustedDomain) Try { $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($context) } Catch [exception] { $Host.UI.WriteErrorLine("ERROR: $($_.Exception.Message)") Exit } } # Get AD Distinguished Name $DomainDistinguishedName = $Domain.GetDirectoryEntry() | select -ExpandProperty DistinguishedName If ($OUStructureToProcess -eq "") { $ADSearchBase = $DomainDistinguishedName } else { $ADSearchBase = $OUStructureToProcess + "," + $DomainDistinguishedName } $garbageCounter = 0 $TotalUsersProcessed = 0 $UserCount = 0 $SCRIPT_Count = 0 $ACCOUNTDISABLE_Count = 0 $HOMEDIR_REQUIRED_Count = 0 $LOCKOUT_Count = 0 $PASSWD_NOTREQD_Count = 0 $PASSWD_CANT_CHANGE_Count = 0 $ENCRYPTED_TEXT_PWD_ALLOWED_Count = 0 $TEMP_DUPLICATE_ACCOUNT_Count = 0 $NORMAL_ACCOUNT_Count = 0 $INTERDOMAIN_TRUST_ACCOUNT_Count = 0 $WORKSTATION_TRUST_ACCOUNT_Count = 0 $SERVER_TRUST_ACCOUNT_Count = 0 $DONT_EXPIRE_PASSWORD_Count = 0 $MNS_LOGON_ACCOUNT_Count = 0 $SMARTCARD_REQUIRED_Count = 0 $TRUSTED_FOR_DELEGATION_Count = 0 $NOT_DELEGATED_Count = 0 $USE_DES_KEY_ONLY_Count = 0 $DONT_REQ_PREAUTH_Count = 0 $PASSWORD_EXPIRED_Count = 0 $TRUSTED_TO_AUTH_FOR_DELEGATION_Count = 0 $PARTIAL_SECRETS_ACCOUNT_Count = 0 # Create new variable for each flag in the $arrInterestingFlags array # and set its initial value to $False ForEach ($InterestingFlag in $arrInterestingFlags) { new-variable -name $InterestingFlag -value $False -Force } If ($ProcessDisabledUsers -eq $False) { # Create an LDAP search for all enabled users $ADFilter = "(&(objectClass=user)(objectcategory=person)(!userAccountControl:1.2.840.113556.1.4.803:=2))" } else { # Create an LDAP search for all users $ADFilter = "(&(objectClass=user)(objectcategory=person))" } # There is a known bug in PowerShell requiring the DirectorySearcher # properties to be in lower case for reliability. $ADPropertyList = @("distinguishedname","samaccountname","useraccountcontrol","lastlogontimestamp","whencreated") $ADScope = "SUBTREE" $ADPageSize = 1000 $ADSearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$($ADSearchBase)") $ADSearcher = New-Object System.DirectoryServices.DirectorySearcher $ADSearcher.SearchRoot = $ADSearchRoot $ADSearcher.PageSize = $ADPageSize $ADSearcher.Filter = $ADFilter $ADSearcher.SearchScope = $ADScope if ($ADPropertyList) { foreach ($ADProperty in $ADPropertyList) { [Void]$ADSearcher.PropertiesToLoad.Add($ADProperty) } } Try { write-host " " If ($ProcessDisabledUsers -eq $False) { write-verbose "Please be patient whilst the script retrieves all enabled user objects and specified attributes..." } Else { write-verbose "Please be patient whilst the script retrieves all user objects and specified attributes..." } write-host " " $UserCount = $ADSearcher.FindAll().Count } Catch { $UserCount = 0 $Host.UI.WriteErrorLine("ERROR: The $ADSearchBase structure cannot be found!") } Finally { # Dispose of the search and results properly to avoid a memory leak $ADSearcher.Dispose() | Out-Null [System.GC]::Collect() | Out-Null } if ($UserCount -ne 0) { write-verbose "Processing $UserCount user objects in the $domain Domain..." write-host " " # The ForEach-Object cmdlet processes each item in turn as it is passed through the pipeline # whereas foreach generates the whole collection first. So this should alleviate memory issues. $ADSearcher.Findall() | ForEach-Object { #$_.Properties #$_.Properties.propertynames $lastLogonTimeStamp = "" $lastLogon = "" $UserDN = $_.Properties.distinguishedname[0] $samAccountName = $_.Properties.samaccountname[0] $TotalUsersProcessed ++ If ($ProgressBar) { Write-Progress -Activity "Processing $($UserCount) Users" -Status ("Count: $($TotalUsersProcessed) - Username: {0}" -f $samAccountName) -PercentComplete (($TotalUsersProcessed/$UserCount)*100) } If ($(Try{($_.Properties.lastlogontimestamp | Measure-Object).Count -gt 0}Catch{$False})) { $lastLogonTimeStamp = $_.Properties.lastlogontimestamp[0] $lastLogon = [System.DateTime]::FromFileTime($lastLogonTimeStamp) if ($lastLogon -match "1/01/1601") {$lastLogon = "Never logged on before"} } else { $lastLogon = "Never logged on before" } $whencreated = $_.Properties.whencreated[0] # Get User Account Control & Primary Group by binding to the user account # ADSI Requires that / Characters be Escaped with the \ Escape Character $UserDN = $UserDN.Replace("/", "\/") $objUser = [ADSI]("LDAP://" + $UserDN) If ($(Try{($objUser.useraccountcontrol | Measure-Object).Count -gt 0}Catch{$False})) { $UACValue = $objUser.useraccountcontrol[0] } else { $UACValue = "" } $flags = @() switch ($UACValue) { {($UACValue -bor 0x0001) -eq $UACValue} { $flags += "SCRIPT" $SCRIPT_Count ++ } {($UACValue -bor 0x0002) -eq $UACValue} { $flags += "ACCOUNTDISABLE" $ACCOUNTDISABLE_Count ++ } {($UACValue -bor 0x0008) -eq $UACValue} { $flags += "HOMEDIR_REQUIRED" $HOMEDIR_REQUIRED_Count ++ } {($UACValue -bor 0x0010) -eq $UACValue} { $flags += "LOCKOUT" $LOCKOUT_Count ++ } {($UACValue -bor 0x0020) -eq $UACValue} { $flags += "PASSWD_NOTREQD" $PASSWD_NOTREQD_Count ++ } {($UACValue -bor 0x0040) -eq $UACValue} { $flags += "PASSWD_CANT_CHANGE" $PASSWD_CANT_CHANGE_Count ++ } {($UACValue -bor 0x0080) -eq $UACValue} { $flags += "ENCRYPTED_TEXT_PWD_ALLOWED" $ENCRYPTED_TEXT_PWD_ALLOWED_Count ++ } {($UACValue -bor 0x0100) -eq $UACValue} { $flags += "TEMP_DUPLICATE_ACCOUNT" $TEMP_DUPLICATE_ACCOUNT_Count ++ } {($UACValue -bor 0x0200) -eq $UACValue} { $flags += "NORMAL_ACCOUNT" $NORMAL_ACCOUNT_Count ++ } {($UACValue -bor 0x0800) -eq $UACValue} { $flags += "INTERDOMAIN_TRUST_ACCOUNT" $INTERDOMAIN_TRUST_ACCOUNT_Count ++ } {($UACValue -bor 0x1000) -eq $UACValue} { $flags += "WORKSTATION_TRUST_ACCOUNT" $WORKSTATION_TRUST_ACCOUNT_Count ++ } {($UACValue -bor 0x2000) -eq $UACValue} { $flags += "SERVER_TRUST_ACCOUNT" $SERVER_TRUST_ACCOUNT_Count ++ } {($UACValue -bor 0x10000) -eq $UACValue} { $flags += "DONT_EXPIRE_PASSWORD" $DONT_EXPIRE_PASSWORD_Count ++ } {($UACValue -bor 0x20000) -eq $UACValue} { $flags += "MNS_LOGON_ACCOUNT" $MNS_LOGON_ACCOUNT_Count ++ } {($UACValue -bor 0x40000) -eq $UACValue} { $flags += "SMARTCARD_REQUIRED" $SMARTCARD_REQUIRED_Count ++ } {($UACValue -bor 0x80000) -eq $UACValue} { $flags += "TRUSTED_FOR_DELEGATION" $TRUSTED_FOR_DELEGATION_Count ++ } {($UACValue -bor 0x100000) -eq $UACValue} { $flags += "NOT_DELEGATED" $NOT_DELEGATED_Count ++ } {($UACValue -bor 0x200000) -eq $UACValue} { $flags += "USE_DES_KEY_ONLY" $USE_DES_KEY_ONLY_Count ++ } {($UACValue -bor 0x400000) -eq $UACValue} { $flags += "DONT_REQ_PREAUTH" $DONT_REQ_PREAUTH_Count ++ } {($UACValue -bor 0x800000) -eq $UACValue} { $flags += "PASSWORD_EXPIRED" $PASSWORD_EXPIRED_Count ++ } {($UACValue -bor 0x1000000) -eq $UACValue} { $flags += "TRUSTED_TO_AUTH_FOR_DELEGATION" $TRUSTED_TO_AUTH_FOR_DELEGATION_Count ++ } {($UACValue -bor 0x04000000) -eq $UACValue} { $flags += "PARTIAL_SECRETS_ACCOUNT" $PARTIAL_SECRETS_ACCOUNT_Count ++ } } # Set the InterestingFlag variable to $True if it's in the $flags array $AddToReport = $False ForEach ($InterestingFlag in $arrInterestingFlags) { If ($flags -contains $InterestingFlag) { $AddToReport = $True set-variable -name $InterestingFlag -value $True -Force } Else { set-variable -name $InterestingFlag -value $False -Force } } If ($AddToReport) { $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name "Domain" -value $domain $obj | Add-Member -MemberType NoteProperty -Name "SamAccountName" -value $SamAccountName $obj | Add-Member -MemberType NoteProperty -Name "UACValue" -value $UACValue ForEach ($InterestingFlag in $arrInterestingFlags) { $obj | Add-Member -MemberType NoteProperty -Name $InterestingFlag (get-variable -name $InterestingFlag -valueonly) } $obj | Add-Member -MemberType NoteProperty -Name "Flags" -value ([string]::Join("|",($flags))) $obj | Add-Member -MemberType NoteProperty -Name "LastLogon" -value $lastLogon $obj | Add-Member -MemberType NoteProperty -Name "WhenCreated" -value $whencreated # PowerShell V2 doesn't have an Append parameter for the Export-Csv cmdlet. Out-File does, but it's # very difficult to get the formatting right, especially if you want to use quotes around each item # and add a delimeter. However, we can easily do this by piping the object using the ConvertTo-Csv, # Select-Object and Out-File cmdlets instead. if ($PSVersionTable.PSVersion.Major -gt 2) { $obj | Export-Csv -Path "$ReferenceFile" -Append -Delimiter $Delimiter -NoTypeInformation -Encoding ASCII } Else { if (!(Test-Path -path $ReferenceFile)) { $obj | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -First 1 | Out-File -Encoding ascii -filepath "$ReferenceFile" } $obj | ConvertTo-Csv -NoTypeInformation -Delimiter $Delimiter | Select-Object -Skip 1 | Out-File -Encoding ascii -filepath "$ReferenceFile" -append -noclobber } $obj = $Null } $garbageCounter++ if ($garbageCounter -eq 500) { [System.GC]::Collect() $garbageCounter = 0 } } # Dispose of the search and results properly to avoid a memory leak $ADSearcher.Dispose() | Out-Null [System.GC]::Collect() | Out-Null If ($OutputSummary) { write-verbose "User Account Control Summary:" write-verbose "- Processed $UserCount user accounts and calculated the following flags..." write-verbose " - $SCRIPT_Count accounts are set to SCRIPT." If ($ProcessDisabledUsers) { write-verbose " - $ACCOUNTDISABLE_Count accounts are set to ACCOUNTDISABLE." } write-verbose " - $HOMEDIR_REQUIRED_Count accounts are set to HOMEDIR_REQUIRED." write-verbose " - $LOCKOUT_Count accounts are set to LOCKOUT." $Host.UI.WriteErrorLine("WARNING: - $PASSWD_NOTREQD_Count accounts are set to PASSWD_NOTREQD.") write-warning " - $PASSWD_CANT_CHANGE_Count accounts are set to PASSWD_CANT_CHANGE." write-verbose " - $ENCRYPTED_TEXT_PWD_ALLOWED_Count accounts are set to ENCRYPTED_TEXT_PWD_ALLOWED." write-verbose " - $TEMP_DUPLICATE_ACCOUNT_Count accounts are set to TEMP_DUPLICATE_ACCOUNT." write-verbose " - $NORMAL_ACCOUNT_Count accounts are set to NORMAL_ACCOUNT." write-verbose " - $INTERDOMAIN_TRUST_ACCOUNT_Count accounts are set to INTERDOMAIN_TRUST_ACCOUNT." write-verbose " - $WORKSTATION_TRUST_ACCOUNT_Count accounts are set to WORKSTATION_TRUST_ACCOUNT." write-verbose " - $SERVER_TRUST_ACCOUNT_Count accounts are set to SERVER_TRUST_ACCOUNT." write-warning " - $DONT_EXPIRE_PASSWORD_Count accounts are set to DONT_EXPIRE_PASSWORD." write-verbose " - $MNS_LOGON_ACCOUNT_Count accounts are set to MNS_LOGON_ACCOUNT." write-verbose " - $SMARTCARD_REQUIRED_Count accounts are set to SMARTCARD_REQUIRED." write-warning " - $TRUSTED_FOR_DELEGATION_Count accounts are set to TRUSTED_FOR_DELEGATION." write-verbose " - $NOT_DELEGATED_Count accounts are set to NOT_DELEGATED." $Host.UI.WriteErrorLine("WARNING: - $USE_DES_KEY_ONLY_Count accounts are set to USE_DES_KEY_ONLY.") $Host.UI.WriteErrorLine("WARNING: - $DONT_REQ_PREAUTH_Count accounts are set to DONT_REQ_PREAUTH.") write-verbose " - $PASSWORD_EXPIRED_Count accounts are set to PASSWORD_EXPIRED." $Host.UI.WriteErrorLine("WARNING: - $TRUSTED_TO_AUTH_FOR_DELEGATION_Count accounts are set to TRUSTED_TO_AUTH_FOR_DELEGATION.") write-verbose " - $PARTIAL_SECRETS_ACCOUNT_Count accounts are set to PARTIAL_SECRETS_ACCOUNT." } if (Test-Path -path $ReferenceFile) { Write-Host " " Write-Verbose "CSV files to review:" Write-Verbose " - $ReferenceFile" } }
References:
- Microsoft KB305144: How to use the UserAccountControl flags to manipulate user account properties
- Microsoft MSDN: User-Account-Control attribute
- Industry Blog: More userAccountControl Flag Fun (Convert-ToUACFlag.ps1)
- Microsoft TechNet Script Center: Convert userAccountControl values to flags
- Industry Blog: UserAccountControl Attribute/Flag Values
Enjoy!