- The security assessment script
- User account issues
- Inactive accounts
- Users with ReversibleEncryptionPasswordArray
- Use Kerberos DES encryption types for this account
- Do not require Kerberos pre-authentication
- Review the domain password policy
- Tombstone lifetime and backups
- Unconstrained Kerberos delegation
- Scan SYSVOL for Group Policy Preference passwords
- Review KRBTGT account information
- Audit privileged AD groups
- Run the script
- Conclusion
- Windows security event log backup to SQL Server Express with PowerShell - Fri, Mar 18 2022
- Exploiting the CVE-2021-42278 (sAMAccountName spoofing) and CVE-2021-42287 (deceiving the KDC) Active Directory vulnerabilities - Thu, Feb 10 2022
- Perform Active Directory security assessment using PowerShell - Thu, Jan 6 2022
The security assessment script
Whatever fact does not fall under compliance is marked in red, as shown in the screenshot below. This script makes lightweight calls against AD to avoid the performance issues. It is designed for a single AD forest and is not designed to capture all the data in a multidomain forest. However, it can be customized to extend its scope.
The following topics describe the assessment checks performed by the script.
User account issues
It is essential to audit and track the changes made to Active Directory users and groups to maintain security governance. The script uses the accounts that fall under the various security compliance facts, such as inactive user accounts and accounts configured with a legacy version of encryption.
Inactive accounts
When an inactive account is not disabled or remains outside password expiration limits, perpetrators who try to hack into an organization can use these accounts because their activities will go unnoticed. In addition, employees who leave the organization can misuse their login credentials to access network resources. The script identifies accounts that have not been updated for the last 180 days by using the LastLogonDate and PasswordLastSet attributes. You can modify this setting from config.ini.
$LastLoggedOnDate = $(Get-Date) - $(New-TimeSpan -days 180) $PasswordStaleDate = $(Get-Date) - $(New-TimeSpan -days 180) Get-ADUser -Filter * -Properties * | Where { ($_.LastLogonDate -le $LastLoggedOnDate) -AND ($_.PasswordLastSet -le $PasswordStaleDate) }
Users with ReversibleEncryptionPasswordArray
The option to store passwords using reversible encryption provides support for applications that require the user's password for authentication. Anyone who knows the account password can misuse the account. Microsoft recommends disabling this setting through Group Policy using the Computer Configuration\Windows Settings\Security Settings\Account Policies\Password Policy\ policy if the option is no longer in use.
The following command displays the accounts with reversible encryption enabled in the domain:
Get-ADUser -Filter * -Properties * | Where { $_.UserAccountControl -band 0x0080 }
Use Kerberos DES encryption types for this account
Accounts that can use DES to authenticate to services are at significantly greater risk of having that account's logon sequence decrypted and the account compromised, since DES is considered weaker cryptography. The command below helps identify the accounts that support Kerberos DES encryption in the domain.
Get-ADUser -Filter {UserAccountControl -band 0x200000}
Do not require Kerberos pre-authentication
In earlier versions, Kerberos allowed authentication without a password. Now, in Kerberos 5, a password is required, which is called "pre-authentication." An attack that focuses on accounts with the pre-authentication option disabled is called an AS-REP roasting attack.
The following command helps to retrieve all the accounts with pre-authentication disabled:
Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true}
Review the domain password policy
It is important to review the domain password policy to determine whether the enforced configuration meets the benchmark standards. The script uses CIS -Windows Server 2019 Password Policy security standards for comparison. Optionally, you can keep the password standards in that INI file instead of inputting the values in the script, as shown in the screenshot below.
Input the domain in the command below to get the domain password policy information. If the server switch is not specified, it gets the details of the currently logged-on domain.
Get-ADDefaultDomainPasswordPolicy -Server “test.local”
Tombstone lifetime and backups
The tombstone lifetime option determines how long your backups can be used to perform data recovery in the event of a disaster in the IT environment. Microsoft recommends a value of 180 days. One of the benefits this provides is an increase in the useful life of backups.
The following command helps to identify the tombstone lifetime value and the last successful backup of each directory partition in the domain.
$ADRootDSE = get-adrootdse -Server “Test.local” $ADConfigurationNamingContext = $ADRootDSE.configurationNamingContext (Get-ADObject -Identity "CN=Directory Service,CN=Windows NT,CN=Services,$ADConfigurationNamingContext" ` -Partition "$ADConfigurationNamingContext" -Properties *).tombstoneLifetime $domaininfo = Get-ADDomain -Server “Test.local” [string[]]$Partitions = (Get-ADRootDSE -Server $domaininfo.PDCEmulator).namingContexts $contextType = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext($contextType,$($domaininfo.DNSRoot)) $domainController = [System.DirectoryServices.ActiveDirectory.DomainController]::findOne($context) ForEach($partition in $partitions) { $domainControllerMetadata = $domainController.GetReplicationMetadata($partition) $domainControllerMetadata.Item(“dsaSignature”) }
Unconstrained Kerberos delegation
When unconstrained delegation is configured, the userAccountControl attribute of the object is updated to include the TRUSTED_FOR_DELEGATION flag. When an object authenticates to a host with unconstrained delegation configured, the ticket-granting ticket (TGT) for that account is stored in memory. This is so that the host with unconstrained delegation configured can impersonate that user later, if needed. When the delegated servers are under the control of an attacker, the attacker can easily impersonate any server within the network using privileged TGT tokens that are cached locally.
The following query gets all the unconstrained computer objects in a domain other than Writable Domain (PrimaryGroupID=516) and Read-Only Domain Controllers (PrimaryGroupID=521)
Get-ADComputer -Filter { (TrustedForDelegation -eq $True) -AND (PrimaryGroupID -ne '516') -AND (PrimaryGroupID -ne '521') }
Scan SYSVOL for Group Policy Preference passwords
Group Policy Preferences (GPP) came into use from Windows Server 2008. This feature helps secure the password for administrative tasks, such as creating a local user and mapping a network drive within the policy. Whenever a new GPP is created for the user or group account, the password is associated with an XML file that is stored inside the SYSVOL folder. As you can see in the figure below, this XML file holds cpassword for user raaz within the property tags in plain text.
Microsoft released a patch (KB2962486) to address this security gap, which prevents admins from putting password data into a Group Policy Preference. The following script helps to find any GPP items that were created in the past and left unnoticed until now:
$domaininfo = Get-ADDomain -Server "test.local" $domainname = ($domaininfo.DistinguishedName.Replace("DC=","")).replace(",",".") $DomainSYSVOLShareScan = "\\$domainname\SYSVOL\$domainname\Policies\" Get-ChildItem $DomainSYSVOLShareScan -Filter *.xml -Recurse | % { If(Select-String -Path $_.FullName -Pattern "Cpassword"){ $_.FullName } }
Review KRBTGT account information
The KRBTGT account is a domain default account that acts as a service account for the KDC service. In most cases, KRBTGT resets might be performed when Active Directory is compromised. Still, Microsoft advises changing the password at regular intervals to keep the environment more secure. The script checks and highlights whether the account password has not changed within the last 180 days.
$DomainKRBTGTAccount= Get-ADUser 'krbtgt' -Server $DCServer -Properties 'msds-keyversionnumber',Created,PasswordLastSet If($(New-TimeSpan -Start ($DomainKRBTGTAccount.PasswordLastSet) -End $(Get-Date)).Days -gt 180) { Write-Host "Failed the test" }
Audit privileged AD groups
In Active Directory, privileged accounts have controlling rights and permissions. They can carry out all designated tasks in Active Directory, on domain controllers, and on client computers. On the flip side, privileged account abuse can result in data breaches, downtime, failed compliance audits, and other bad situations. These groups should be audited often and cleaned up if any inappropriate members are added to them.
The script was written to pull out information about specific privileged groups. Click the count in the report table to see the list of group members.
$ADPrivGroupArray = @( 'Administrators', 'Domain Admins', 'Enterprise Admins', 'Schema Admins', 'Account Operators', 'Server Operators', 'Group Policy Creator Owners', 'DNSAdmins', 'Enterprise Key Admins', 'Exchange Domain Servers', 'Exchange Enterprise Servers', 'Exchange Admins', 'Organization Management', 'Exchange Windows Permissions' ) foreach($group in $ADPrivGroupArray){ try { $GrpProps = Get-ADGroupMember -Identity $group -Recursive -Server $env:COMPUTERNAME -ErrorAction SilentlyContinue | select SamAccountName,distinguishedName $GrpProps | % { $_.SamAccountName } } catch{ $_.Exception.Message } }
Run the script
The script gets the dynamic inputs from a file called config.ini. Edit the INI file according to your environment. The script and the INI file should be placed in the same directory; otherwise, the script will fail.
After the initial changes in the INI file, you can run the script from PowerShell, as shown in the screenshot below. It generates the output in an HTML file called Reports_[Timestamp].HTML. The logging information is stored in a file called Log_[Timestamp].log".
The script source is published in the GitHub repository. You can view or download it using this link.
Conclusion
No organization with an IT infrastructure is immune from attack, but if appropriate, policies, processes, and controls can be implemented to secure Active Directory. One way is to assess Active Directory periodically, which helps to increase its security posture. Significantly, assessment helps to spot settings that do not meet the current security standards, namely, the CIS Benchmark.
Remediation guidelines and best practices can be defined based on the assessment outcome. I have written this script mainly to identify the vulnerable areas within Active Directory and generate information about vulnerabilities in a way that is viewable to audiences other than IT admins. I was not able to cover all the vulnerable spots in AD using this script, so there remain other vulnerabilities.
Subscribe to 4sysops newsletter!
You can download the script here. The latest version can be found on GitHub.
I was curious if the AD security assessment script I should be able to copy and run it? I see that I have gotten quite a few errors when running the script. I copied exactly what I saw into notepad++ and ran it. Only to get errors.
ps1:45 char:15
“^\[(.+)\]â€
I posted the errors in a previous post.
You know I am not trying to sound like a jerk, but I didn’t see your previous post.
Is there something wrong with posting it again? Not everyone posts or replies
at the same time. Plus we do miss posts. But that’s ok I will just ask some of your other colleague’s who probably wouldn’t mind assisting me.
Looks nice.. but yeah got errors also.. And got no idea what it should be…
You need to replace � with ”
and ofcourse create a config.ini
Getting a ton of these types of errors
t C:\Users\Desktop\AD_SecurityCheck.ps1:261 char:138
+ … -primary text-bold py-2″ data-hydro-click=”{"event_type":&q …
+ ~
The ampersand (&) character is not allowed. The & operator is reserved for future use; wrap an
ampersand in double quotation marks (“&”) to pass it as part of a string.
At C:\Users\MFAdmin\Desktop\AD_SecurityCheck.ps1:261 char:145
+ … y text-bold py-2″ data-hydro-click=”{"event_type":"ana …
+ ~
The ampersand (&) character is not allowed. The & operator is reserved for future use; wrap an
ampersand in double quotation marks (“&”) to pass it as part of a string.
Found the answer by Googling it. Apparently in Github, you have to choose the “Raw” button. If you try to download the file, a bunch of Github junk comes down with the file.
Has anyone figured out a fix for the SNMP function being deprecated? I get an error when trying to use my gmail account. I’ll start researching a fix, but if someone already has a work-around, why reinvent the wheel?
$Attachment = “C:\temp\Some random file.txt”
$Subject = “Email Subject”
$Body = “Insert body text here”
$SMTPServer = “smtp.gmail.com”
$SMTPPort = “587”
Send-MailMessage -From $From -to $To -Cc $Cc -Subject $Subject `
-Body $Body -SmtpServer $SMTPServer -port $SMTPPort -UseSsl `
-Credential (Get-Credential) -Attachments $Attachment
Reuse the above code. Don’t provide your default creentails instead generate google application password. Refer here “https://support.google.com/accounts/answer/185833?hl=en”
One other question for the group, has anyone found documentation on everything that is allowed in the config.ini file?
The script takes all the inputs other than the line that starts with comment “#” char. Use can refer here for more details “https://devblogs.microsoft.com/scripting/use-powershell-to-work-with-any-ini-file/”
First, I am NOT a programmer 😉 I hacked together some code to get the email functionality working without using the deprecated Send-MailMessage commandlet. Anyway, I’m sure this can be consolidated, but it works:
#———————————————————————————————————————————————
# Sending Mail
#———————————————————————————————————————————————
if($SendEmail -eq ‘Yes’ ) {
# Send ADHealthCheck Report
if(Test-Path $HealthReport)
{
try {
$Message = new-object Net.Mail.MailMessage
$smtp = new-object Net.Mail.SmtpClient(“smtp.gmail.com”, 587)
$smtp.Credentials = New-Object System.Net.NetworkCredential(“xxxxxxx@gmail.com”, “GmailPassword”);
$smtp.EnableSsl = $true
$smtp.Timeout = 400000
$Message.From = “xxxxxxx@gmail.com”
$Message.To.Add(“xxxxxxx@gmail.com”)
$Message.Attachments.Add(“$HealthReport”)
$Message.Subject = “AD Health Check Report”
$Message.Body = “Please find AD Health Check report attached.”
$smtp.Send($Message)
} catch {
Write-Log ‘Error in sending AD Health Check Report!’
}
}
#Send an ERROR mail if Report is not found
if(!(Test-Path $HealthReport))
{
try {
$Message = new-object Net.Mail.MailMessage
$smtp = new-object Net.Mail.SmtpClient(“smtp.gmail.com”, 587)
$smtp.Credentials = New-Object System.Net.NetworkCredential(“xxxxxxx@gmail.com”, “GmailPassword”);
$smtp.EnableSsl = $true
$smtp.Timeout = 400000
$Message.From = “xxxxxxx@gmail.com”
$Message.To.Add(“xxxxxxx@gmail.com”)
$Message.Subject = “AD Health Check Report”
$Message.Body = “ERROR: NO AD Health Check report.”
$smtp.Send($Message)
} catch {
Write-Log ‘Unable to send Error mail.’
}
}
}
else
{
Write-Log “As Send Email is NO so report through mail is not being sent. Please find the report in Script directory.”
}
You have to modify the code when you use Gmail for email notifications. So always try to use Google App password option.
Hi,
First thanks you for your script and for sharing, it is a aood idea, i tested it and every things work fine,
i made also same adjustement, but i hesitate to contribute if it’s worth it, because i wonder if pingcastle doesn’t do the same with more details.
Mehdi, don’t be shy. It is fine to share your adjustments here.
Hello Michael,
-the script uses hard variables which limits it to DCs in English
-the try catch method is not efficient to return error
-an AD module can be injected in order to be able to launch the script without prerequisite and from client
-I even thought to make a simple GUI interface which displays the result and allows advance configuration .ini
but like I said? is there more interest than pingCastel
Hello,
I am no as good as I expected in PowerShell, I was try to run script but cannot go trough this:
Cannot find path ‘C:\ADcheck\Config.ini’ because it does not exist.
Even that file is on expected path, no success. And I think this is, what stopped me from running it.
I will appreciate any advices.
Thank you
Tomas
Script and INI file should be there in the same directory. Post the screenshot if you looking for the further help.
You can take INI file from here – https://github.com/gkm-automation/AD-Security-Assessment
Hello Krishnamoorthi,
I took your config.ini files and put it to the same location:
PS C:\ADcheck> dir
Directory: C:\ADcheck
Mode LastWriteTime Length Name
—- ————- —— —-
-a—- 1/8/2022 11:31 AM 33181 AD_SecurityCheck.ps1
-a—- 1/11/2022 7:54 AM 241 Config.ini.txt
-a—- 1/11/2022 7:54 AM 866 Log11_01_2022-07_54_25.log
-a—- 1/11/2022 7:54 AM 15543 Reports11_01_2022-07_54_25.htm
PS C:\ADcheck>
Result:
PS C:\ADcheck> .\AD_SecurityCheck.ps1
Cannot find path ‘C:\ADcheck\Config.ini’ because it does not exist.
At C:\ADcheck\AD_SecurityCheck.ps1:43 char:25
+ switch -regex -file $FilePath
+ ~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\ADcheck\Config.ini:String) [], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound
You cannot call a method on a null-valued expression.
At C:\ADcheck\AD_SecurityCheck.ps1:82 char:1
I have even tried to copy config.ini to C: root, but no success. And thats why I am wondering, why that issue is.
Thank you
Tomas
Pls remove txt from the INI file format..It is Config.ini.txt currently
Thank you very much Krishnamoorthi. That was it. I am so sorry about such a stupid mistake. 🙂
Thank you, I appreciate your help.
Tomas
Hello guys,what a nice script, I just noticed a little error in the variable of RID master.
Can you please edit the line 551 with $domaininfo.RIDMaster instead of $domaininfo.DomainMode ?
Thanks
Thanks for reminding… Its updated
Thank you for this, it’s really helpful and puts my mind at ease to see a lot of green. I do have a query with regards to the ‘Users with Password Not Required’ line though. My report found;
10,000 odd Total Users
3000 enabled
7000 disabled
1100 inactive (how many days does it use for inactivity out of interest?)
6000 users with password not required
It’s that last line that concerns me but looking into the script it’s looking at all users where ‘passwordnotrequired -eq $true’. I’ve ran that myself with get-aduser -filter * | where {$_.PasswordNotRequired -eq $true} and I get 0 results (which I’d expect). Any thoughts on why it’s pulling 6000 as part of the wider script?
get-aduser -filter * -properties * | where {$_.PasswordNotRequired -eq $true}
Try the above one
Ah, of course, forgot I’d need to expand the list of properties. Used that and have now resolved them all and documented them just incase.
Thanks again.
First of all, thank for the script.
Unlikely I’m just getting one User out of the script with the following error in PowerShell:
Get-ADUser : Not a valid Win32-FileTime.
Parametername: fileTime
Make sure Powershell AD Module is exists from where u running the script
Hi Krishnamoorthi,
as I explained above, there is the possibility of packaging the AD module, to launch the script from a client, it will be more secure than doing it on the DC itself, adjust it too to remove the static variables will be interesting, as well as a GUI, if you agree you can tell me how to contact you to optimize it
Sorry my bad, 551 was good, it’s the 567 that need to be change.
551 : $domaininfo.RIDMaster –> $domaininfo.DomainMode
567 : $domaininfo.DomainMode –> $domaininfo.RIDMaster
Hi,
i made some progress, the script can be used from Computer Client like Win10, and he dont need to import Active Directory modules,
also dont need to enter config.ini DC information, it will be get automatically
Notepad++ screwed even with copying RAW from github.
Easiest was to paste raw directly into Powershell ISE and this seems to work.
Of course after editing the config.ini to your personal settings…. 🙂
The issue appears to be the way browsers display the REGEX lines (40-66). I downloaded the raw data from Github and everything was fine… odd. Has anyone made a list of all of the Config.ini options available?
Nice work Krishna it was pleasure working with you. You created amazing script and help automation dashboard