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