<# .NOTES Author : phillips321.co.uk Creation Date: 16/08/2018 Script Name : ADAudit.ps1 .SYNOPSIS PowerShell Script to perform a quick AD audit .DESCRIPTION o Compatibility : * PowerShell v2.0 (PowerShell 5.0 needed if you intend to use DSInternals PowerShell module) * Tested on Windows Server 2008R2/2012/2012R2/2016/2019/2022 * All languages (you may need to adjust $AdministratorTranslation variable) o Changelog : [x] Version 5.4 - 16/08/2022 * Added nessus output tags for LAPS * Added nessus output for GPO issues [ ] Version 5.3 - 07/03/2022 * Added SamAccountName to Get-PrivilegedGroupMembership output * Swapped some write-host to write-both so it's captured in the consolelog.txt [ ] Version 5.2 - 28/01/2022 * Enhanced Get-LAPSStatus * Added news checks (AD services + Windows Update + NTP source + Computer/User container + RODC + Locked accounts + Password Quality + SYSVOL & NETLOGON share presence) * Added support for WS 2022 * Fix OS version difference check for WS 2008 * Fix Write-Progress not disappearing when done [ ] Version 5.1 * Added check for newly created users and groups * Added check for replication mechanism * Added check for Recycle Bin * Fix ProtectedUsers for WS 2008 [ ] Version 5.0 * Make the script compatible with other language than English * Fix the cpassword search in GPO * Fix Get-ACL bad syntax error * Fix Get-DNSZoneInsecure for WS 2008 [ ] Version 4.9 * Bug fix in checking password comlexity [ ] Version 4.8 * Added checks for vista, win7 and 2008 old operating systems * Added insecure DNS zone checks [ ] Version 4.7 * Added powershel-v2 suport and fixed array issue [ ] Version 4.6 * Fixed potential division by zero [ ] Version 4.5 * PR to resolve count issue when count = 1 [ ] Version 4.4 * Reinstated nessus fix and put output in a list for findings * Changed Get-AdminSDHolders with Get-PrivilegedGroupAccounts [ ] Version 4.3 * Temp fix with nessus output [ ] Version 4.2 * Bug fix on cpassword count [ ] Version 4.1 * Loads of fixes * Works with Powershellv2 again now * Filtered out disabled accounts * Improved domain trusts checking * OUperms improvements and filtering * Check for w2k * Fixed typos/spelling and various other fixes [ ] Version 4.0 * Added XML output for import to CheckSecCanopy [ ] Version 3.5 * Added KB more references for internal use [ ] Version 3.4 * Added KB references for internal use [ ] Version 3.3 * Added a greater level of accuracy to Inactive Accounts (thanks exceedio) [ ] Version 3.2 * Added search for DCs not owned by Domain Admins group [ ] Version 3.1 * Added progress to functions that have count * Added check for transitive trusts [ ] Version 3.0 * Added ability to choose functions before runtime * Cleaned up get-ouperms output [ ] Version 2.5 * Bug fixes to version check for 2012R2 or greater specific checks [ ] Version 2.4 * Forked project * Added Get-OUPerms, Get-LAPSStatus, Get-AdminSDHolders, Get-ProtectedUsers and Get-AuthenticationPoliciesAndSilos functions * Also added FineGrainedPasswordPolicies to Get-PasswordPolicy and changed order slightly [ ] Version 2.3 * Added more useful user output to .txt files (Cheers DK) [ ] Version 2.2 * Minor typo fix [ ] Version 2.1 * Added check for null sessions [ ] Version 2.0 * Multiple Additions and knocked off lots of the todo list [ ] Version 1.9 * Fixed bug, that used Administrator account name instead of UID 500 and a bug with inactive accounts timespan [ ] Version 1.8 * Added check for last time 'Administrator' account logged on [ ] Version 1.6 * Added Get-FunctionalLevel and krbtgt password last changed check [ ] Version 1.5 * Added Get-HostDetails to output simple info like username, hostname, etc... [ ] Version 1.4 * Added Get-WinVersion version to assist with some checks (SMBv1 currently) [ ] Version 1.3 * Added XML output for GPO (for offline processing using grouper https://github.com/l0ss/Grouper/blob/master/grouper.psm1) [ ] Version 1.2 * Added check for modules [ ] Version 1.1 * Fixed bug where SYSVOL research returns empty [ ] Version 1.0 * First release .EXAMPLE PS> ADAudit.ps1 -installdeps -all Install external features and launch all checks .EXAMPLE PS> ADAudit.ps1 -all Launch all checks (but do not install external modules) .EXAMPLE PS> ADAudit.ps1 -installdeps Installs optionnal features (DSInternals) .EXAMPLE PS> ADAudit.ps1 -hostdetails -domainaudit Retrieves hostname and other useful audit info Retrieves information about the AD such as functional level #> [CmdletBinding()] Param ( [switch]$installdeps = $false, [switch]$hostdetails = $false, [switch]$domainaudit = $false, [switch]$trusts = $false, [switch]$accounts = $false, [switch]$passwordpolicy = $false, [switch]$ntds = $false, [switch]$oldboxes = $false, [switch]$gpo = $false, [switch]$ouperms = $false, [switch]$laps = $false, [switch]$authpolsilos = $false, [switch]$insecurednszone = $false, [switch]$recentchanges = $false, [switch]$all = $false ) $versionnum = "v5.4" $AdministratorTranslation = @("Administrator","Administrateur","Administrador")#If missing put the default Administrator name for your own language here Function Get-Variables(){#Retrieve group names and OS version $script:OSVersion = (Get-Itemproperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName $script:Administrators = (Get-ADGroup -Identity S-1-5-32-544).SamAccountName $script:Users = (Get-ADGroup -Identity S-1-5-32-545).SamAccountName $script:DomainAdminsSID = ((Get-ADDomain -Current LoggedOnUser).domainsid.value)+"-512" $script:DomainUsersSID = ((Get-ADDomain -Current LoggedOnUser).domainsid.value)+"-513" $script:DomainControllersSID = ((Get-ADDomain -Current LoggedOnUser).domainsid.value)+"-516" $script:SchemaAdminsSID = ((Get-ADDomain -Current LoggedOnUser).domainsid.value)+"-518" $script:EnterpriseAdminsSID = ((Get-ADDomain -Current LoggedOnUser).domainsid.value)+"-519" $script:EveryOneSID = New-Object System.Security.Principal.SecurityIdentifier "S-1-1-0" $script:EntrepriseDomainControllersSID = New-Object System.Security.Principal.SecurityIdentifier "S-1-5-9" $script:AuthenticatedUsersSID = New-Object System.Security.Principal.SecurityIdentifier "S-1-5-11" $script:SystemSID = New-Object System.Security.Principal.SecurityIdentifier "S-1-5-18" $script:LocalServiceSID = New-Object System.Security.Principal.SecurityIdentifier "S-1-5-19" $script:DomainAdmins = (Get-ADGroup -Identity $DomainAdminsSID).SamAccountName $script:DomainUsers = (Get-ADGroup -Identity $DomainUsersSID).SamAccountName $script:DomainControllers = (Get-ADGroup -Identity $DomainControllersSID).SamAccountName $script:SchemaAdmins = (Get-ADGroup -Identity $SchemaAdminsSID).SamAccountName $script:EnterpriseAdmins = (Get-ADGroup -Identity $EnterpriseAdminsSID).SamAccountName $script:EveryOne = $EveryOneSID.Translate([System.Security.Principal.NTAccount]).Value $script:EntrepriseDomainControllers = $EntrepriseDomainControllersSID.Translate([System.Security.Principal.NTAccount]).Value $script:AuthenticatedUsers = $AuthenticatedUsersSID.Translate([System.Security.Principal.NTAccount]).Value $script:System = $SystemSID.Translate([System.Security.Principal.NTAccount]).Value $script:LocalService = $LocalServiceSID.Translate([System.Security.Principal.NTAccount]).Value Write-Both " [+] Administrators : $Administrators" Write-Both " [+] Users : $Users" Write-Both " [+] Domain Admins : $DomainAdmins" Write-Both " [+] Domain Users : $DomainUsers" Write-Both " [+] Domain Controllers : $DomainControllers" Write-Both " [+] Schema Admins : $SchemaAdmins" Write-Both " [+] Enterprise Admins : $EnterpriseAdmins" Write-Both " [+] Every One : $EveryOne" Write-Both " [+] Entreprise Domain Controllers: $EntrepriseDomainControllers" Write-Both " [+] Authenticated Users : $AuthenticatedUsers" Write-Both " [+] System : $System" Write-Both " [+] Local Service : $LocalService" } Function Write-Both(){#Writes to console screen and output file Write-Host "$args" Add-Content -Path "$outputdir\consolelog.txt" -Value "$args" } Function Write-Nessus-Header(){#Creates nessus XML file header Add-Content -Path "$outputdir\adaudit.nessus" -Value "" Add-Content -Path "$outputdir\adaudit.nessus" -Value "" Add-Content -Path "$outputdir\adaudit.nessus" -Value "" } Function Write-Nessus-Finding( [string]$pluginname, [string]$pluginid, [string]$pluginexample){ Add-Content -Path "$outputdir\adaudit.nessus" -Value "" Add-Content -Path "$outputdir\adaudit.nessus" -Value "There's an issue with $pluginname" Add-Content -Path "$outputdir\adaudit.nessus" -Value "remoteLow" Add-Content -Path "$outputdir\adaudit.nessus" -Value "CCS Recommends fixing the issues with $pluginname on the host" Add-Content -Path "$outputdir\adaudit.nessus" -Value "There's an issue with the $pluginname settings on the host" Add-Content -Path "$outputdir\adaudit.nessus" -Value "$pluginexample" } Function Write-Nessus-Footer(){ Add-Content -Path "$outputdir\adaudit.nessus" -Value "" } Function Get-DNSZoneInsecure{#Check DNS zones allowing insecure updates if($OSVersion -notlike "Windows Server 2008*"){ $count = 0 $progresscount = 0 $insecurezones = Get-DnsServerZone | Where-Object {$_.DynamicUpdate -like '*nonsecure*'} $totalcount = ($insecurezones | Measure-Object | Select-Object Count).count if($totalcount -gt 0){ foreach($insecurezone in $insecurezones ){ Add-Content -Path "$outputdir\insecure_dns_zones.txt" -Value "The DNS Zone $($insecurezone.ZoneName) allows insecure updates ($($insecurezone.DynamicUpdate))" } Write-Both " [!] There were $totalcount DNS zones configured to allow insecure updates (KB842)" Write-Nessus-Finding "InsecureDNSZone" "KB842" ([System.IO.File]::ReadAllText("$outputdir\insecure_dns_zones.txt")) } }else{ Write-Both " [-] Not Windows 2012 or above, skipping Get-DNSZoneInsecure check." } } Function Get-OUPerms{#Check for non-standard perms for authenticated users, domain users, users and everyone groups $count = 0 $progresscount = 0 $objects = (Get-ADObject -Filter *) $totalcount = ($objects | Measure-Object | Select-Object Count).count foreach($object in $objects){ if($totalcount -eq 0){ break } $progresscount++ Write-Progress -Activity "Searching for non standard permissions for authenticated users..." -Status "Currently identifed $count" -PercentComplete ($progresscount / $totalcount*100) if($OSVersion -like "Windows Server 2019*" -or $OSVersion -like "Windows Server 2022*"){ $output = (Get-Acl "Microsoft.ActiveDirectory.Management.dll\ActiveDirectory:://RootDSE/$object").Access | Where-Object {($_.IdentityReference -eq "$AuthenticatedUsers") -or ($_.IdentityReference -eq "$EveryOne") -or ($_.IdentityReference -like "*\$DomainUsers") -or ($_.IdentityReference -eq "BUILTIN\$Users")} | Where-Object {($_.ActiveDirectoryRights -ne 'GenericRead') -and ($_.ActiveDirectoryRights -ne 'GenericExecute') -and ($_.ActiveDirectoryRights -ne 'ExtendedRight') -and ($_.ActiveDirectoryRights -ne 'ReadControl') -and ($_.ActiveDirectoryRights -ne 'ReadProperty') -and ($_.ActiveDirectoryRights -ne 'ListObject') -and ($_.ActiveDirectoryRights -ne 'ListChildren') -and ($_.ActiveDirectoryRights -ne 'ListChildren, ReadProperty, ListObject') -and ($_.ActiveDirectoryRights -ne 'ReadProperty, GenericExecute') -and ($_.AccessControlType -ne 'Deny')} }else{ $output = (Get-Acl AD:$object).Access | Where-Object {($_.IdentityReference -eq "$AuthenticatedUsers") -or ($_.IdentityReference -eq "$EveryOne") -or ($_.IdentityReference -like "*\$DomainUsers") -or ($_.IdentityReference -eq "BUILTIN\$Users")} | Where-Object {($_.ActiveDirectoryRights -ne 'GenericRead') -and ($_.ActiveDirectoryRights -ne 'GenericExecute') -and ($_.ActiveDirectoryRights -ne 'ExtendedRight') -and ($_.ActiveDirectoryRights -ne 'ReadControl') -and ($_.ActiveDirectoryRights -ne 'ReadProperty') -and ($_.ActiveDirectoryRights -ne 'ListObject') -and ($_.ActiveDirectoryRights -ne 'ListChildren') -and ($_.ActiveDirectoryRights -ne 'ListChildren, ReadProperty, ListObject') -and ($_.ActiveDirectoryRights -ne 'ReadProperty, GenericExecute') -and ($_.AccessControlType -ne 'Deny')} } if($output -ne $null){ $count++ Add-Content -Path "$outputdir\ou_permissions.txt" -Value "OU: $object" Add-Content -Path "$outputdir\ou_permissions.txt" -Value "[!] Rights: $($output.IdentityReference) $($output.ActiveDirectoryRights) $($output.AccessControlType)" } } Write-Progress -Activity "Searching for non standard permissions for authenticated users..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] Issue identified, see $outputdir\ou_permissions.txt" Write-Nessus-Finding "OUPermissions" "KB551" ([System.IO.File]::ReadAllText("$outputdir\ou_permissions.txt")) } } Function Get-LAPSStatus{#Check for presence of LAPS in domain try{ Get-ADObject "CN=ms-Mcs-AdmPwd,CN=Schema,CN=Configuration,$((Get-ADDomain).DistinguishedName)" -ErrorAction Stop | Out-Null Write-Both " [+] LAPS Installed in domain" } catch{ Write-Both " [!] LAPS Not Installed in domain (KB258)" Write-Nessus-Finding "LAPSMissing" "KB258" "LAPS Not Installed in domain" } if(Get-Module -ListAvailable -Name AdmPwd.PS){ Import-Module AdmPwd.PS $count = 0 $missingComputers = (Get-ADComputer -Filter {ms-Mcs-AdmPwd -notlike "*"}).Name $totalcount = ($missingComputers | Measure-Object | Select-Object Count).count if($totalcount -gt 0){ $missingComputers | Add-Content -Path $outputdir\laps_missing-computers.txt Write-Both " [!] Some computers/servers don't have LAPS password set, see $outputdir\laps_missing-computers.txt" Write-Nessus-Finding "LAPSMissingorExpired" "KB258" ([System.IO.File]::ReadAllText("$outputdir\laps_missing-computers.txt")) } $count = 0 $computersList = (Get-ADComputer -Filter {ms-Mcs-AdmPwdExpirationTime -like "*"} -Properties ms-Mcs-AdmPwdExpirationTime | select Name,ms-Mcs-AdmPwdExpirationTime) foreach($computer in $computersList ){ $expiration = [datetime]::FromFileTime($computer.'ms-Mcs-AdmPwdExpirationTime') $today = Get-Date if($expiration -lt $today){ $count++ "$($computer.Name) password is expired since $expiration" | Add-Content -Path $outputdir\laps_expired-passwords.txt } } if($count -gt 0){ Write-Both " [!] Some computers/servers have LAPS password expired, see $outputdir\laps_expired-passwords.txt" Write-Nessus-Finding "LAPSMissingorExpired" "KB258" ([System.IO.File]::ReadAllText("$outputdir\laps_expired-passwords.txt")) } Get-ADOrganizationalUnit -Filter * | Find-AdmPwdExtendedRights -PipelineVariable OU | foreach{ $_.ExtendedRightHolders | foreach{ if($_ -ne $System){ "$_ can read password attribute of $($Ou.ObjectDN)" | Add-Content -Path $outputdir\laps_read-extendedrights.txt } } } Write-Both " [!] LAPS extended rights exported, see $outputdir\laps_read-extendedrights.txt" Write-Nessus-Finding "LAPSMissingorExpired" "KB258" ([System.IO.File]::ReadAllText("$outputdir\laps_read-extendedrights.txt")) }else{ Write-Both " [!] LAPS PowerShell module is not installed, can't run LAPS checks on this DC" } } Function Get-PrivilegedGroupAccounts{#Lists users in Admininstrators, DA and EA groups [array]$privilegedusers = @() $privilegedusers += Get-ADGroupMember $Administrators -Recursive $privilegedusers += Get-ADGroupMember $DomainAdmins -Recursive $privilegedusers += Get-ADGroupMember $EnterpriseAdmins -Recursive $privusersunique = $privilegedusers | Sort-Object -Unique $count = 0 $totalcount = ($privilegedusers | Measure-Object | Select-Object Count).count foreach($account in $privusersunique){ if($totalcount -eq 0){ break } Write-Progress -Activity "Searching for users who are in privileged groups..." -Status "Currently identifed $count" -PercentComplete ($count / $totalcount*100) Add-Content -Path "$outputdir\accounts_userPrivileged.txt" -Value "$($account.SamAccountName) ($($account.Name))" $count++ } Write-Progress -Activity "Searching for users who are in privileged groups..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] There are $count accounts in privileged groups, see accounts_userPrivileged.txt (KB426)" Write-Nessus-Finding "AdminSDHolders" "KB426" ([System.IO.File]::ReadAllText("$outputdir\accounts_userPrivileged.txt")) } } Function Get-ProtectedUsers{#Lists users in "Protected Users" group (2012R2 and above) $DomainLevel = (Get-ADDomain).domainMode if($DomainLevel -eq "Windows2012Domain" -or $DomainLevel -eq "Windows2012R2Domain" -or $DomainLevel -eq "Windows2016Domain"){#Checking for 2012 or above domain functional level $ProtectedUsersSID = ((Get-ADDomain -Current LoggedOnUser).domainsid.value)+"-525" $ProtectedUsers = (Get-ADGroup -Identity $ProtectedUsersSID).SamAccountName $count = 0 $protectedaccounts = (Get-ADGroup $ProtectedUsers -Properties members).Members $totalcount = ($protectedaccounts | Measure-Object | Select-Object Count).count foreach($members in $protectedaccounts){ if($totalcount -eq 0){ break } Write-Progress -Activity "Searching for protected users..." -Status "Currently identifed $count" -PercentComplete ($count / $totalcount*100) $account = Get-ADObject $members -Properties SamAccountName Add-Content -Path "$outputdir\accounts_protectedusers.txt" -Value "$($account.SamAccountName) ($($account.Name))" $count++ } Write-Progress -Activity "Searching for protected users..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] There are $count accounts in the 'Protected Users' group, see accounts_protectedusers.txt" Write-Nessus-Finding "ProtectedUsers" "KB549" ([System.IO.File]::ReadAllText("$outputdir\accounts_protectedusers.txt")) } } else {Write-Both " [-] Not Windows 2012 Domain Functional level or above, skipping Get-ProtectedUsers check."} } Function Get-AuthenticationPoliciesAndSilos {#Lists any authentication policies and silos (2012R2 and above) if([single](Get-WinVersion) -ge [single]6.3){#NT6.2 or greater detected so running this script $count = 0 foreach($policy in Get-ADAuthenticationPolicy -Filter *){ Write-Both " [!] Found $policy Authentication Policy" $count++ } if($count -lt 1){ Write-Both " [!] There were no AD Authentication Policies found in the domain" } $count = 0 foreach($policysilo in Get-ADAuthenticationPolicySilo -Filter *){ Write-Both " [!] Found $policysilo Authentication Policy Silo" $count++ } if($count -lt 1){ Write-Both " [!] There were no AD Authentication Policy Silos found in the domain" } } } Function Get-MachineAccountQuota{#Get number of machines a user can add to a domain $MachineAccountQuota = (Get-ADDomain | select -ExpandProperty DistinguishedName | Get-ADObject -Property 'ms-DS-MachineAccountQuota' | select -ExpandProperty ms-DS-MachineAccountQuota) if($MachineAccountQuota -gt 0){ Write-Both " [!] Domain users can add $MachineAccountQuota devices to the domain! (KB251)" Write-Nessus-Finding "DomainAccountQuota" "KB251" "Domain users can add $MachineAccountQuota devices to the domain" } } Function Get-PasswordPolicy{ Write-Both " [+] Checking default password policy" if(!(Get-ADDefaultDomainPasswordPolicy).ComplexityEnabled){ Write-Both " [!] Password Complexity not enabled (KB262)" Write-Nessus-Finding "PasswordComplexity" "KB262" "Password Complexity not enabled" } if((Get-ADDefaultDomainPasswordPolicy).LockoutThreshold -lt 5){ Write-Both " [!] Lockout threshold is less than 5, currently set to $((Get-ADDefaultDomainPasswordPolicy).LockoutThreshold) (KB263)" Write-Nessus-Finding "LockoutThreshold" "KB263" "Lockout threshold is less than 5, currently set to $((Get-ADDefaultDomainPasswordPolicy).LockoutThreshold)" } if((Get-ADDefaultDomainPasswordPolicy).MinPasswordLength -lt 14){ Write-Both " [!] Minimum password length is less than 14, currently set to $((Get-ADDefaultDomainPasswordPolicy).MinPasswordLength) (KB262)" Write-Nessus-Finding "PasswordLength" "KB262" "Minimum password length is less than 14, currently set to $((Get-ADDefaultDomainPasswordPolicy).MinPasswordLength)" } if((Get-ADDefaultDomainPasswordPolicy).ReversibleEncryptionEnabled){ Write-Both " [!] Reversible encryption is enabled" } if((Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge -eq "00:00:00"){ Write-Both " [!] Passwords do not expire (KB254)" Write-Nessus-Finding "PasswordsDoNotExpire" "KB254" "Passwords do not expire" } if((Get-ADDefaultDomainPasswordPolicy).PasswordHistoryCount -lt 12){ Write-Both " [!] Passwords history is less than 12, currently set to $((Get-ADDefaultDomainPasswordPolicy).PasswordHistoryCount) (KB262)" Write-Nessus-Finding "PasswordHistory" "KB262" "Passwords history is less than 12, currently set to $((Get-ADDefaultDomainPasswordPolicy).PasswordHistoryCount)" } if((Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Control\Lsa).NoLmHash -eq 0){ Write-Both " [!] LM Hashes are stored! (KB510)" Write-Nessus-Finding "LMHashesAreStored" "KB510" "LM Hashes are stored" } Write-Both " [-] Finished checking default password policy" Write-Both " [+] Checking fine-grained password policies if they exist" foreach($finegrainedpolicy in Get-ADFineGrainedPasswordPolicy -Filter *){ $finegrainedpolicyappliesto=$finegrainedpolicy.AppliesTo Write-Both " [!] Policy: $finegrainedpolicy" Write-Both " [!] AppliesTo: $($finegrainedpolicyappliesto)" if(!($finegrainedpolicy).PasswordComplexity){ Write-Both " [!] Password Complexity not enabled (KB262)" Write-Nessus-Finding "PasswordComplexity" "KB262" "Password Complexity not enabled for $finegrainedpolicy" } if(($finegrainedpolicy).LockoutThreshold -lt 5){ Write-Both " [!] Lockout threshold is less than 5, currently set to $($finegrainedpolicy).LockoutThreshold) (KB263)" Write-Nessus-Finding "LockoutThreshold" "KB263" " Lockout threshold for $finegrainedpolicy is less than 5, currently set to $(($finegrainedpolicy).LockoutThreshold)" } if(($finegrainedpolicy).MinPasswordLength -lt 14){ Write-Both " [!] Minimum password length is less than 14, currently set to $(($finegrainedpolicy).MinPasswordLength) (KB262)" Write-Nessus-Finding "PasswordLength" "KB262" "Minimum password length for $finegrainedpolicy is less than 14, currently set to $(($finegrainedpolicy).MinPasswordLength)" } if(($finegrainedpolicy).ReversibleEncryptionEnabled){ Write-Both " [!] Reversible encryption is enabled" } if(($finegrainedpolicy).MaxPasswordAge -eq "00:00:00"){ Write-Both " [!] Passwords do not expire (KB254)" } if(($finegrainedpolicy).PasswordHistoryCount -lt 12){ Write-Both " [!] Passwords history is less than 12, currently set to $(($finegrainedpolicy).PasswordHistoryCount) (KB262)" Write-Nessus-Finding "PasswordHistory" "KB262" "Passwords history for $finegrainedpolicy is less than 12, currently set to $(($finegrainedpolicy).PasswordHistoryCount)" } } Write-Both " [-] Finished checking fine-grained password policy" } Function Get-NULLSessions{ if((Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Control\Lsa).RestrictAnonymous -eq 0){ Write-Both " [!] RestrictAnonymous is set to 0! (KB81)" Write-Nessus-Finding "NullSessions" "KB81" " RestrictAnonymous is set to 0" } if((Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Control\Lsa).RestrictAnonymousSam -eq 0){ Write-Both " [!] RestrictAnonymousSam is set to 0! (KB81)" Write-Nessus-Finding "NullSessions" "KB81" " RestrictAnonymous is set to 0" } if((Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Control\Lsa).everyoneincludesanonymous -eq 1){ Write-Both " [!] EveryoneIncludesAnonymous is set to 1! (KB81)" Write-Nessus-Finding "NullSessions" "KB81" "EveryoneIncludesAnonymous is set to 1" } } Function Get-DomainTrusts{#Lists domain trusts if they are bad foreach($trust in (Get-ADObject -Filter {objectClass -eq "trustedDomain"} -Properties TrustPartner,TrustDirection,trustType,trustAttributes)){ if($trust.TrustDirection -eq 2){ if($trust.TrustAttributes -eq 1 -or $trust.TrustAttributes -eq 4){#1 means trust is non-transitive, 4 is external so we check for anything but that Write-Both " [!] The domain $($trust.Name) is trusted by $env:UserDomain! (KB250)" Write-Nessus-Finding "DomainTrusts" "KB250" "The domain $($trust.Name) is trusted by $env:UserDomain." }else{ Write-Both " [!] The domain $($trust.Name) is trusted by $env:UserDomain and it is Transitive! (KB250)" Write-Nessus-Finding "DomainTrusts" "KB250" "The domain $($trust.Name) is trusted by $env:UserDomain and it is Transitive!" } } if($trust.TrustDirection -eq 3){ if($trust.TrustAttributes -eq 1 -or $trust.TrustAttributes -eq 4){#1 means trust is non-transitive, 4 is external so we check for anything but that Write-Both " [!] The domain $($trust.Name) is trusted by $env:UserDomain! (KB250)" Write-Nessus-Finding "DomainTrusts" "KB250" "The domain $($trust.Name) is trusted by $env:UserDomain." }else{ Write-Both " [!] The domain $($trust.Name) is trusted by $env:UserDomain and it is Transitive! (KB250)" Write-Nessus-Finding "DomainTrusts" "KB250" "The domain $($trust.Name) is trusted by $env:UserDomain and it is Transitive!" } } } } Function Get-WinVersion{ $WinVersion = [single]([string][environment]::OSVersion.Version.Major + "." + [string][environment]::OSVersion.Version.Minor) return [single]$WinVersion } Function Get-SMB1Support{#Check if server supports SMBv1 if([single](Get-WinVersion) -le [single]6.1){#NT6.1 or less detected so checking reg key if(!(Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters).SMB1 -eq 0){ Write-Both " [!] SMBv1 is not disabled (KB290)" Write-Nessus-Finding "SMBv1Support" "KB290" "SMBv1 is enabled" } }elseif([single](Get-WinVersion) -ge [single]6.2){#NT6.2 or greater detected so using powershell function if((Get-SmbServerConfiguration).EnableSMB1Protocol){ Write-Both " [!] SMBv1 is enabled! (KB290)" Write-Nessus-Finding "SMBv1Support" "KB290" "SMBv1 is enabled" } } } Function Get-UserPasswordNotChangedRecently{#Reports users that haven't changed passwords in more than 90 days $count = 0 $DaysAgo = (Get-Date).AddDays(-90) $accountsoldpasswords = Get-ADUser -Filter {PwdLastSet -lt $DaysAgo -and Enabled -eq "true"} -Properties PasswordLastSet $totalcount = ($accountsoldpasswords | Measure-Object | Select-Object Count).count foreach($account in $accountsoldpasswords){ if($totalcount -eq 0){ break } Write-Progress -Activity "Searching for passwords older than 90days..." -Status "Currently identifed $count" -PercentComplete ($count / $totalcount*100) if($account.PasswordLastSet){ $datelastchanged = $account.PasswordLastSet }else{ $datelastchanged = "Never" } Add-Content -Path "$outputdir\accounts_with_old_passwords.txt" -Value "User $($account.SamAccountName) ($($account.Name)) has not changed their password since $datelastchanged" $count++ } Write-Progress -Activity "Searching for passwords older than 90days..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] $count accounts with passwords older than 90days, see accounts_with_old_passwords.txt (KB550)" Write-Nessus-Finding "AccountsWithOldPasswords" "KB550" ([System.IO.File]::ReadAllText("$outputdir\accounts_with_old_passwords.txt")) } $krbtgtPasswordDate = (Get-ADUser -Filter {SamAccountName -eq "krbtgt"} -Properties PasswordLastSet).PasswordLastSet if($krbtgtPasswordDate -lt (Get-Date).AddDays(-180)){ Write-Both " [!] krbtgt password not changed since $krbtgtPasswordDate! (KB253)" Write-Nessus-Finding "krbtgtPasswordNotChanged" "KB253" "krbtgt password not changed since $krbtgtPasswordDate" } } Function Get-GPOtoFile{#Outputs complete GPO report if(Test-Path "$outputdir\GPOReport.html"){ Remove-Item "$outputdir\GPOReport.html" -Recurse } Get-GPOReport -All -ReportType HTML -Path "$outputdir\GPOReport.html" Write-Both " [+] GPO Report saved to GPOReport.html" if(Test-Path "$outputdir\GPOReport.xml"){ Remove-Item "$outputdir\GPOReport.xml" -Recurse } Get-GPOReport -All -ReportType XML -Path "$outputdir\GPOReport.xml" Write-Both " [+] GPO Report saved to GPOReport.xml, now run Grouper offline using the following command (KB499)" Write-Both " [+] PS>Import-Module Grouper.psm1 ; Invoke-AuditGPOReport -Path C:\GPOReport.xml -Level 3" } Function Get-GPOsPerOU{#Lists all OUs and which GPOs apply to them $count = 0 $ousgpos = @(Get-ADOrganizationalUnit -Filter *) $totalcount = ($ousgpos | Measure-Object | Select-Object Count).count foreach($ouobject in $ousgpos){ if($totalcount -eq 0){ break } Write-Progress -Activity "Identifying which GPOs apply to which OUs..." -Status "Currently identifed $count OUs" -PercentComplete ($count / $totalcount*100) $combinedgpos = ($(((Get-GPInheritance -Target $ouobject).InheritedGpoLinks) | select DisplayName) | ForEach-Object { $_.DisplayName }) -join ',' Add-Content -Path "$outputdir\ous_inheritedGPOs.txt" -Value "$($ouobject.Name) Inherits these GPOs: $combinedgpos" $count++ } Write-Progress -Activity "Identifying which GPOs apply to which OUs..." -Status "Ready" -Completed Write-Both " [+] Inherited GPOs saved to ous_inheritedGPOs.txt" } Function Get-NTDSdit{#Dumps NTDS.dit, SYSTEM and SAM for password cracking if(Test-Path "$outputdir\ntds.dit"){ Remove-Item "$outputdir\ntds.dit" -Recurse } $outputdirntds = '\"' + $outputdir + '\ntds.dit\"' $command = "ntdsutil `"ac in ntds`" `"ifm`" `"cr fu $outputdirntds `" q q" $hide = cmd.exe /c "$command" 2>&1 Write-Both " [+] NTDS.dit, SYSTEM & SAM saved to output folder" Write-Both " [+] Use secretsdump.py -system registry/SYSTEM -ntds Active\ Directory/ntds.dit LOCAL -outputfile customer" } Function Get-SYSVOLXMLS{#Finds XML files in SYSVOL (thanks --> https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPPassword.ps1) $XMLFiles = Get-ChildItem -Path "\\$Env:USERDNSDOMAIN\SYSVOL" -Recurse -ErrorAction SilentlyContinue -Include 'Groups.xml','Services.xml','Scheduledtasks.xml','DataSources.xml','Printers.xml','Drives.xml' $count = 0 if($XMLFiles){ $progresscount = 0 $totalcount = ($XMLFiles | Measure-Object | Select-Object Count).count foreach($File in $XMLFiles){ if($totalcount -eq 0){ break } $progresscount++ Write-Progress -Activity "Searching SYSVOL *.xmls for cpassword..." -Status "Currently searched through $count" -PercentComplete ($progresscount / $totalcount*100) $Filename = Split-Path $File -Leaf $Distinguishedname = (Split-Path (Split-Path (Split-Path( Split-Path (Split-Path $File -Parent) -Parent ) -Parent ) -Parent) -Leaf).Substring(1).TrimEnd('}') [xml]$Xml = Get-Content ($File) if($Xml.innerxml -like "*cpassword*" -and $Xml.innerxml -notlike '*cpassword=""*'){ if(!(Test-Path "$outputdir\sysvol")){ New-Item -ItemType Directory -Path "$outputdir\sysvol" | Out-Null } Write-Both " [!] cpassword found in file, copying to output folder (KB329)" Write-Both " $File" Copy-Item -Path $File -Destination $outputdir\sysvol\$Distinguishedname.$Filename $count++ } } Write-Progress -Activity "Searching SYSVOL *.xmls for cpassword..." -Status "Ready" -Completed } if($count -eq 0){ Write-Both " ...cpassword not found in the $($XMLFiles.count) XML files found." }else{ $GPOxml = (Get-Content "$outputdir\sysvol\*.xml" -ErrorAction SilentlyContinue) $GPOxml = $GPOxml -Replace "<", "<" $GPOxml = $GPOxml -Replace ">", ">" Write-Nessus-Finding "GPOPasswordStorage" "KB329" "$GPOxml" } } Function Get-InactiveAccounts{#Lists accounts not used in past 180 days plus some checks for admin accounts $count = 0 $progresscount = 0 $inactiveaccounts = Search-ADaccount -AccountInactive -Timespan (New-TimeSpan -Days 180) -UsersOnly | Where-Object {$_.Enabled -eq $true} $totalcount = ($inactiveaccounts | Measure-Object | Select-Object Count).count foreach($account in $inactiveaccounts){ if($totalcount -eq 0){ break } $progresscount++ Write-Progress -Activity "Searching for inactive users..." -Status "Currently identifed $count" -PercentComplete ($progresscount / $totalcount*100) if($account.Enabled){ if($account.LastLogonDate){ $userlastused = $account.LastLogonDate }else{ $userlastused = "Never" } Add-Content -Path "$outputdir\accounts_inactive.txt" -Value "User $($account.SamAccountName) ($($account.Name)) has not logged on since $userlastused" $count++ } } Write-Progress -Activity "Searching for inactive users..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] $count inactive user accounts(180days), see accounts_inactive.txt (KB500)" Write-Nessus-Finding "InactiveAccounts" "KB500" ([System.IO.File]::ReadAllText("$outputdir\accounts_inactive.txt")) } } Function Get-AdminAccountChecks{#Checks if Administrator account has been renamed, replaced and is no longer used. $AdministratorSID = ((Get-ADDomain -Current LoggedOnUser).domainsid.value)+"-500" $AdministratorSAMAccountName = (Get-ADUser -Filter {SID -eq $AdministratorSID} -Properties SamAccountName).SamAccountName $AdministratorName = (Get-ADUser -Filter {SID -eq $AdministratorSID} -Properties SamAccountName).Name if($AdministratorTranslation -contains $AdministratorSAMAccountName){ Write-Both " [!] Local Administrator account (UID500) has not been renamed (KB309)" Write-Nessus-Finding "AdminAccountRenamed" "KB309" "Local Administrator account (UID500) has not been renamed" }else{ $count = 0 foreach($AdminName in $AdministratorTranslation){ if((Get-ADUser -Filter {SamAccountName -eq $AdminName})) { $count++ } } if($count -eq 0){ Write-Both " [!] Local Administrator account renamed to $AdministratorSAMAccountName ($($AdministratorName)), but a dummy account not made in it's place! (KB309)" Write-Nessus-Finding "AdminAccountRenamed" "KB309" "Local Admin account renamed to $AdministratorSAMAccountName ($($AdministratorName)), but a dummy account not made in it's place" } } $AdministratorLastLogonDate = (Get-ADUser -Filter {SID -eq $AdministratorSID} -Properties LastLogonDate).LastLogonDate if($AdministratorLastLogonDate -gt (Get-Date).AddDays(-180)){ Write-Both " [!] UID500 (LocalAdministrator) account is still used, last used $AdministratorLastLogonDate! (KB309)" Write-Nessus-Finding "AdminAccountRenamed" "KB309" "UID500 (LocalAdmini) account is still used, last used $AdministratorLastLogonDate" } } Function Get-DisabledAccounts{#Lists disabled accounts $disabledaccounts = Search-ADaccount -AccountDisabled -UsersOnly $count = 0 $totalcount = ($disabledaccounts | Measure-Object | Select-Object Count).count foreach($account in $disabledaccounts){ if($totalcount -eq 0){ break } Write-Progress -Activity "Searching for disabled users..." -Status "Currently identifed $count" -PercentComplete ($count / $totalcount*100) Add-Content -Path "$outputdir\accounts_disabled.txt" -Value "Account $($account.SamAccountName) ($($account.Name)) is disabled" $count++ } Write-Progress -Activity "Searching for disabled users..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] $count disabled user accounts, see accounts_disabled.txt (KB501)" Write-Nessus-Finding "DisabledAccounts" "KB501" ([System.IO.File]::ReadAllText("$outputdir\accounts_disabled.txt")) } } Function Get-LockedAccounts{#Lists locked accounts $lockedAccounts = Get-ADUser -Filter * -Properties LockedOut | Where-Object {$_.LockedOut -eq $true} $count = 0 $totalcount = ($lockedAccounts | Measure-Object | Select-Object Count).Count foreach($account in $lockedAccounts){ if($totalcount -eq 0){ break } Write-Progress -Activity "Searching for locked users..." -Status "Currently identifed $count" -PercentComplete ($count / $totalcount*100) Add-Content -Path "$outputdir\accounts_locked.txt" -Value "Account $($account.SamAccountName) ($($account.Name)) is locked" $count++ } Write-Progress -Activity "Searching for locked users..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] $count locked user accounts, see accounts_locked.txt" } } Function Get-AccountPassDontExpire{#Lists accounts who's passwords dont expire $count = 0 $nonexpiringpasswords = Search-ADAccount -PasswordNeverExpires -UsersOnly | Where-Object {$_.Enabled -eq $true} $totalcount = ($nonexpiringpasswords | Measure-Object | Select-Object Count).count foreach($account in $nonexpiringpasswords){ if($totalcount -eq 0){ break } Write-Progress -Activity "Searching for users with passwords that dont expire..." -Status "Currently identifed $count" -PercentComplete ($count / $totalcount*100) Add-Content -Path "$outputdir\accounts_passdontexpire.txt" -Value "$($account.SamAccountName) ($($account.Name))" $count++ } Write-Progress -Activity "Searching for users with passwords that dont expire..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] There are $count accounts that don't expire, see accounts_passdontexpire.txt (KB254)" Write-Nessus-Finding "AccountsThatDontExpire" "KB254" ([System.IO.File]::ReadAllText("$outputdir\accounts_passdontexpire.txt")) } } Function Get-OldBoxes{#Lists 2000/2003/XP/Vista/7/2008 machines $count = 0 $oldboxes = Get-ADComputer -Filter {OperatingSystem -Like "*2003*" -and Enabled -eq "true" -or OperatingSystem -Like "*XP*" -and Enabled -eq "true" -or OperatingSystem -Like "*2000*" -and Enabled -eq "true" -or OperatingSystem -like '*Windows 7*' -and Enabled -eq "true" -or OperatingSystem -like '*vista*' -and Enabled -eq "true" -or OperatingSystem -like '*2008*' -and Enabled -eq "true"} -Property OperatingSystem $totalcount = ($oldboxes | Measure-Object | Select-Object Count).count foreach($machine in $oldboxes){ if($totalcount -eq 0){ break } Write-Progress -Activity "Searching for 2000/2003/XP/Vista/7/2008 devices joined to the domain..." -Status "Currently identifed $count" -PercentComplete ($count / $totalcount*100) Add-Content -Path "$outputdir\machines_old.txt" -Value "$($machine.Name), $($machine.OperatingSystem), $($machine.OperatingSystemServicePack), $($machine.OperatingSystemVersio), $($machine.IPv4Address)" $count++ } Write-Progress -Activity "Searching for 2000/2003/XP/Vista/7/2008 devices joined to the domain..." -Status "Ready" -Completed if($count -gt 0){ Write-Both " [!] We found $count machines running 2000/2003/XP/Vista/7/2008! see machines_old.txt (KB3/37/38/KB259)" Write-Nessus-Finding "OldBoxes" "KB259" ([System.IO.File]::ReadAllText("$outputdir\machines_old.txt")) } } Function Get-DCsNotOwnedByDA {#Searches for DC objects not owned by the Domain Admins group $count = 0 $progresscount = 0 $domaincontrollers = Get-ADComputer -Filter {PrimaryGroupID -eq 516 -or PrimaryGroupID -eq 521} -Property * $totalcount = ($domaincontrollers | Measure-Object | Select-Object Count).count if($totalcount -gt 0){ foreach($machine in $domaincontrollers){ $progresscount++ Write-Progress -Activity "Searching for DCs not owned by Domain Admins group..." -Status "Currently identifed $count" -PercentComplete ($progresscount / $totalcount*100) if($machine.ntsecuritydescriptor.Owner -ne "$env:UserDomain\$DomainAdmins"){ Add-Content -Path "$outputdir\dcs_not_owned_by_da.txt" -Value "$($machine.Name), $($machine.OperatingSystem), $($machine.OperatingSystemServicePack), $($machine.OperatingSystemVersio), $($machine.IPv4Address), owned by $($machine.ntsecuritydescriptor.Owner)" $count++ } } Write-Progress -Activity "Searching for DCs not owned by Domain Admins group..." -Status "Ready" -Completed } if($count -gt 0){ Write-Both " [!] We found $count DCs not owned by Domains Admins group! see dcs_not_owned_by_da.txt" Write-Nessus-Finding "DCsNotByDA" "KB547" ([System.IO.File]::ReadAllText("$outputdir\dcs_not_owned_by_da.txt")) } } Function Get-HostDetails{#Gets basic information about the host Write-Both " [+] Device Name: $env:ComputerName" Write-Both " [+] Domain Name: $env:UserDomain" Write-Both " [+] User Name : $env:UserName" Write-Both " [+] NT Version : $(Get-WinVersion)" $IPAddresses = [net.dns]::GetHostAddresses("") | select -ExpandProperty IP* foreach($ip in $IPAddresses){ if($ip -ne "::1"){ Write-Both " [+] IP Address : $ip" } } } Function Get-FunctionalLevel{#Gets the functional level for domain and forest $DomainLevel = (Get-ADDomain).domainMode if($DomainLevel -eq "Windows2000Domain" -and [single](Get-WinVersion) -gt 5.0) { Write-Both " [!] DomainLevel is reduced for backwards compatibility to $DomainLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "DomainLevel is reduced for backwards compatibility to $DomainLevel" } if($DomainLevel -eq "Windows2003InterimDomain" -and [single](Get-WinVersion) -gt 5.1) { Write-Both " [!] DomainLevel is reduced for backwards compatibility to $DomainLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "DomainLevel is reduced for backwards compatibility to $DomainLevel" } if($DomainLevel -eq "Windows2003Domain" -and [single](Get-WinVersion) -gt 5.2) { Write-Both " [!] DomainLevel is reduced for backwards compatibility to $DomainLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "DomainLevel is reduced for backwards compatibility to $DomainLevel" } if($DomainLevel -eq "Windows2008Domain" -and [single](Get-WinVersion) -gt 6.0) { Write-Both " [!] DomainLevel is reduced for backwards compatibility to $DomainLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "DomainLevel is reduced for backwards compatibility to $DomainLevel" } if($DomainLevel -eq "Windows2008R2Domain" -and [single](Get-WinVersion) -gt 6.1) { Write-Both " [!] DomainLevel is reduced for backwards compatibility to $DomainLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "DomainLevel is reduced for backwards compatibility to $DomainLevel" } if($DomainLevel -eq "Windows2012Domain" -and [single](Get-WinVersion) -gt 6.2) { Write-Both " [!] DomainLevel is reduced for backwards compatibility to $DomainLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "DomainLevel is reduced for backwards compatibility to $DomainLevel" } if($DomainLevel -eq "Windows2012R2Domain" -and [single](Get-WinVersion) -gt 6.3) { Write-Both " [!] DomainLevel is reduced for backwards compatibility to $DomainLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "DomainLevel is reduced for backwards compatibility to $DomainLevel" } if($DomainLevel -eq "Windows2016Domain" -and [single](Get-WinVersion) -gt 10.0){ Write-Both " [!] DomainLevel is reduced for backwards compatibility to $DomainLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "DomainLevel is reduced for backwards compatibility to $DomainLevel" } $ForestLevel = (Get-ADForest).ForestMode if($ForestLevel -eq "Windows2000Forest" -and [single](Get-WinVersion) -gt 5.0) { Write-Both " [!] ForestLevel is reduced for backwards compatibility to $ForestLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "ForestLevel is reduced for backwards compatibility to $ForestLevel" } if($ForestLevel -eq "Windows2003InterimForest" -and [single](Get-WinVersion) -gt 5.1) { Write-Both " [!] ForestLevel is reduced for backwards compatibility to $ForestLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "ForestLevel is reduced for backwards compatibility to $ForestLevel" } if($ForestLevel -eq "Windows2003Forest" -and [single](Get-WinVersion) -gt 5.2) { Write-Both " [!] ForestLevel is reduced for backwards compatibility to $ForestLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "ForestLevel is reduced for backwards compatibility to $ForestLevel" } if($ForestLevel -eq "Windows2008Forest" -and [single](Get-WinVersion) -gt 6.0) { Write-Both " [!] ForestLevel is reduced for backwards compatibility to $ForestLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "ForestLevel is reduced for backwards compatibility to $ForestLevel" } if($ForestLevel -eq "Windows2008R2Forest" -and [single](Get-WinVersion) -gt 6.1) { Write-Both " [!] ForestLevel is reduced for backwards compatibility to $ForestLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "ForestLevel is reduced for backwards compatibility to $ForestLevel" } if($ForestLevel -eq "Windows2012Forest" -and [single](Get-WinVersion) -gt 6.2) { Write-Both " [!] ForestLevel is reduced for backwards compatibility to $ForestLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "ForestLevel is reduced for backwards compatibility to $ForestLevel" } if($ForestLevel -eq "Windows2012R2Forest" -and [single](Get-WinVersion) -gt 6.3) { Write-Both " [!] ForestLevel is reduced for backwards compatibility to $ForestLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "ForestLevel is reduced for backwards compatibility to $ForestLevel" } if($ForestLevel -eq "Windows2016Forest" -and [single](Get-WinVersion) -gt 10.0){ Write-Both " [!] ForestLevel is reduced for backwards compatibility to $ForestLevel!" ; Write-Nessus-Finding "FunctionalLevel" "KB546" "ForestLevel is reduced for backwards compatibility to $ForestLevel" } } Function Get-GPOEnum{#Loops GPOs for some important domain-wide settings $AllowedJoin = @() $HardenNTLM = @() $DenyNTLM = @() $AuditNTLM = @() $NTLMAuthExceptions = @() $EncryptionTypesNotConfigured = $true $AdminLocalLogonAllowed = $true $AdminRPDLogonAllowed = $true $AdminNetworkLogonAllowed = $true $AllGPOs = Get-GPO -All | sort DisplayName foreach($GPO in $AllGPOs){ $GPOreport = Get-GPOReport -Guid $GPO.Id -ReportType Xml #Look for GPO that allows join PC to domain $permissionindex = $GPOreport.IndexOf('SeMachineAccountPrivilege') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeMachineAccountPrivilege'}).Member) ){ $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name GPO -Value $GPO.DisplayName $obj | Add-Member -MemberType NoteProperty -Name SID -Value $member.Sid.'#text' $obj | Add-Member -MemberType NoteProperty -Name Name -Value $member.Name.'#text' $AllowedJoin += $obj } } #Look for GPO that hardens NTLM $permissionindex = $GPOreport.IndexOf('NoLMHash') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport $value = $xmlreport.GPO.Computer.ExtensionData.Extension.SecurityOptions | Where-Object {$_.KeyName -Match 'NoLMHash'} $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name GPO -Value $GPO.DisplayName $obj | Add-Member -MemberType NoteProperty -Name Value -Value "NoLMHash $($value.Display.DisplayBoolean)" $HardenNTLM += $obj } $permissionindex = $GPOreport.IndexOf('LmCompatibilityLevel') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport $value = $xmlreport.GPO.Computer.ExtensionData.Extension.SecurityOptions | Where-Object {$_.KeyName -Match 'LmCompatibilityLevel'} $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name GPO -Value $GPO.DisplayName $obj | Add-Member -MemberType NoteProperty -Name Value -Value "LmCompatibilityLevel $($value.Display.DisplayString)" $HardenNTLM += $obj } #Look for GPO that denies NTLM $permissionindex = $GPOreport.IndexOf('RestrictNTLMInDomain') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport $value = $xmlreport.GPO.Computer.ExtensionData.Extension.SecurityOptions | Where-Object {$_.KeyName -Match 'RestrictNTLMInDomain'} $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name GPO -Value $GPO.DisplayName $obj | Add-Member -MemberType NoteProperty -Name Value -Value "RestrictNTLMInDomain $($value.Display.DisplayString)" $DenyNTLM += $obj } #Look for GPO that audits NTLM $permissionindex = $GPOreport.IndexOf('AuditNTLMInDomain') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport $value = $xmlreport.GPO.Computer.ExtensionData.Extension.SecurityOptions | Where-Object {$_.KeyName -Match 'AuditNTLMInDomain'} $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name GPO -Value $GPO.DisplayName $obj | Add-Member -MemberType NoteProperty -Name Value -Value "AuditNTLMInDomain $($value.Display.DisplayString)" $AuditNTLM += $obj } $permissionindex = $GPOreport.IndexOf('AuditReceivingNTLMTraffic') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport $value = $xmlreport.GPO.Computer.ExtensionData.Extension.SecurityOptions | Where-Object {$_.KeyName -Match 'AuditReceivingNTLMTraffic'} $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name GPO -Value $GPO.DisplayName $obj | Add-Member -MemberType NoteProperty -Name Value -Value "AuditReceivingNTLMTraffic $($value.Display.DisplayString)" $AuditNTLM += $obj } #Look for GPO that allows NTLM exclusions $permissionindex = $GPOreport.IndexOf('DCAllowedNTLMServers') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.SecurityOptions | Where-Object {$_.KeyName -Match 'DCAllowedNTLMServers'}).SettingStrings.Value) ){ $NTLMAuthExceptions += $member } } #Validate Kerberos Encryption algorythm $permissionindex = $GPOreport.IndexOf('MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\Parameters\SupportedEncryptionTypes') if($permissionindex -gt 0){ $EncryptionTypesNotConfigured = $false $xmlreport = [xml]$GPOreport $EncryptionTypes = $xmlreport.GPO.Computer.ExtensionData.Extension.SecurityOptions.Display.DisplayFields.Field if(($EncryptionTypes | Where-Object {$_.Name -eq 'DES_CBC_CRC'} | select -ExpandProperty value) -eq 'true') { Write-Both " [!] GPO [$($GPO.DisplayName)] enabled DES_CBC_CRC for Kerberos!" } elseif(($EncryptionTypes | Where-Object {$_.Name -eq 'DES_CBC_MD5'} | select -ExpandProperty value) -eq 'true') { Write-Both " [!] GPO [$($GPO.DisplayName)] enabled DES_CBC_MD5 for Kerberos!" } elseif(($EncryptionTypes | Where-Object {$_.Name -eq 'RC4_HMAC_MD5'} | select -ExpandProperty value) -eq 'true') { Write-Both " [!] GPO [$($GPO.DisplayName)] enabled RC4_HMAC_MD5 for Kerberos!" } elseif(($EncryptionTypes | Where-Object {$_.Name -eq 'AES128_HMAC_SHA1'} | select -ExpandProperty value) -eq 'false'){ Write-Both " [!] AES128_HMAC_SHA1 not enabled for Kerberos!" } elseif(($EncryptionTypes | Where-Object {$_.Name -eq 'AES256_HMAC_SHA1'} | select -ExpandProperty value) -eq 'false'){ Write-Both " [!] AES256_HMAC_SHA1 not enabled for Kerberos!" } elseif(($EncryptionTypes | Where-Object {$_.Name -eq 'Future encryption types'} | select -ExpandProperty value) -eq 'false'){ Write-Both " [!] Future encryption types not enabled for Kerberos!" } } #Validates Admins local logon restrictions $permissionindex = $GPOreport.IndexOf('SeDenyInteractiveLogonRight') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeDenyInteractiveLogonRight'}).Member)){ if($member.Name.'#text' -match "$SchemaAdmins" -or $member.Name.'#text' -match "$DomainAdmins" -or $member.Name.'#text' -match "$EnterpriseAdmins"){ $AdminLocalLogonAllowed = $false Add-Content -Path "$outputdir\admin_logon_restrictions.txt" -Value "$($GPO.DisplayName) SeDenyInteractiveLogonRight $($member.Name.'#text')" } } } #Validates Admins RDP logon restrictions $permissionindex = $GPOreport.IndexOf('SeDenyRemoteInteractiveLogonRight') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeDenyRemoteInteractiveLogonRight'}).Member)){ if($member.Name.'#text' -match "$SchemaAdmins" -or $member.Name.'#text' -match "$DomainAdmins" -or $member.Name.'#text' -match "$EnterpriseAdmins"){ $AdminRPDLogonAllowed = $false Add-Content -Path "$outputdir\admin_logon_restrictions.txt" -Value "$($GPO.DisplayName) SeDenyRemoteInteractiveLogonRight $($member.Name.'#text')" } } } #Validates Admins network logon restrictions $permissionindex = $GPOreport.IndexOf('SeDenyNetworkLogonRight') if($permissionindex -gt 0){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeDenyNetworkLogonRight'}).Member)){ if($member.Name.'#text' -match "$SchemaAdmins" -or $member.Name.'#text' -match "$DomainAdmins" -or $member.Name.'#text' -match "$EnterpriseAdmins"){ $AdminNetworkLogonAllowed = $false Add-Content -Path "$outputdir\admin_logon_restrictions.txt" -Value "$($GPO.DisplayName) SeDenyNetworkLogonRight $($member.Name.'#text')" } } } } #Output for join PC to domain foreach($record in $AllowedJoin){ Write-Both " [+] GPO [$($record.GPO)] allows [$($record.Name)] to join computers to domain" } #Output for Admins local logon restrictions if($AdminLocalLogonAllowed){ Write-Both " [!] No GPO restricts Domain, Schema and Enterprise local logon across domain!!!" Write-Nessus-Finding "AdminLogon" "KB479" "No GPO restricts Domain, Schema and Enterprise local logon across domain!" } #Output for Admins RDP logon restrictions if($AdminRPDLogonAllowed){ Write-Both " [!] No GPO restricts Domain, Schema and Enterprise RDP logon across domain!!!" Write-Nessus-Finding "AdminLogon" "KB479" "No GPO restricts Domain, Schema and Enterprise RDP logon across domain!" } #Output for Admins network logon restrictions if($AdminNetworkLogonAllowed){ Write-Both " [!] No GPO restricts Domain, Schema and Enterprise network logon across domain!!!" Write-Nessus-Finding "AdminLogon" "KB479" "No GPO restricts Domain, Schema and Enterprise network logon across domain!" } #Output for Validate Kerberos Encryption algorythm if($EncryptionTypesNotConfigured){ Write-Both " [!] RC4_HMAC_MD5 enabled for Kerberos across domain!!!" Write-Nessus-Finding "WeakKerberosEncryption" "KB995" "RC4_HMAC_MD5 enabled for Kerberos across domain!" } #Output for deny NTLM if($DenyNTLM.count -eq 0){ if($HardenNTLM.count -eq 0){ Write-Both " [!] No GPO denies NTLM authentication!" Write-Both " [!] No GPO explicitely restricts LM or NTLMv1!" }else{ Write-Both " [+] NTLM authentication hardening implemented, but NTLM not denied" foreach($record in $HardenNTLM){ Write-Both " [-] $($record.value)" Add-Content -Path "$outputdir\ntlm_restrictions.txt" -Value "NTLM restricted by GPO [$($record.gpo)] with value [$($record.value)]" } } }else{ foreach($record in $DenyNTLM){ Add-Content -Path "$outputdir\ntlm_restrictions.txt" -Value "NTLM restricted by GPO [$($record.gpo)] with value [$($record.value)]" } } #Output for NTLM exceptions if($NTLMAuthExceptions.count -ne 0){ foreach($record in $NTLMAuthExceptions){ Add-Content -Path "$outputdir\ntlm_restrictions.txt" -Value "NTLM auth exceptions $($record)" } } #Output for NTLM audit if($AuditNTLM.count -eq 0){ Write-Both " [!] No GPO enables NTLM audit authentication!" }else{ foreach($record in $DenyNTLM){ Add-Content -Path "$outputdir\ntlm_restrictions.txt" -Value "NTLM audit GPO [$($record.gpo)] with value [$($record.value)]" } } } Function Get-PrivilegedGroupMembership{#List Domain Admins, Enterprise Admins and Schema Admins members $SchemaMembers = Get-ADGroup $SchemaAdmins | Get-ADGroupMember $EnterpriseMembers = Get-ADGroup $EnterpriseAdmins | Get-ADGroupMember $DomainAdminsMembers = Get-ADGroup $DomainAdmins | Get-ADGroupMember if(($SchemaMembers | measure).count -ne 0){ Write-Both " [!] Schema Admins not empty!!!" foreach($member in $SchemaMembers){ Add-Content -Path "$outputdir\schema_admins.txt" -Value "$($member.objectClass) $($member.SamAccountName) $($member.Name)" } } if(($EnterpriseMembers | measure).count -ne 0){ Write-Both " [!] Enterprise Admins not empty!!!" foreach($member in $EnterpriseMembers){ Add-Content -Path "$outputdir\enterprise_admins.txt" -Value "$($member.objectClass) $($member.SamAccountName) $($member.Name)" } } foreach($member in $DomainAdminsMembers){ Add-Content -Path "$outputdir\domain_admins.txt" -Value "$($member.objectClass) $($member.SamAccountName) $($member.Name)" } } Function Get-DCEval{#Basic validation of all DCs in forest #Collect all DCs in forest $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() $ADs = Get-ADDomainController -Filter { Site -like "*" } #Validate OS version of DCs $osList = @() $ADs | ForEach-Object { $osList += $_.OperatingSystem } if(($osList | sort -Unique | measure).Count -eq 1){ Write-Both " [+] All DCs are the same OS version of $($osList | sort -Unique)" }else{ Write-Both " [!] Operating system differs across DCs!!!" if(($ADs | Where-Object {$_.OperatingSystem -Match '2003'}) -ne $null){ Write-Both " [+] Domain controllers with WS 2003" ; $ADs | Where-Object {$_.OperatingSystem -Match '2003'} | ForEach-Object { Write-Both " [-] $($_.Name) has $($_.OperatingSystem)" }} if(($ADs | Where-Object {$_.OperatingSystem -Match '2008 !(R2)'}) -ne $null){ Write-Both " [+] Domain controllers with WS 2008" ; $ADs | Where-Object {$_.OperatingSystem -Match '2008 !(R2)'} | ForEach-Object { Write-Both " [-] $($_.Name) has $($_.OperatingSystem)" }} if(($ADs | Where-Object {$_.OperatingSystem -Match '2008 R2'}) -ne $null){ Write-Both " [+] Domain controllers with WS 2008 R2" ; $ADs | Where-Object {$_.OperatingSystem -Match '2008 R2'} | ForEach-Object { Write-Both " [-] $($_.Name) has $($_.OperatingSystem)" }} if(($ADs | Where-Object {$_.OperatingSystem -Match '2012 !(R2)'}) -ne $null){ Write-Both " [+] Domain controllers with WS 2012" ; $ADs | Where-Object {$_.OperatingSystem -Match '2012 !(R2)'} | ForEach-Object { Write-Both " [-] $($_.Name) has $($_.OperatingSystem)" }} if(($ADs | Where-Object {$_.OperatingSystem -Match '2012 R2'}) -ne $null){ Write-Both " [+] Domain controllers with WS 2012 R2" ; $ADs | Where-Object {$_.OperatingSystem -Match '2012 R2'} | ForEach-Object { Write-Both " [-] $($_.Name) has $($_.OperatingSystem)" }} if(($ADs | Where-Object {$_.OperatingSystem -Match '2016'}) -ne $null){ Write-Both " [+] Domain controllers with WS 2016" ; $ADs | Where-Object {$_.OperatingSystem -Match '2016'} | ForEach-Object { Write-Both " [-] $($_.Name) has $($_.OperatingSystem)" }} if(($ADs | Where-Object {$_.OperatingSystem -Match '2019'}) -ne $null){ Write-Both " [+] Domain controllers with WS 2019" ; $ADs | Where-Object {$_.OperatingSystem -Match '2019'} | ForEach-Object { Write-Both " [-] $($_.Name) has $($_.OperatingSystem)" }} if(($ADs | Where-Object {$_.OperatingSystem -Match '2022'}) -ne $null){ Write-Both " [+] Domain controllers with WS 2022" ; $ADs | Where-Object {$_.OperatingSystem -Match '2022'} | ForEach-Object { Write-Both " [-] $($_.Name) has $($_.OperatingSystem)" }} } #Validate DCs hotfix level if( (( $ADs | Select-Object OperatingSystemHotfix -Unique ) | measure).count -eq 1 -or ( $ADs | Select-Object OperatingSystemHotfix -Unique ) -eq $null ){ Write-Both " [+] All DCs have the same hotfix of [$($ADs | Select-Object OperatingSystemHotFix -Unique | ForEach-Object {$_.OperatingSystemHotfix})]" }else{ Write-Both " [!] Hotfix level differs across DCs!!!" $ADs | ForEach-Object { Write-Both " [-] DC $($_.Name) hotfix [$($_.OperatingSystemHotfix)]" } } #Validate DCs Service Pack level if((($ADs | Select-Object OperatingSystemServicePack -Unique) | measure).count -eq 1 -or ($ADs | Select-Object OperatingSystemServicePack -Unique) -eq $null){ Write-Both " [+] All DCs have the same Service Pack of [$($ADs | Select-Object OperatingSystemServicePack -Unique | ForEach-Object {$_.OperatingSystemServicePack})]" }else{ Write-Both " [!] Service Pack level differs across DCs!!!" $ADs | ForEach-Object { Write-Both " [-] DC $($_.Name) Service Pack [$($_.OperatingSystemServicePack)]" } } #Validate DCs OS Version if((($ADs | Select-Object OperatingSystemVersion -Unique ) | measure).count -eq 1 -or ($ADs | Select-Object OperatingSystemVersion -Unique) -eq $null){ Write-Both " [+] All DCs have the same OS Version of [$($ADs | Select-Object OperatingSystemVersion -Unique | ForEach-Object {$_.OperatingSystemVersion})]" }else{ Write-Both " [!] OS Version differs across DCs!!!" $ADs | ForEach-Object { Write-Both " [-] DC $($_.Name) OS Version [$($_.OperatingSystemVersion)]" } } #List sites without GC $SitesWithNoGC = $false foreach($Site in $Forest.Sites){ if(($ADs | Where-Object {$_.Site -eq $Site.Name} | Where-Object {$_.IsGlobalCatalog -eq $true}) -eq $null){ $SitesWithNoGC = $true Add-Content -Path "$outputdir\sites_no_gc.txt" -Value "$($Site.Name)" } } if($SitesWithNoGC -eq $true){ Write-Both " [!] You have sites with no Global Catalog!" } #Does one DC holds all FSMO if(($ADs | Where-Object {$_.OperationMasterRoles -ne $null} | measure).count -eq 1){ Write-Both " [!] DC $($ADs | Where-Object {$_.OperationMasterRoles -ne $null} | select -ExpandProperty Hostname) holds all FSMO roles!" } #DCs with weak Kerberos algorhythm (*CH* Changed below to look for msDS-SupportedEncryptionTypes to work with 2008R2) $ADcomputers = $ADs | ForEach-Object {Get-ADComputer $_.Name -Properties msDS-SupportedEncryptionTypes} $WeakKerberos = $false foreach($DC in $ADcomputers){#(*CH* Need to define all combinations here, only done 28 and 31 so far) (31 = "DES, RC4, AES128, AES256", 28 = "RC4, AES128, AES256") if( $DC."msDS-SupportedEncryptionTypes" -eq 28 -or $DC."msDS-SupportedEncryptionTypes" -eq 31 ){ $WeakKerberos = $true Add-Content -Path "$outputdir\dcs_weak_kerberos_ciphersuite.txt" -Value "$($DC.DNSHostName) $($dc."msDS-SupportedEncryptionTypes")" } } Write-Both " [!] You have DCs with RC4 or DES allowed for Kerberos!!!" #Check where newly joined computers go $newComputers = (Get-ADDomain).ComputersContainer $newUsers = (Get-ADDomain).UsersContainer Write-Both " [+] New joined computers are stored in $newComputers" Write-Both " [+] New users are stored in $newUsers" } Function Get-DefaultDomainControllersPolicy{#Enumerates Default Domain Controllers Policy for default unsecure and excessive options $ExcessiveDCInteractiveLogon = $false $ExcessiveDCBackupPermissions = $false $ExcessiveDCRestorePermissions = $false $ExcessiveDCDriverPermissions = $false $ExcessiveDCLocalShutdownPermissions = $false $ExcessiveDCRemoteShutdownPermissions = $false $ExcessiveDCTimePermissions = $false $ExcessiveDCBatchLogonPermissions = $false $ExcessiveDCRDPLogonPermissions = $false $GPO = Get-GPO 'Default Domain Controllers Policy' $GPOreport = Get-GPOReport -Guid $GPO.Id -ReportType Xml #Interactive local logon $permissionindex = $GPOreport.IndexOf('SeInteractiveLogonRight') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeInteractiveLogonRight'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators' -and $member.Name.'#text' -ne "$EntrepriseDomainControllers"){ $ExcessiveDCInteractiveLogon = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeInteractiveLogonRight $($member.Name.'#text')" } } } #Batch logon $permissionindex = $GPOreport.IndexOf('SeBatchLogonRight') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeBatchLogonRight'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators'){ $ExcessiveDCBatchLogonPermissions = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeBatchLogonRight $($member.Name.'#text')" } } } #RDP logon $permissionindex = $GPOreport.IndexOf('SeInteractiveLogonRight') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeInteractiveLogonRight'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators' -and $member.Name.'#text' -ne "$EntrepriseDomainControllers"){ $ExcessiveDCRDPLogonPermissions = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeInteractiveLogonRight $($member.Name.'#text')" } } } #Backup $permissionindex = $GPOreport.IndexOf('SeBackupPrivilege') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeBackupPrivilege'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators'){ $ExcessiveDCBackupPermissions = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeBackupPrivilege $($member.Name.'#text')" } } } #Restore $permissionindex = $GPOreport.IndexOf('SeRestorePrivilege') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeRestorePrivilege'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators'){ $ExcessiveDCRestorePermissions = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeRestorePrivilege $($member.Name.'#text')" } } } #Load driver $permissionindex = $GPOreport.IndexOf('SeLoadDriverPrivilege') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeLoadDriverPrivilege'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators'){ $ExcessiveDCDriverPermissions = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeLoadDriverPrivilege $($member.Name.'#text')" } } } #Local shutdown $permissionindex = $GPOreport.IndexOf('SeShutdownPrivilege') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeShutdownPrivilege'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators'){ $ExcessiveDCLocalShutdownPermissions = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeShutdownPrivilege $($member.Name.'#text')" } } } #Remote shutdown $permissionindex = $GPOreport.IndexOf('SeRemoteShutdownPrivilege') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeRemoteShutdownPrivilege'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators'){ $ExcessiveDCRemoteShutdownPermissions = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeRemoteShutdownPrivilege $($member.Name.'#text')" } } } #Change time $permissionindex = $GPOreport.IndexOf('SeSystemTimePrivilege') if($permissionindex -gt 0 -and $GPO.DisplayName -eq 'Default Domain Controllers Policy'){ $xmlreport = [xml]$GPOreport foreach($member in (($xmlreport.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object {$_.Name -eq 'SeSystemTimePrivilege'}).Member)){ if($member.Name.'#text' -ne 'BUILTIN\$Administrators' -and $member.Name.'#text' -ne "$LocalService"){ $ExcessiveDCTimePermissions = $true Add-Content -Path "$outputdir\default_domain_controller_policy_audit.txt" -Value "SeSystemTimePrivilege $($member.Name.'#text')" } } } #Output for Default Domain Controllers Policy if($ExcessiveDCInteractiveLogon -or $ExcessiveDCBackupPermissions -or $ExcessiveDCRestorePermissions -or $ExcessiveDCDriverPermissions -or $ExcessiveDCLocalShutdownPermissions -or $ExcessiveDCRemoteShutdownPermissions -or $ExcessiveDCTimePermissions -or $ExcessiveDCBatchLogonPermissions -or $ExcessiveDCRDPLogonPermissions){ Write-Both " [!] Excessive permissions in Default Domain Controllers Policy detected!" } } Function Get-RecentChanges(){#Retrieve users and groups that have been created during last 30 days $DateCutOff = ((Get-Date).AddDays(-30)).Date $newUsers = Get-ADUser -Filter {whenCreated -ge $DateCutOff} -Properties whenCreated | select whenCreated,SamAccountName $newGroups = Get-ADGroup -Filter {whenCreated -ge $DateCutOff} -Properties whenCreated | select whenCreated,SamAccountName $countUsers = 0 $countGroups = 0 $progresscountUsers = 0 $progresscountGroups = 0 $totalcountUsers = ($newUsers | Measure-Object | Select-Object Count).count $totalcountGroups = ($newGroups | Measure-Object | Select-Object Count).count if($totalcountUsers -gt 0){ foreach($newUser in $newUsers ){Add-Content -Path "$outputdir\new_users.txt" -Value "Account $($newUser.SamAccountName) was created $($newUser.whenCreated)"} Write-Both " [!] $totalcountUsers new users were created last 30 days, see $outputdir\new_users.txt" } if($totalcountGroups -gt 0){ foreach($newGroup in $newGroups ){Add-Content -Path "$outputdir\new_groups.txt" -Value "Group $($newGroup.SamAccountName) was created $($newGroup.whenCreated)"} Write-Both " [!] $totalcountGroups new groups were created last 30 days, see $outputdir\new_groups.txt" } } Function Get-ReplicationType{#Retrieve replication mechanism (FRS or DFSR) $objectName = "DFSR-GlobalSettings" $searcher = [ADSISearcher] "(objectClass=msDFSR-GlobalSettings)" $objectExists = $searcher.FindOne() -ne $null if($objectExists){ $DFSRFlags=(Get-ADObject -Identity "CN=DFSR-GlobalSettings,$((Get-ADDomain).systemscontainer)" -Properties msDFSR-Flags).'msDFSR-Flags' switch($DFSRFlags){ 0 { Write-Both " [!] Migration from FRS to DFSR is not finished. Current state: started!" } 16 { Write-Both " [!] Migration from FRS to DFSR is not finished. Current state: prepared!" } 32 { Write-Both " [!] Migration from FRS to DFSR is not finished. Current state: redirected!" } 48 { Write-Both " [+] DFSR mechanism is used to replicate across domain controllers." } } }else{ Write-Both " [!] FRS mechanism is still used to replicate across domain controllers, you should migrate to DFSR!" } } Function Get-RecycleBinState {#Check if recycle bin is enabled if((Get-ADOptionalFeature -Filter 'Name -eq "Recycle Bin Feature"').EnabledScopes){ Write-Both " [+] Recycle Bin is enabled in the domain" }else{ Write-Both " [!] Recycle Bin is disabled in the domain, you should consider enabling it!" } } Function Get-CriticalServicesStatus{#Check AD services status Write-Both " [+] Checking services on all DCs" $dcList = @() (Get-ADDomainController -Filter *) | ForEach-Object{$dcList += $_.Name} $objectName = "DFSR-GlobalSettings" $searcher = [ADSISearcher] "(objectClass=msDFSR-GlobalSettings)" $objectExists = $searcher.FindOne() -ne $null if($objectExists){ $services = @("dns","netlogon","kdc","w32time","ntds","dfsr") }else{ $services = @("dns","netlogon","kdc","w32time","ntds","ntfrs") } foreach($DC in $dcList){ foreach($service in $services){ $checkService = Get-Service $service -ComputerName $DC -ErrorAction SilentlyContinue $serviceName = $checkService.Name $serviceStatus = $checkService.Status if(!($serviceStatus)){ Write-Both " [!] Service $($service) cannot be checked on $DC!" } elseif($serviceStatus -ne "Running"){ Write-Both " [!] Service $($service) is not running on $DC!" } } } } Function Get-LastWUDate{#Check Windows update status and last install date $dcList = @() (Get-ADDomainController -Filter *) | ForEach-Object{$dcList+=$_.Name} $lastMonth = (Get-Date).AddDays(-30) Write-Both " [+] Checking Windows Update" foreach($DC in $dcList){ $startMode = (Get-WmiObject -ComputerName $DC -Class Win32_Service -Property StartMode -Filter "Name='wuauserv'" -ErrorAction SilentlyContinue).StartMode if(!($startMode)){ Write-Both " [!] Windows Update service cannot be checked on $DC!" } elseif($startMode -eq "Disabled"){ Write-Both " [!] Windows Update service is disabled on $DC!" } } $progresscount = 0 $totalcount = ($dcList | Measure-Object | Select-Object Count).count foreach($DC in $dcList){ if($totalcount -eq 0){ break } Write-Progress -Activity "Searching for last Windows Update installation on all DCs..." -Status "Currently searching on $DC" -PercentComplete ($progresscount / $totalcount*100) try{ $lastHotfix = (Get-HotFix -ComputerName $DC | Where-Object {$_.InstalledOn -ne $null} | Sort-Object -Descending InstalledOn | Select-Object -First 1).InstalledOn if($lastHotfix -lt $lastMonth){ Write-Both " [!] Windows is not up to date on $DC, last install: $($lastHotfix)" }else{ Write-Both " [+] Windows is up to date on $DC, last install: $($lastHotfix)" } } catch{ Write-Both " [!] Cannot check last update date on $DC" } $progresscount++ } Write-Progress -Activity "Searching for last Windows Update installation on all DCs..." -Status "Ready" -Completed } Function Get-TimeSource {#Get NTP sync source $dcList = @() (Get-ADDomainController -Filter *) | ForEach-Object{$dcList += $_.Name} Write-Both " [+] Checking NTP configuration" foreach($DC in $dcList){ $ntpSource = w32tm /query /source /computer:$DC if($ntpSource -like '*0x800706BA*'){ Write-Both " [+] Cannot get time source for $DC" }else{ Write-Both " [+] $DC is syncing time from $ntpSource" } } } Function Get-RODC{#Check for RODC Write-Both " [+] Checking for Read Only DCs" $ADs = Get-ADDomainController -Filter { Site -like "*" } $ADs | ForEach-Object{ if($_.IsReadOnly){ Write-Both " [+] DC $($_.Name) is a RODC server!" } } } Function Install-Dependencies{#Install DSInternals if($PSVersionTable.PSVersion.Major -ge 5){ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $count = 0 $totalcount = 3 Write-Progress -Activity "Installing dependencies..." -Status "Currently installing NuGet Package Provider" -PercentComplete ($count / $totalcount*100) if(!(Get-PackageProvider -ListAvailable -Name Nuget -ErrorAction SilentlyContinue)){ Install-PackageProvider -Name NuGet -Force | Out-Null } $count++ Write-Progress -Activity "Installing dependencies..." -Status "Currently adding PSGallery to trusted Repositories" -PercentComplete ($count / $totalcount*100) if((Get-PSRepository -Name PSGallery).InstallationPolicy -eq "Untrusted"){ Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted } $count++ Write-Progress -Activity "Installing dependencies..." -Status "Currently installing module DSInternals" -PercentComplete ($count / $totalcount*100) if(!(Get-Module -ListAvailable -Name DSInternals)){ Install-Module -Name DSInternals -Force } Write-Progress -Activity "Installing dependencies..." -Status "Ready" -Completed Import-Module DSInternals }else{ Write-Both " [!] PowerShell 5 or greater is needed, see https://www.microsoft.com/en-us/download/details.aspx?id=54616" } } Function Remove-StringLatinCharacters{#Removes latin characters PARAM ([string]$String) [Text.Encoding]::ASCII.GetString([Text.Encoding]::GetEncoding("Cyrillic").GetBytes($String)) } Function Get-PasswordQuality{#Use DSInternals to evaluate password quality if(Get-Module -ListAvailable -Name DSInternals){ $totalSite = (Get-ADObject -Filter {objectClass -like "site" } -SearchBase (Get-ADRootDSE).ConfigurationNamingContext | measure).Count $count = 0 Get-ADObject -Filter {objectClass -like "site" } -SearchBase (Get-ADRootDSE).ConfigurationNamingContext | ForEach-Object{ if($_.Name -eq $(Remove-StringLatinCharacters $_.Name)){ $count++ } } if($count -ne $totalSite){ Write-Both " [!] One or more site have illegal characters in their name, can't get password quality!" }else{ Get-ADReplAccount -All -Server $env:ComputerName -NamingContext $(Get-ADDomain | select -ExpandProperty DistinguishedName) | Test-PasswordQuality -IncludeDisabledAccounts | Out-File "$outputdir\password_quality.txt" Write-Both " [!] Password quality test done, see $outputdir\password_quality.txt" } } } Function Check-Shares {#Check SYSVOL and NETLOGON share exists $dcList = @() (Get-ADDomainController -Filter *) | ForEach-Object{$dcList += $_.Name} Write-Both " [+] Checking SYSVOL and NETLOGON shares on all DCs" foreach($DC in $dcList){ $shareList = (Get-WmiObject -Class Win32_Share -ComputerName $DC -ErrorAction SilentlyContinue) if(!($shareList)){ Write-Both " [!] Cannot test shares on $DC!" }else{ $sysvolShare = ($shareList | ?{$_ -match 'SYSVOL'} | measure).Count $netlogonShare = ($shareList | ?{$_ -match 'NETLOGON'} | measure).Count if($sysvolShare -eq 0){ Write-Both " [!] SYSVOL share is missing on $DC!" } if($netlogonShare -eq 0){ Write-Both " [!] NETLOGON share is missing on $DC!" } } } } $outputdir = (Get-Item -Path ".\").FullName + "\" + $env:computername $starttime = Get-Date $scriptname = $MyInvocation.MyCommand.Name if(!(Test-Path "$outputdir")){ New-Item -ItemType Directory -Path $outputdir | Out-Null } Write-Both " _____ ____ _____ _ _ _ | _ | \ | _ |_ _ _| |_| |_ | | | | | | | | . | | _| |__|__|____/ |__|__|___|___|_|_| $versionnum by phillips321 " $running=$false Write-Both "[*] Script start time $starttime" if(Get-Module -ListAvailable -Name ActiveDirectory){ Import-Module ActiveDirectory }else{ Write-Both "[!] ActiveDirectory module not installed, exiting..." ; exit } if(Get-Module -ListAvailable -Name ServerManager) { Import-Module ServerManager }else{ Write-Both "[!] ServerManager module not installed, exiting..." ; exit } if(Get-Module -ListAvailable -Name GroupPolicy) { Import-Module GroupPolicy }else{ Write-Both "[!] GroupPolicy module not installed, exiting..." ; exit } if(Get-Module -ListAvailable -Name DSInternals) { Import-Module DSInternals }else{ Write-Both "[!] DSInternals module not installed, use -installdeps to force install" } if(Test-Path "$outputdir\adaudit.nessus"){ Remove-Item -recurse "$outputdir\adaudit.nessus" | Out-Null } Write-Nessus-Header Write-Host "[+] Outputting to $outputdir" Write-Both "[*] Lang specific variables" Get-Variables if($installdeps) { $running=$true ; Write-Both "[*] Installing optionnal features" ; Install-Dependencies } if($hostdetails -or $all) { $running=$true ; Write-Both "[*] Device Information" ; Get-HostDetails } if($domainaudit -or $all) { $running=$true ; Write-Both "[*] Domain Audit" ; Get-LastWUDate ; Get-DCEval ; Get-TimeSource ; Get-PrivilegedGroupMembership ; Get-MachineAccountQuota; Get-DefaultDomainControllersPolicy ; Get-SMB1Support ; Get-FunctionalLevel ; Get-DCsNotOwnedByDA ; Get-ReplicationType ; Check-Shares ; Get-RecycleBinState ; Get-CriticalServicesStatus ; Get-RODC } if($trusts -or $all) { $running=$true ; Write-Both "[*] Domain Trust Audit" ; Get-DomainTrusts } if($accounts -or $all) { $running=$true ; Write-Both "[*] Accounts Audit" ; Get-InactiveAccounts ; Get-DisabledAccounts ; Get-LockedAccounts ; Get-AdminAccountChecks ; Get-NULLSessions ; Get-PrivilegedGroupAccounts ; Get-ProtectedUsers } if($passwordpolicy -or $all) { $running=$true ; Write-Both "[*] Password Information Audit" ; Get-AccountPassDontExpire ; Get-UserPasswordNotChangedRecently ; Get-PasswordPolicy ; Get-PasswordQuality } if($ntds -or $all) { $running=$true ; Write-Both "[*] Trying to save NTDS.dit, please wait..." ; Get-NTDSdit } if($oldboxes -or $all) { $running=$true ; Write-Both "[*] Computer Objects Audit" ; Get-OldBoxes } if($gpo -or $all) { $running=$true ; Write-Both "[*] GPO audit (and checking SYSVOL for passwords)" ; Get-GPOtoFile ; Get-GPOsPerOU ; Get-SYSVOLXMLS; Get-GPOEnum } if($ouperms -or $all) { $running=$true ; Write-Both "[*] Check Generic Group AD Permissions" ; Get-OUPerms } if($laps -or $all) { $running=$true ; Write-Both "[*] Check For Existence of LAPS in domain" ; Get-LAPSStatus } if($authpolsilos -or $all) { $running=$true ; Write-Both "[*] Check For Existence of Authentication Polices and Silos" ; Get-AuthenticationPoliciesAndSilos } if($insecurednszone -or $all){ $running=$true ; Write-Both "[*] Check For Existence DNS Zones allowing insecure updates" ; Get-DNSZoneInsecure } if($recentchanges -or $all) { $running=$true ; Write-Both "[*] Check For newly created users and groups" ; Get-RecentChanges } if(!$running){ Write-Both "[!] No arguments selected" Write-Both "[!] Other options are as follows, they can be used in combination" Write-Both " -installdeps installs optionnal features (DSInternals)" Write-Both " -hostdetails retrieves hostname and other useful audit info" Write-Both " -domainaudit retrieves information about the AD such as functional level" Write-Both " -trusts retrieves information about any doman trusts" Write-Both " -accounts identifies account issues such as expired, disabled, etc..." Write-Both " -passwordpolicy retrieves password policy information" Write-Both " -ntds dumps the NTDS.dit file using ntdsutil" Write-Both " -oldboxes identifies outdated OSs like 2000/2003/XP/Vista/7/2008 joined to the domain" Write-Both " -gpo dumps the GPOs in XML and HTML for later analysis" Write-Both " -ouperms checks generic OU permission issues" Write-Both " -laps checks if LAPS is installed" Write-Both " -authpolsilos checks for existence of authentication policies and silos" Write-Both " -insecurednszone checks for insecure DNS zones" Write-Both " -recentchanges checks for newly created users and groups (last 30 days)" Write-Both " -all runs all checks, e.g. $scriptname -all" } Write-Nessus-Footer #Dirty fix for .nessus characters (will do this properly or as a function later. Will need more characters adding here...) $originalnessusoutput = Get-Content $outputdir\adaudit.nessus $nessusoutput = $originalnessusoutput -Replace "&", "&" $nessusoutput = $nessusoutput -Replace "`“", """ $nessusoutput = $nessusoutput -Replace "`'", "'" $nessusoutput = $nessusoutput -Replace "ü", "u" $nessusoutput | Out-File $outputdir\adaudit-replaced.nessus $endtime = Get-Date Write-Both "[*] Script end time $endtime"