My Active Directory security assessment script pulls important security facts from Active Directory and generates nicely viewable reports in HTML format by highlighting the spots that require attention. The script manipulates user data using facts collected with benchmark values.

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.

AD Security Assessment report view

AD Security Assessment report view

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.

Domain Users summary repor

Domain Users summary repor

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) }
AD user attributes to identify inactive accounts

AD user attributes to identify inactive accounts

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 } 
User with ReversibleEncryptionPasswordArray enabled

User with ReversibleEncryptionPasswordArray enabled

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}
Kerberos pre authentication setting in AD

Kerberos pre authentication setting in AD

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.

Config INI file with password standards

Config INI file with password standards

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”
Domain Password Policy report

Domain Password Policy report

Windows Server 2019 password policy security standards

Windows Server 2019 password policy security standards

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”)
   
} 						
Tombstone setting and backup onformation report

Tombstone setting and backup onformation report

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.

Unconstrained deletion setting of a computer

Unconstrained deletion setting of a computer

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.

XML configuration file of a group policy

XML configuration file of a group policy

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"
}
KRBTGT account info report

KRBTGT account info report

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.

Privileged AD Group Info report

Privileged AD Group Info report

$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.

Config INI file details

Config INI file details

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".

Script logging from the PowerShell console

Script logging from the PowerShell console

Script directory view post execution

Script directory view post execution

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.

33 Comments
  1. wr 7 months ago
    PS C:\users\wr\downloads> .\AD_SecurityCheck.ps1
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:46 char:15
    +         �^\[(.+)\]� # Section
    +               ~
    Missing statement block in switch statement clause.
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:46 char:19
    +         �^\[(.+)\]� # Section
    +                   ~
    Missing statement block in switch statement clause.
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:52 char:13
    +         �^(;.*)$� # Comment
    +             ~
    Missing statement block in switch statement clause.
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:52 char:14
    +         �^(;.*)$� # Comment
    +              ~
    An expression was expected after '('.
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:52 char:14
    +         �^(;.*)$� # Comment
    +              ~
    Missing closing ')' in expression.
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:52 char:14
    +         �^(;.*)$� # Comment
    +              ~
    Missing statement block in switch statement clause.
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:52 char:17
    +         �^(;.*)$� # Comment
    +                 ~
    Missing statement block in switch statement clause.
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:52 char:17
    +         �^(;.*)$� # Comment
    +                 ~
    Missing condition in switch statement clause.
    At C:\users\wr\downloads\AD_SecurityCheck.ps1:42 char:1
    + {
    + ~
    Missing closing '}' in statement block or type definition.
    At C:\users\wri\downloads\AD_SecurityCheck.ps1:52 char:17
    +         �^(;.*)$� # Comment

  2. Jeff 7 months ago

    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.

    • Brandon 7 months ago

      ps1:45 char:15
      “^\[(.+)\]â€

  3. WR 7 months ago

    I posted the errors in a previous post.

    • Jeff 7 months ago

      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.

  4. Trubar 7 months ago

    Looks nice.. but yeah got errors also.. And got no idea what it should be…

    At C:\Install\AD_Securitycheck.ps1:46 char:15
    +         �^\[(.+)\]� # Section
    +               ~
    Missing statement block in switch statement clause.
    At C:\Install\AD_Securitycheck.ps1:46 char:19
    +         �^\[(.+)\]� # Section
    +                   ~
    Missing statement block in switch statement clause.
    At C:\Install\AD_Securitycheck.ps1:52 char:13
    +         �^(;.*)$� # Comment
    +             ~
    Missing statement block in switch statement clause.
    At C:\Install\AD_Securitycheck.ps1:52 char:14
    +         �^(;.*)$� # Comment
    +              ~
    An expression was expected after '('.
    At C:\Install\AD_Securitycheck.ps1:52 char:14
    +         �^(;.*)$� # Comment
    +              ~
    Missing closing ')' in expression.
    At C:\Install\AD_Securitycheck.ps1:52 char:14
    +         �^(;.*)$� # Comment
    +              ~
    Missing statement block in switch statement clause.
    At C:\Install\AD_Securitycheck.ps1:52 char:17
    +         �^(;.*)$� # Comment
    +                 ~
    Missing statement block in switch statement clause.
    At C:\Install\AD_Securitycheck.ps1:52 char:17
    +         �^(;.*)$� # Comment
    +                 ~
    Missing condition in switch statement clause.
    At C:\Install\AD_Securitycheck.ps1:42 char:1
    + {
    + ~
    Missing closing '}' in statement block or type definition.
    At C:\Install\AD_Securitycheck.ps1:52 char:17
    +         �^(;.*)$� # Comment
    +                 ~
    Unexpected token ')' in expression or statement.
    Not all parse errors were reported.  Correct the reported errors and try again.
        + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
        + FullyQualifiedErrorId : MissingSwitchStatementClause		
  5. JustMe 7 months ago

    You need to replace � with ”
    and ofcourse create a config.ini

    • David Ekstrom 7 months ago

      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.

  6. David Ekstrom 7 months ago

    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.

  7. Jon Irish (Rank 1) 7 months ago

    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?

    avatar
    • Author

      $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”

  8. Jon Irish (Rank 1) 7 months ago

    One other question for the group, has anyone found documentation on everything that is allowed in the config.ini file?

    • Author

      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/”

  9. Jon Irish (Rank 1) 7 months ago

    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.”
    }

    • Author

      You have to modify the code when you use Gmail for email notifications. So always try to use Google App password option.

  10. mehdi 7 months ago

    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.

      • MEHDI 7 months ago

        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

  11. Tomas 7 months ago

    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

    • Author

      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

      • Tomas 7 months ago

        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

        • Author

          Pls remove txt from the INI file format..It is Config.ini.txt currently

          • Tomas 7 months ago

            Thank you very much Krishnamoorthi. That was it. I am so sorry about such a stupid mistake. 🙂
            Thank you, I appreciate your help.

            Tomas

            avatar
  12. Jonathan Droesch 7 months ago

    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

  13. Killian 7 months ago

    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?

    • Author

      get-aduser -filter * -properties * | where {$_.PasswordNotRequired -eq $true}

      Try the above one

      • Killian 7 months ago

        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.

  14. Kevin H. 7 months ago

    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

    • Author

      Make sure Powershell AD Module is exists from where u running the script

      • Mehdi 7 months ago

        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

  15. Jonathan Droesch 7 months ago

    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

  16. Mehdi 7 months ago

    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

Leave a reply to Brandon Click here to cancel the reply

Please enclose code in pre tags

Your email address will not be published.

*

© 4sysops 2006 - 2022

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account