BlueTuxedo.psm1
function Get-BTADIZone { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $Zones = @() $ZoneList = @() $Root = Get-ADRootDSE $RootNC = $Root.rootDomainNamingContext $Zones = Get-ADObject -Filter { objectClass -eq 'dNSZone' } -SearchBase "CN=MicrosoftDNS,DC=ForestDnsZones,$RootNC" foreach ($zone in $Zones) { $AddToList = [PSCustomObject]@{ 'Domain' = (Get-ADForest).RootDomain 'Zone Name' = $zone.name 'Zone Type' = 'Forest-replicated' 'Is Reverse?' = ($zone.name -match '\.in-addr\.arpa$') 'Dynamic Update' = $zone.DynamicUpdate } $ZoneList += $AddToList } foreach ($domain in $Domains) { $Zones = @() $domainDN = (Get-ADDomain -Identity $domain).distinguishedName foreach ($context in @('CN=System', 'DC=DomainDnsZones') ) { $Zones = Get-ADObject -Filter { objectClass -eq 'dNSZone' } -SearchBase "CN=MicrosoftDNS,$context,$domainDN" -Server $domain if ($context -eq 'CN=System') { $ZoneType = 'Legacy' } else { $ZoneType = 'Domain-replicated' } foreach ($zone in $Zones) { $AddToList = [PSCustomObject]@{ 'Domain' = $domain 'Zone Name' = $zone.name 'Zone Type' = $ZoneType 'Is Reverse?' = ($zone.name -match '\.in-addr\.arpa$') 'Dynamic Update' = $zone.DynamicUpdate } $ZoneList += $AddToList } } } $ZoneList } function Get-BTConditionalForwarder { [CmdletBinding()] param ( [Parameter()] [array]$Domains, # Name of the DNS server[s] to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($null -eq $script:DNSServers) { $script:DNSServers = Get-BTDnsServer -Domains $Domains -Exclude $Exclude } $ZoneList = @() foreach ($dnsServer in $script:DNSServers) { # Enumerate the zones on each DNS server $Zones = Get-DnsServerZone -ComputerName $dnsServer.IPAddress | Where-Object { ( $_.IsAutoCreated -eq $false ) -and ( $_.ZoneType -eq 'Forwarder' ) -and ( $_.IsDsIntegrated -eq $true ) } # Loop through each zone on the server foreach ($zone in $Zones) { $AddToList = [PSCustomObject]@{ 'Domain' = $dnsServer.Domain 'Zone Name' = $zone.ZoneName } # Add the info to the ZoneList array $ZoneList += $AddToList } } if ($ZoneList.Count -lt 1) { Write-Host 'No conditional forward lookup zones were found.' } # Return the ZoneList object $ZoneList } function Get-BTDanglingSPN { <# .SYNOPSIS Get dangling SPNs from Active Directory. .DESCRIPTION Get dangling SPNs from all domains in an Active Directory forest. A dangling SPN is a SPN that references an unresolved hostname. .PARAMETER Domains The domain (or domains) to check for dangling SPNs. These can be entered as 'domain.com' or "@('domain1.com','domain2.com')". .EXAMPLE Get-BTDanglingSPN Get dangling SPNs in all domains in the current forest. .EXAMPLE $DanglingSPNs = Get-BTDanglingSPN -Domains 'domain.com' | Group-Object PrincipalIdentityReference Get dangling SPNs in domain.com and group them by the principal they are attached to. .EXAMPLE Get-BTDanglingSPN -Domains @('domain1.com','domain2.com','domain3.com') Get danging SPNs in domain1.com, domain2.com, and domain3.com. .NOTES General notes #> [CmdletBinding()] param ( [Parameter()] [array]$Domains ) begin { # If no domains were specified, get all domains in the current forest. if ($null -eq $Domains) { $Domains = Get-BTTarget } # Define a RegEx for valid FQDNs. $RegexHostname = '^(?=^.{1,254}$)(^((?!-)[a-zA-Z0-9_-]{1,63}(?<!-)\.)+[a-zA-Z]{2,})$' # Initialize the hash tables (does making it ordered help performance?) $DanglingSPNList = [hashtable]@{} $DNSRecords = [ordered] @{} } process { # Cache all DNS records from all domains to make lookups faster. Will only need Resolve-DnsName for SPNs that refer to public names. # Keep this outside the other domain loop so all DNS records will be available for the entire script. Write-Host "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Getting DNS records from domains: $($Domains -join ', ')" -ForegroundColor White -BackgroundColor Black foreach ($domain in $Domains) { $DomainDNSRecords = Get-DnsServerResourceRecord -ComputerName $domain -ZoneName $domain -ErrorAction SilentlyContinue foreach ($record in $DomainDNSRecords) { $DNSRecords[$record.HostName] = $record.RecordData } } foreach ($domain in $Domains) { # Get all objects with SPNs. Write-Host "`n[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] [$domain] Getting AD objects with SPNs." -ForegroundColor White -BackgroundColor Black $PrincipalWithSPN = Get-ADObject -Filter { ServicePrincipalName -ne "$null" -and ServicePrincipalName -ne 'kadmin/changepw' } -Properties * -Server $domain $PrincipalCount = $PrincipalWithSPN.Count Write-Host "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] [$domain] Found $PrincipalCount AD objects with SPNs." -ForegroundColor White -BackgroundColor Black # Loop through each security principal that has a SPN. $PrincipalProgress = 0 foreach ($principal in $PrincipalWithSPN) { ++$PrincipalProgress Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] [$domain] [$PrincipalProgress`/$PrincipalCount] [$($principal.CanonicalName)]" # Check each SPN to see if its hostname matches the principal's hostname. $CheckSPN = $false foreach ($spn in ($principal.serviceprincipalname)) { # Remove the service name, the forward slash, and the port from the SPN to get its hostname. $SPNHostname = ($spn).Split('/')[1].Split(':')[0] $PrincipalHostname = $principal.DnsHostName if ($SPNHostname -eq $PrincipalHostname) { # If FQDNs match, ignore and $CheckSPN stays $false Write-Verbose "$spn`n FQDN Match: `'$PrincipalHostname`' = `'$SPNHostname`'. [CheckSPN = $CheckSPN]" continue } elseif ("${SPNHostname}.${domain}" -eq $PrincipalHostname ) { # Construct FQDN from SPNHostname + Domain and check for an FQDN match with PrincipalHostname. Write-Verbose "`n Short Name Match: `'$PrincipalHostname`' = `'${SPNHostname}.${domain}`'. [CheckSPN = $CheckSPN]" continue } elseif ($SPNHostname -match '[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?\._msdcs\.') { # Do not inspect domain controller SPNs as long as they are in the DC OU. ## NEED TO ADD EXTRA VALIDATION ## Write-Verbose "`n Domain controller GUID. [CheckSPN = $CheckSPN]" continue } else { # Flag the SPN for inspection if the ServicePrincipal hostname does not match any of the above conditions. $CheckSPN = $true $DnsResourceRecordExist = $false Write-Host "`n[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] [$domain] [$PrincipalProgress`/$PrincipalCount] Inspecting: " -NoNewline -ForegroundColor White -BackgroundColor Black Write-Host "$spn" -NoNewline -ForegroundColor Cyan -BackgroundColor Black Write-Host " > " -NoNewline -ForegroundColor White -BackgroundColor Black # Try to find the hostname in internal DNS zones. if ($DNSRecords[$SPNHostname] -or $DNSRecords["${SPNHostname}.${domain}"]) { # Check the cached internal DNS records for the hostname. $DnsResourceRecordExist = $true Write-Host "[PASS] DNS record found." -ForegroundColor Green -BackgroundColor Black continue } # Check for FQDNs not found in the internal $Domains list. if ( ($SPNHostname -match $RegexHostname) -and -not (($domains | ForEach-Object { $SPNHostname.Contains($_) }) -contains $true) ) { # Try to resolve the external hostname. if (Resolve-DnsName -Name $SPNHostname -ErrorAction SilentlyContinue) { $DnsResourceRecordExist = $true Write-Host "[PASS] External DNS record found." -ForegroundColor Green -BackgroundColor Black } else { # Might need more error handling, but basically the name didn't resolve and it is a dangling SPN. Write-Host "[FAIL] DNS record not found found for $SPNHostname." -ForegroundColor Red -BackgroundColor Black $DnsResourceRecordExist = $false } } # If a DNS record was not found, this is a dangling SPN. if ( -not $DnsResourceRecordExist ) { Write-Host "[FAIL] DNS record not found found for $SPNHostname." -ForegroundColor Red -BackgroundColor Black $DanglingSPN = [PSCustomObject]@{ 'Identity Reference' = ConvertTo-IdentityReference -SID $principal.objectSID 'Dangling SPN' = $spn 'PrincipalDistinguishedName' = $principal.distinguishedName } # Avoid adding duplicates to the list (construct a unique key from the CN + SPN). if ( -not $DanglingSPNList[ "$($principal.CanonicalName)`:$spn" ] ) { $DanglingSPNList.Add( "$($principal.CanonicalName)`:$spn", $DanglingSPN ) } } } # end if/else hostname checks } # end foreach SPN } # end foreach principal Write-Host "Finished analzying $($PrincipalWithSPN.Count) principles with SPNs in $domain." -ForegroundColor Cyan -BackgroundColor Black } # end foreach domain } # end process block end { # Return the results as an array (should I leave it as a hash table?). # Use optional parameters to write this to host, logfile, or clipboard. [array]$DanglingSPNList.Values } # end end block } # end function function Get-BTDnsAdminsMembership { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $ForestDnsAdminsMembership = @() foreach ($domain in $Domains) { $domainDnsAdminsMembership = Get-ADGroupMember 'DnsAdmins' -Recursive -Server $domain # TODO Capture nested members with non-standard PGID foreach ($member in $domainDnsAdminsMembership) { $principal = [PSCustomObject]@{ 'Group Domain' = $domain 'Member Name' = $member.Name 'Member Distinguished Name' = $member.distinguishedName } if ($ForestDnsAdminsMembership.distinguishedName -notcontains $member.distinguishedName) { $ForestDnsAdminsMembership += $principal } } } $ForestDnsAdminsMembership } function Get-BTDnsServer { [CmdletBinding()] param ( # Domains to inspect [Parameter()] [array]$Domains, # Name server to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($PSBoundParameters.Keys.Contains('Exclude')) { # Exclude certain name server[s] [string[]]$ExcludeList = @() foreach ($item in $Exclude) { foreach ($domain in $Domains) { # Normalize the server name to an FQDN if ($item -match "$($domain)$") { $ExcludeList += $item } else { $ExcludeList += "$item.${domain}" } # This could more precisely get the proper FQDN but works for now } } Write-Verbose "Excluding: $($ExcludeList -join ',')" } $DnsServerList = @() # Loop through each domain foreach ($domain in $Domains) { # Find and loop through each DNS server $DnsServers = @(Resolve-DnsName -Type NS -Name $domain | Where-Object { $ExcludeList -notin $_.Name } | Where-Object { $_.QueryType -eq 'A' } | Sort-Object Name) $DnsServers | ForEach-Object { $_ | Add-Member -NotePropertyName Domain -NotePropertyValue $domain -Force } $DnsServerList += $DnsServers } Write-Verbose "Found $($DNSServers.Count) DNS servers in $($Domains.Count) domains." $DnsServerList } function Get-BTDnsUpdateProxyMembership { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $ForestDnsUpdateProxyMembership = @() foreach ($domain in $Domains) { $domainDnsUpdateProxyMembership = Get-ADGroupMember 'DnsUpdateProxy' -Recursive -Server $domain # TODO Capture nested members with non-standard PGID foreach ($member in $domainDnsUpdateProxyMembership) { $principal = [PSCustomObject]@{ 'Group Domain' = $domain 'Member Name' = $member.Name 'Member Distinguished Name' = $member.distinguishedName } if ($ForestDnsUpdateProxyMembership.distinguishedName -notcontains $member.distinguishedName) { $ForestDnsUpdateProxyMembership += $principal } } } $ForestDnsUpdateProxyMembership } function Get-BTDynamicUpdateServiceAccount { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $DynamicUpdateServiceAccountList = @() $DHCPServers = Get-DhcpServerInDC foreach ($dhcpserver in $DHCPServers) { $DynamicUpdateServiceAccounts = try { Get-DhcpServerDnsCredential -ComputerName $dhcpserver.IPAddress } catch { [PSCustomObject]@{ UserName = 'Not Configured' DomainName = 'N/A' } } if ($DynamicUpdateServiceAccountList.'Server IP' -notcontains $dhcpserver.IPAddress) { foreach ($account in $DynamicUpdateServiceAccounts) { $AddToList = [PSCustomObject]@{ 'Server Name' = $dhcpserver.dnsName 'Server IP' = $dhcpserver.IPAddress 'Service Account Name' = $account.UserName 'Service Account Domain' = $account.DomainName } $DynamicUpdateServiceAccountList += $AddToList } } } $DynamicUpdateServiceAccountList } function Get-BTForwarderConfiguration { [CmdletBinding()] param ( [Parameter()] [array]$Domains, # Name of the DNS server[s] to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($null -eq $script:DNSServers) { $script:DNSServers = Get-BTDnsServer -Domains $Domains -Exclude $Exclude } $ForwarderList = @() foreach ($dnsServer in $script:DNSServers) { # Enumerate the forwarders on each DNS server [array]$Forwarders = Get-DnsServerForwarder -ComputerName $dnsServer.IPAddress # Add to the list if this DNS server's IP address is not already in the list. if ($ForwarderList.'Server IP' -notcontains $dnsServer.IPAddress) { foreach ($forwarder in $Forwarders) { $AddToList = [PSCustomObject]@{ 'Server Name' = $dnsServer.Name 'Server IP' = $dnsServer.IPAddress 'Forwarders' = $forwarder.IPAddress.IPAddressToString } } $ForwarderList += $AddToList } } if ($ForwarderList.Count -lt 1) { Write-Host 'No forwarders were found.' } # Return the ForwarderList object $ForwarderList } function Get-BTGlobalQueryBlockList { [CmdletBinding()] param ( [Parameter()] [array]$Domains, # Name of the DNS server[s] to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($null -eq $script:DNSServers) { $script:DNSServers = Get-BTDnsServer -Domains $Domains -Exclude $Exclude } $GlobalQueryBlockListList = @() foreach ($dnsServer in $script:DNSServers) { # Enumerate the global query blocklists on each DNS server [array]$ServerGQBLs = Get-DnsServerGlobalQueryBlockList -ComputerName $dnsServer.IPAddress foreach ($gqbl in $ServerGQBLs) { # Add it to the list with server information $AddToList = [PSCustomObject]@{ 'Server Name' = $($dnsServer.Name) 'Server IP' = $($dnsServer.IPAddress) 'Enabled?' = $($gqbl.Enable) 'GQBL' = $($gqbl.List) } $GlobalQueryBlockListList += $AddToList } } if ($GlobalQueryBlockListList.Count -lt 1) { Write-Host 'No global query blocklists were found.' } # Return the GlobalQueryBlockList object $GlobalQueryBlockListList } function Get-BTNameProtectionConfiguration { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $NameProtectionConfigurationList = @() foreach ($dhcpServer in $DHCPServers) { $NameProtectionv4Configuration = (Get-DhcpServerv4DnsSetting -ComputerName $dhcpServer.IPAddress).NameProtection $NameProtectionv6Configuration = (Get-DhcpServerv6DnsSetting -ComputerName $dhcpServer.IPAddress).NameProtection if ($NameProtectionConfigurationList.'Server IP' -notcontains $dhcpServer.IPAddress) { $AddToList = [PSCustomObject]@{ 'Server Name' = $dhcpServer.DnsName 'Server IP' = $dhcpServer.IPAddress 'IPv4 Name Protection' = $NameProtectionv4Configuration 'IPv6 Name Protection' = $NameProtectionv6Configuration } } $NameProtectionConfigurationList += $AddToList } $NameProtectionConfigurationList } function Get-BTNameServer { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $NameServerList = @() foreach ($domain in $Domains) { $DNSServers = Resolve-DnsName -Type NS -Name $domain | Where-Object QueryType -EQ 'A' foreach ($dnsServer in $DNSServers) { if ($NameServerList.'Server IP' -notcontains $dnsServer.IP4Address) { $AddToList = [PSCustomObject]@{ 'Server Name' = $dnsServer.Name 'Server IP' = $dnsServer.IP4Address } } $NameServerList += $AddToList } } $NameServerList } function Get-BTNonADIZone { [CmdletBinding()] param ( [Parameter()] [array]$Domains, # Name of the DNS server[s] to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($null -eq $script:DNSServers) { $script:DNSServers = Get-BTDnsServer -Domains $Domains -Exclude $Exclude } $ZoneList = @() foreach ($dnsServer in $script:DNSServers) { # Enumerate the zones on each DNS server. $Zones = Get-DnsServerZone -ComputerName $dnsServer.IPAddress | Where-Object { ($_.IsAutoCreated -eq $false) -and ($_.ZoneType -ne 'Forwarder') -and ($_.IsDsIntegrated -eq $false) } # Add zone and server details to the zone list. foreach ($zone in $Zones) { $AddToList = [PSCustomObject]@{ 'Server Name' = $dnsServer.Name 'Server IP' = $dnsServer.IPAddress 'Zone Name' = $zone.ZoneName 'Zone Type' = $zone.ZoneType 'Is Reverse?' = $zone.IsReverseLookupZone } $ZoneList += $AddToList } } if ($ZoneList.Count -lt 1) { Write-Host 'No non-AD-integrated zones were found.' } # Return the ZoneList object $ZoneList } function Get-BTQueryResolutionPolicy { [CmdletBinding()] param ( [Parameter()] [array]$Domains, # Name of the DNS server[s] to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($null -eq $script:DNSServers) { $script:DNSServers = Get-BTDnsServer -Domains $Domains -Exclude $Exclude } $QueryResolutionPolicyList = @() foreach ($dnsServer in $script:DNSServers) { # Enumerate the query resolution policies on each DNS server $QueryResolutionPolicies = (Get-DnsServer -ComputerName $dnsServer.IPAddress -ErrorAction Ignore -WarningAction Ignore).ServerPolicies # Add to the list if this DNS server's IP address is not already in the list. if ($QueryResolutionPolicyList.'Server IP' -notcontains $dnsServer.IPAddress) { foreach ($policy in $QueryResolutionPolicies) { $AddToList = [PSCustomObject]@{ 'Server Name' = $dnsServer.Name 'Server IP' = $dnsServer.IPAddress 'QRP Name' = $policy.Name 'QRP Level' = $policy.Level 'QRP Processing Order' = $policy.ProcessingOrder 'QRP Enabled?' = $policy.IsEnabled 'QRP Action' = $policy.Action } $QueryResolutionPolicyList += $AddToList } } } $QueryResolutionPolicyList } function Get-BTSecurityDescriptor { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $ObjectACLList = @() $ForestDN = (Get-ADRootDSE).rootDomainNamingContext foreach ($domain in $Domains) { $DomainDN = (Get-ADDomain $domain).DistinguishedName $DomainNetBIOSName = (Get-ADDomain $domain).NetBIOSName $Locations = @() if ($ForestDN -eq $DomainDN) { $Locations = 'DC=ForestDnsZones', 'DC=DomainDnsZones', 'CN=MicrosoftDNS,CN=System' } else { $Locations = 'DC=DomainDnsZones' } New-PSDrive -Name $DomainNetBIOSName -PSProvider ActiveDirectory -Server $domain -Root "//RootDSE/" | Out-Null $Objects = @() foreach ($location in $Locations) { $Objects = Get-ADObject -Filter * -SearchBase "$location,$DomainDN" -Server $domain foreach ($object in $Objects) { $AddToList = Get-Acl "$($DomainNetBIOSName):$($object.DistinguishedName)" $AddToList | Add-Member NoteProperty -Name Name -Value $object.Name $AddToList | Add-Member NoteProperty -Name DistinguishedName -Value $object.DistinguishedName $ObjectACLList += $AddToList } } } $ObjectACLList } function Get-BTSocketPoolSize { [CmdletBinding()] param ( [Parameter()] [array]$Domains, # Name of the DNS server[s] to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($null -eq $script:DNSServers) { $script:DNSServers = Get-BTDnsServer -Domains $Domains -Exclude $Exclude } $SocketPoolSizeList = @() foreach ($dnsServer in $script:DNSServers) { # Enumerate the socket pool size on each DNS server. [int32]$SocketPoolSize = (Get-DnsServerSetting -ComputerName $dnsServer.IPAddress -All -WarningAction Ignore).SocketPoolSize if ($SocketPoolSizeList.'Server IP' -notcontains $dnsServer.IPAddress) { $AddToList = [PSCustomObject]@{ 'Server Name' = $dnsServer.Name 'Server IP' = $dnsServer.IPAddress 'Socket Pool Size' = $SocketPoolSize } } $SocketPoolSizeList += $AddToList } $SocketPoolSizeList } function Get-BTTarget { param ( [string]$Forest = (Get-ADForest).Name, [string]$InputPath ) if ($InputPath) { $Targets = Get-Content $InputPath } else { $Targets = (Get-ADForest $Forest).Domains } $Targets } function Get-BTTombstonedNode { [CmdletBinding()] param ( [Parameter()] [array]$Domains, # Name of the DNS server[s] to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($null -eq $script:DNSServers) { $script:DNSServers = Get-BTDnsServer -Domains $Domains -Exclude $Exclude } $TombstonedNodeList = @() foreach ($domain in $Domains) { $domainDN = (Get-ADDomain $domain).DistinguishedName $Zones = Get-DnsServerZone -ComputerName $domain foreach ($zone in $Zones) { $Nodes = Get-DnsServerResourceRecord -ComputerName $domain -ZoneName $zone.ZoneName foreach ($node in $Nodes) { if ($node.DistinguishedName -like "*$domainDN") { try { $nodeDetails = Get-ADObject -Identity $node.DistinguishedName -Properties dNSTombstoned -Server $domain } catch { Write-Verbose "Unable to find tombstoned node $($node.DistinguishedName)" -Verbose } } if ($nodeDetails.dNSTombstoned) { $AddToList = [PSCustomObject]@{ 'Zone Name' = $zone.ZoneName 'Node Name' = $node.HostName 'Record Type' = $node.RecordType 'Node DN' = $node.DistinguishedName } $TombstonedNodeList += $AddToList } } } } $TombstonedNodeList } function Get-BTWildcardRecord { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $WildcardRecordList = @() foreach ($domain in $Domains) { $RRTypes = @('HInfo', 'Afsdb', 'Atma', 'Isdn', 'Key', 'Mb', 'Md', 'Mf', 'Mg', 'MInfo', 'Mr', 'Mx', 'NsNxt', 'Rp', 'Rt', 'Wks', 'X25', 'A', 'AAAA', 'CName', 'Ptr', 'Srv', 'Txt', 'Wins', 'WinsR', 'Ns', 'Soa', 'NasP', 'NasPtr', 'DName', 'Gpos', 'Loc', 'DhcId', 'Naptr', 'RRSig', 'DnsKey', 'DS', 'NSec', 'NSec3', 'NSec3Param', 'Tlsa') $WildcardExists = $false foreach ($rrtype in $RRTypes) { if (Get-DnsServerResourceRecord -ComputerName $domain -ZoneName $domain -RRType $rrtype -Name '*' -ErrorAction Ignore) { $WildcardExists = $true $ActualRRType = $rrtype } } if ($WildcardExists -eq $true) { $AddToList = [PSCustomObject]@{ 'Domain' = $domain 'Wildcard Exists?' = $true 'Wildcard Type' = $ActualRRType } } else { $AddToList = [PSCustomObject]@{ 'Domain' = $domain 'Wildcard Exists?' = $false 'Wildcard Type' = 'N/A' } } $WildcardRecordList += $AddToList } $WildcardRecordList } function Get-BTWPADRecord { [CmdletBinding()] param ( [Parameter()] [array]$Domains ) if ($null -eq $Domains) { $Domains = Get-BTTarget } $WPADRecordList = @() foreach ($domain in $Domains) { $RRTypes = @('HInfo', 'Afsdb', 'Atma', 'Isdn', 'Key', 'Mb', 'Md', 'Mf', 'Mg', 'MInfo', 'Mr', 'Mx', 'NsNxt', 'Rp', 'Rt', 'Wks', 'X25', 'A', 'AAAA', 'CName', 'Ptr', 'Srv', 'Txt', 'Wins', 'WinsR', 'Ns', 'Soa', 'NasP', 'NasPtr', 'DName', 'Gpos', 'Loc', 'DhcId', 'Naptr', 'RRSig', 'DnsKey', 'DS', 'NSec', 'NSec3', 'NSec3Param', 'Tlsa') $WPADExists = $false foreach ($rrtype in $RRTypes) { if (Get-DnsServerResourceRecord -ComputerName $domain -ZoneName $domain -RRType $rrtype -Name 'wpad' -ErrorAction Ignore) { $WPADExists = $true $ActualRRType = $rrtype } } if ($WPADExists -eq $true) { $AddToList = [PSCustomObject]@{ 'Domain' = $domain 'WPAD Exists?' = $true 'WPAD Type' = $ActualRRType } } else { $AddToList = [PSCustomObject]@{ 'Domain' = $domain 'WPAD Exists?' = $false 'WPAD Type' = 'N/A' } } $WPADRecordList += $AddToList } $WPADRecordList } function Get-BTZoneScope { [CmdletBinding()] param ( [Parameter()] [array]$Domains, # Name of the DNS server[s] to exclude [Parameter()] [string[]] $Exclude ) if ($null -eq $Domains) { $Domains = Get-BTTarget } if ($null -eq $script:DNSServers) { $script:DNSServers = Get-BTDnsServer -Domains $Domains -Exclude $Exclude } $ZoneScopeList = @() foreach ($dnsServer in $script:DNSServers) { # Enumerate the zone scopes on each DNS server $ZoneScopes = Get-DnsServerZone -ComputerName $dnsServer.IPAddress | Where-Object { ($_.IsDsIntegrated -eq $true) -and ($_.IsReverseLookupZone -eq $false) -and ($_.ZoneName -ne 'TrustAnchors') } | Get-DnsServerZoneScope -ComputerName $dnsServer.IPAddress -ErrorAction Ignore if ($ZoneScopeList.'Server IP' -notcontains $dnsServer.IPAddress) { foreach ($scope in $ZoneScopes) { $AddToList = [PSCustomObject]@{ 'Server Name' = $dnsServer.Name 'Server IP' = $dnsServer.IPAddress 'Zone Scope Name' = $scope.ZoneScope } $ZoneScopeList += $AddToList } } } $ZoneScopeList } function Get-BTZoneScopeContainer { [CmdletBinding()] param ( [Parameter()] [array]$ADIZones ) if ($null -eq $ADIZones) { $ADIZones = Get-BTADIZone } $ZoneScopeContainerList = @() foreach ($adizone in $ADIZones) { [string]$domainDN = (Get-ADDomain $adizone.Domain).DistinguishedName try { $zoneScopeDN = Get-ADObject -Identity "CN=ZoneScopeContainer,DC=$($adizone.'Zone Name'),CN=MicrosoftDNS,DC=DomainDnsZones,$domainDN" -Server $adizone.Domain -Properties DistinguishedName -ErrorAction SilentlyContinue $AddToList = [PSCustomObject]@{ Domain = $adizone.Domain 'Zone Name' = $adizone.'Zone Name' 'Zone Scope Container DN' = $zoneScopeDN } $ZoneScopeContainerList += $AddToList } catch { } } $ZoneScopeContainerList } function Repair-BTDanglingSPN { [CmdletBinding()] param ( [Parameter()] [array]$DanglingSPNs, [switch]$Run = $false ) if ($null -eq $DanglingSPNs) { $DanglingSPNs = Get-BTDanglingSPN } if ($Run) { foreach ($danglingspn in $DanglingSPNs) { setspn -d $danglingspn.'Dangling SPN' $danglingspn.'Identity Reference' } } else { foreach ($danglingspn in $DanglingSPNs) { Write-Host "Run the following code block to delete the identified Dangling SPN" -ForegroundColor Green Write-Host "SPN: $($danglingspn.'Dangling SPN')" -ForegroundColor Green Write-Host "Principal: $($danglingspn.'Identity Reference')" -ForegroundColor Green Write-Host "setspn -d $($danglingspn.'Dangling SPN') $($danglingspn.'Identity Reference')" Write-Host } } } function Repair-BTTestedADILegacyZone { [CmdletBinding()] param ( [Parameter()] [array]$TestedADILegacyZones, [switch]$Run = $false ) if ($null -eq $TestedADILegacyZones) { $TestedADILegacyZones = Test-BTADILegacyZone } if ($Run) { foreach ($adizone in $TestedADILegacyZones) { # $DomainReplicatedZonePartition = "DomainDnsZones.$adizone.Domain" $ForestReplicatedZonePartition = "ForestDnsZones.$(Get-ADForest $($adizone.Domain))" dnscmd $adizone.Domain /ZoneChangeDirectoryPartition $adizone.'Zone Name' $ForestReplicatedZonePartition } } else { foreach ($adizone in $TestedADILegacyZones) { Write-Host "Run the following code block to convert the $($adizone.Domain) Zone from a Legacy (Windows 2000 compatible Zone) to a Forest-replicated Zone." -ForegroundColor Green Write-Host @" `$ForestReplicatedZonePartition = 'ForestDnsZones.$(Get-ADForest $($adizone.Domain))' dnscmd $($adizone.Domain) /ZoneChangeDirectoryPartition $($adizone.'Zone Name') `$ForestReplicatedZonePartition "@ } } } function Repair-BTTestedSocketPoolSize { [CmdletBinding()] param ( [Parameter()] [array]$TestedSocketPoolSizes, [switch]$Run = $false ) if ($null -eq $TestedSocketPoolSizes) { $TestedSocketPoolSizes = Test-BTSocketPoolSize } if ($Run) { foreach ($testedsocketpoolsize in $TestedSocketPoolSizes) { $Settings = Get-DnsServerSetting -ComputerName $testedsocketpoolsize.'Server IP' -All $Settings.SocketPoolSize = 10000 Set-DnsServerSetting -ComputerName $testedsocketpoolsize.'Server IP' -InputObject $Settings } } else { foreach ($testedsocketpoolsize in $TestedSocketPoolSizes) { Write-Host "Run the following code block to set DNS Server $($testedsocketpoolsize.'Server IP') Socket Pool Size to 10,000:" -ForegroundColor Green Write-Host @" `$Settings = Get-DnsServerSetting -ComputerName $($testedsocketpoolsize.'Server IP') -All `$Settings.SocketPoolSize = 10000 Set-DnsServerSetting -ComputerName $($testedsocketpoolsize.'Server IP') -InputObject `$Settings "@ } } } function Repair-BTTestedWildcardRecord { [CmdletBinding()] param ( [Parameter()] [array]$TestedWildcardRecords, [switch]$Run = $false ) if ($null -eq $TestedWildcardRecords) { $TestedWildcardRecords = Test-BTWildcardRecord } if ($Run) { foreach ($wildcardrecord in $TestedWildcardRecords) { $type = "-$($wildcardrecord.'Correct Type')" if ($wildcardrecord.'Wildcard Exists?') { Remove-DnsServerResourceRecord -ComputerName $wildcardrecord.Domain -ZoneName $wildcardrecord.Domain -RRType $wildcardrecord.'Current Wildcard Type' -Name '*' } if ($type -eq '-Txt') { $AddWildcardScriptBlock = "Add-DnsServerResourceRecord -ComputerName $($wildcardrecord.Domain) -ZoneName $($wildcardrecord.Domain) $type -Name '*' -DescriptiveText '0.0.0.0'" } elseif ($type -eq '-A') { $AddWildcardScriptBlock = "Add-DnsServerResourceRecord -ComputerName $($wildcardrecord.Domain) -ZoneName $($wildcardrecord.Domain) $type -Name '*' -RecordData '0.0.0.0'" } $ScriptBlock = [scriptblock]::Create($AddWildcardScriptBlock) Invoke-Command -ScriptBlock $ScriptBlock } } else { foreach ($wildcardrecord in $TestedWildcardRecords) { $type = "-$($wildcardrecord.'Correct Type')" if ($wildcardrecord.'Wildcard Exists?') { Write-Host "Run the following code block to delete the Wildcard Record of incorrect type ($($wildcardrecord.'Current Wildcard Type')) and replace with a Wildcard Record of the correct type ($type) in the $($wildcardrecord.Domain) domain" -ForegroundColor Green if ($type -eq '-Txt') { $AddWildcardScriptBlock = "Add-DnsServerResourceRecord -ComputerName $($wildcardrecord.Domain) -ZoneName $($wildcardrecord.Domain) $type -Name '*' -DescriptiveText '0.0.0.0'" } elseif ($type -eq '-A') { $AddWildcardScriptBlock = "Add-DnsServerResourceRecord -ComputerName $($wildcardrecord.Domain) -ZoneName $($wildcardrecord.Domain) $type -Name '*' -RecordData '0.0.0.0'" } Write-Host @" Remove-DnsServerResourceRecord -ComputerName $($wildcardrecord.Domain) -ZoneName $($wildcardrecord.Domain) -RRType $($wildcardrecord.'Current Wildcard Type') -Name '*' $AddWildcardScriptBlock "@ } else { Write-Host "Run the following code block to create a Wildcard Record in the $($wildcardrecord.Domain) domain" -ForegroundColor Green if ($type -eq '-Txt') { Write-Host "Add-DnsServerResourceRecord -ComputerName $($wildcardrecord.Domain) -ZoneName $($wildcardrecord.Domain) $type -Name '*' -DescriptiveText '0.0.0.0'" } elseif ($type -eq '-A') { Write-Host "Add-DnsServerResourceRecord -ComputerName $($wildcardrecord.Domain) -ZoneName $($wildcardrecord.Domain) $type -Name '*' -RecordData '0.0.0.0'" } Write-Host } } } } function Repair-BTTestedWPADRecord { [CmdletBinding()] param ( [Parameter()] [array]$TestedWPADRecords, [switch]$Run = $false ) if ($null -eq $TestedWPADRecords) { $TestedWPADRecords = Test-BTWPADRecord } if ($Run) { foreach ($wpadrecord in $TestedWPADRecords) { $type = "-$($wpadrecord.'Correct Type')" if ($wpadrecord.'WPAD Exists?') { Remove-DnsServerResourceRecord -ComputerName $wpadrecord.Domain -ZoneName $wpadrecord.Domain -RRType $wpadrecord.'Current WPAD Type' -Name 'WPAD' } if ($type -eq '-Txt') { $AddWPADScriptBlock = "Add-DnsServerResourceRecord -ComputerName $($wpadrecord.Domain) -ZoneName $($wpadrecord.Domain) $type -Name 'WPAD' -DescriptiveText '0.0.0.0'" } elseif ($type -eq '-A') { $AddWPADScriptBlock = "Add-DnsServerResourceRecord -ComputerName $($wpadrecord.Domain) -ZoneName $($wpadrecord.Domain) $type -Name 'WPAD' -RecordData '0.0.0.0'" } $ScriptBlock = [scriptblock]::Create($AddWPADScriptBlock) Invoke-Command -ScriptBlock $ScriptBlock } } else { foreach ($wpadrecord in $TestedWPADRecords) { $type = "-$($wpadrecord.'Correct Type')" if ($wpadrecord.'WPAD Exists?') { Write-Host "Run the following code block to delete the WPAD Record of incorrect type ($($wpadrecord.'Current WPAD Type')) and replace with a WPAD Record of the correct type ($type) in the $($wpadrecord.Domain) domain" -ForegroundColor Green if ($type -eq '-Txt') { $AddWPADScriptBlock = "Add-DnsServerResourceRecord -ComputerName $($wpadrecord.Domain) -ZoneName $($wpadrecord.Domain) $type -Name 'WPAD' -DescriptiveText '0.0.0.0'" } elseif ($type -eq '-A') { $AddWPADScriptBlock = "Add-DnsServerResourceRecord -ComputerName $($wpadrecord.Domain) -ZoneName $($wpadrecord.Domain) $type -Name 'WPAD' -RecordData '0.0.0.0'" } Write-Host @" Remove-DnsServerResourceRecord -ComputerName $($wpadrecord.Domain) -ZoneName $($wpadrecord.Domain) -RRType $($wpadrecord.'Current WPAD Type') -Name 'WPAD' $AddWPADScriptBlock "@ } else { Write-Host "Run the following code block to create a WPAD Record in the $($wpadrecord.Domain) domain" -ForegroundColor Green if ($type -eq '-Txt') { Write-Host "Add-DnsServerResourceRecord -ComputerName $($wpadrecord.Domain) -ZoneName $($wpadrecord.Domain) $type -Name 'WPAD' -DescriptiveText '0.0.0.0'" } elseif ($type -eq '-A') { Write-Host "Add-DnsServerResourceRecord -ComputerName $($wpadrecord.Domain) -ZoneName $($wpadrecord.Domain) $type -Name 'WPAD' -RecordData '0.0.0.0'" } Write-Host } } } } function Repair-BTTombstonedNode { [CmdletBinding()] param ( [Parameter()] [array]$TombstonedNodes, [switch]$Run = $false ) if ($null -eq $TombstonedNodes) { $TombstonedNodes = Get-BTTombstonedNode } if ($Run) { foreach ($tombstonednode in $TombstonedNodes) { Remove-ADObject $tombstonednode.'Node DN' } } else { foreach ($tombstonednode in $TombstonedNodes) { Write-Host "Run the following code block to delete the $($tombstonednode.'Node Name') node from the $($tombstonednode.'Zone Name') zone." -ForegroundColor Green Write-Host @" Remove-ADObject '$($tombstonednode.'Node DN')' "@ } } } # function Repair-BTThing { # [CmdletBinding()] # param ( # [Parameter()] # [array]$Things, # [switch]$Run = $false # ) # if ($null -eq $Things) { # $Things = Get-BTThing # } # if ($Run) { # foreach ($thing in $Things) { # } # } else { # foreach ($thing in $Things) { # Write-Host "DESCRIPTION OF CODE BLOCK" -ForegroundColor Green # Write-Host @" # CODE BLOCK # "@ # } # } # } function Show-BTCollectedData { [CmdletBinding()] param ( [switch]$ShowSecurityDescriptors = $false, [switch]$Demo = $false, [ValidateSet( 'All', 'ADIZones', 'ConditionalForwarders', 'DanglingSPNs', 'DnsAdminsMemberships', 'DnsUpdateProxyMemberships', 'DynamicUpdateServiceAccounts', 'ForwarderConfigurations', 'GlobalQueryBlockLists', 'NonADIZones', 'QueryResolutionPolicys', 'SecurityDescriptors', 'SocketPoolSizes', 'TombstonedNodes', 'WildcardRecords', 'WPADRecords', 'ZoneScopes', 'ZoneScopeContainers' )] [string]$Section = 'All', $ADIZones, $ConditionalForwarders, $DanglingSPNs, $DnsAdminsMemberships, $DnsUpdateProxyMemberships, $DynamicUpdateServiceAccounts, $ForwarderConfigurations, $GlobalQueryBlockLists, $NonADIZones, $QueryResolutionPolicys, $SecurityDescriptors, $SocketPoolSizes, $TombstonedNodes, $WildcardRecords, $WPADRecords, $ZoneScopes, $ZoneScopeContainers ) $Sections = @( 'ADIZones', 'ConditionalForwarders', 'DanglingSPNs', 'DnsAdminsMemberships', 'DnsUpdateProxyMemberships', 'DynamicUpdateServiceAccounts', 'ForwarderConfigurations', 'GlobalQueryBlockLists', 'NonADIZones', 'QueryResolutionPolicys', 'SocketPoolSizes', 'TombstonedNodes', 'WildcardRecords', 'WPADRecords', 'ZoneScopes', 'ZoneScopeContainers' ) $TitleHashtable = @{ 'Section' = 'Friendly Name' 'ADIZones' = 'All ADI Zones' 'ConditionalForwarders' = 'All Conditional Forwarders' 'DanglingSPNs' = 'All Dangling SPNs' 'DnsAdminsMemberships' = 'DnsAdmins Memberships' 'DnsUpdateProxyMemberships' = 'DnsUpdateProxy Memberships' 'DynamicUpdateServiceAccounts' = 'Dynamic Update Service Account Configuration by DHCP Server' 'ForwarderConfigurations' = 'Forwarder Configurations by DNS Server' 'GlobalQueryBlockLists' = 'All Global Query Block Lists' 'NonADIZones' = 'All Non-ADI Zones' 'QueryResolutionPolicys' = 'All Query Resolution Policies' 'SecurityDescriptors' = 'All Security Descriptors' 'SocketPoolSizes' = 'Socket Pool Size Configuration by DNS Server' 'TombstonedNodes' = 'All Tombstoned Nodes' 'WildcardRecords' = 'Wildcard Record Configuration by Domain' 'WPADRecords' = 'WPAD Record Configuration by Domain' 'ZoneScopes' = 'All Zone Scopes' 'ZoneScopeContainers' = 'All Zone Scope Containers' } if ($ShowSecurityDescriptors) { $Sections += 'SecurityDescriptors' } if ($Section = 'All') { foreach ($entry in $Sections) { $Title = $TitleHashtable[$entry] if ($null -ne (Get-Variable $entry).Value) { if ($Demo) { Clear-Host } Write-Host "/--------------- $Title ---------------\" -ForegroundColor Green (Get-Variable $entry).Value | Format-List Write-Host "\--------------- $Title ---------------/" -ForegroundColor Green Read-Host "Press Enter to load the next section" } } } else { $Title = $TitleHashtable[$Section] if ($Demo) { Clear-Host } Write-Host "/--------------- $Title ---------------\" -ForegroundColor Green if ($null -eq (Get-Variable $Section).Value) { Write-Host "No data collected for $Title" -ForegroundColor Yellow } else { (Get-Variable $Section).Value | Format-List } Write-Host "\--------------- $Title ---------------/" -ForegroundColor Green } } function Show-BTFixes { [CmdletBinding()] param ( [switch]$ShowSecurityDescriptors = $false, [switch]$Demo, [ValidateSet( 'All', 'TestedSocketPoolSizes', 'TombstonedNodes', 'TestedWildcardRecords', 'TestedWPADRecords', 'DanglingSPNs', 'TestedADILegacyZones' )] [string]$Section = 'All', $ConditionalForwarders, $DanglingSPNs, $DnsAdminsMemberships, $DnsUpdateProxyMemberships, $NonADIZones, $QueryResolutionPolicys, $TombstonedNodes, $ZoneScopes, $TestedADILegacyZones, $TestedADIInsecureUpdateZones, $TestedDynamicUpdateServiceAccounts, $TestedForwarderConfigurations, $TestedGlobalQueryBlockLists, $TestedSecurityDescriptorACEs, $TestedSecurityDescriptorOwners, $TestedSocketPoolSizes, $TestedWildcardRecords, $TestedWPADRecords, $TestedZoneScopeContainers ) $Sections = @( 'TestedSocketPoolSizes', 'TombstonedNodes', 'TestedWildcardRecords', 'TestedWPADRecords', 'DanglingSPNs', 'TestedADILegacyZones' ) $TitleHashtable = @{ 'Section' = 'Friendly Name' 'TestedSocketPoolSizes' = 'Set Socket Pool Size To Maximum' 'TombstonedNodes' = 'Delete All Tombstoned Nodes' 'TestedWildcardRecords' = 'Fix Wildcard Record Configuration by Domain' 'TestedWPADRecords' = 'Fix WPAD Record Configuration by Domain' 'DanglingSPNs' = 'Delete Dangling SPNs' 'TestedADILegacyZones' = 'Convert Legacy Zones to ForestDNS Zones' } if ($ShowSecurityDescriptors) { $Sections += 'SecurityDescriptors' } if ($Section = 'All') { foreach ($entry in $Sections) { $Title = $TitleHashtable[$entry] if ($null -ne (Get-Variable $entry).Value) { if ($Demo) { Clear-Host } Write-Host "/--------------- $Title ---------------\" -ForegroundColor Green $SectionScriptBlock = "Repair-BT$entry" $SectionScriptBlock = $SectionScriptBlock.TrimEnd('s') + " -$entry `$$entry" $ScriptBlock = [scriptblock]::Create($SectionScriptBlock) Invoke-Command -ScriptBlock $ScriptBlock Write-Host "\--------------- $Title ---------------/" -ForegroundColor Green Read-Host "Press Enter to load the next section" } } } else { $Title = $TitleHashtable[$Section] if ($Demo) { Clear-Host } Write-Host "/--------------- $Title ---------------\" -ForegroundColor Green if ($null -eq (Get-Variable $Section).Value) { Write-Host "No data collected for $Title" -ForegroundColor Yellow } else { $SectionScriptBlock = "Repair-BT$Section" $SectionScriptBlock = $SectionScriptBlock.TrimEnd('s') + " -$Section `$$Section" $ScriptBlock = [scriptblock]::Create($SectionScriptBlock) Invoke-Command -ScriptBlock $ScriptBlock } Write-Host "\--------------- $Title ---------------/" -ForegroundColor Green } } function Show-BTLogo { param( [string]$Version ) $BGColor = $host.UI.RawUI.BackgroundColor Write-Host ' ' -BackgroundColor $BGColor Write-Host ' ::::::::: ::: ::: ::: :::::::::: ::::::::::: ::: ::: ::: ::: :::::::::: ::::::::: :::::::: ' -ForegroundColor DarkMagenta -BackgroundColor Black -NoNewline Write-Host ' ' -BackgroundColor $BGColor Write-Host ' :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: ' -ForegroundColor Magenta -BackgroundColor Black -NoNewline Write-Host ' ' -BackgroundColor $BGColor Write-Host ' +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ ' -ForegroundColor Magenta -BackgroundColor Black -NoNewline Write-Host ' ' -BackgroundColor $BGColor Write-Host ' +#++:++#+ +#+ +#+ +:+ +#++:++# +#+ +#+ +:+ +#++:+ +#++:++# +#+ +:+ +#+ +:+ ' -ForegroundColor DarkBlue -BackgroundColor Black -NoNewline Write-Host ' ' -BackgroundColor $BGColor Write-Host ' +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ ' -ForegroundColor DarkBlue -BackgroundColor Black -NoNewline Write-Host ' ' -BackgroundColor $BGColor Write-Host ' #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# ' -ForegroundColor Blue -BackgroundColor Black -NoNewline Write-Host ' ' -BackgroundColor $BGColor Write-Host ' ######### ######### ######### ########## ### ######## ### ### ########## ######### ######## ' -ForegroundColor Blue -BackgroundColor Black -NoNewline Write-Host ' ' -BackgroundColor $BGColor Write-Host ' ' -BackgroundColor $BGColor Write-Host " v$Version" } function Show-BTTestedData { [CmdletBinding()] param ( [switch]$ShowSecurityDescriptors = $false, [switch]$Demo, [ValidateSet( 'All', 'ConditionalForwarders', 'DanglingSPNs', 'DnsAdminsMemberships', 'DnsUpdateProxyMemberships', 'NonADIZones', 'QueryResolutionPolicys', 'TombstonedNodes', 'ZoneScopes', 'TestedADILegacyZones', 'TestedADIInsecureUpdateZones', 'TestedDynamicUpdateServiceAccounts', 'TestedForwarderConfigurations', 'TestedGlobalQueryBlockLists', 'TestedSecurityDescriptorACEs', 'TestedSecurityDescriptorOwners', 'TestedSocketPoolSizes', 'TestedWildcardRecords', 'TestedWPADRecords', 'TestedZoneScopeContainers' )] [string]$Section = 'All', $ConditionalForwarders, $DanglingSPNs, $DnsAdminsMemberships, $DnsUpdateProxyMemberships, $NonADIZones, $QueryResolutionPolicys, $TombstonedNodes, $ZoneScopes, $TestedADILegacyZones, $TestedADIInsecureUpdateZones, $TestedDynamicUpdateServiceAccounts, $TestedForwarderConfigurations, $TestedGlobalQueryBlockLists, $TestedSecurityDescriptorACEs, $TestedSecurityDescriptorOwners, $TestedSocketPoolSizes, $TestedWildcardRecords, $TestedWPADRecords, $TestedZoneScopeContainers ) $Sections = @( 'ConditionalForwarders', 'DanglingSPNs', 'DnsAdminsMemberships', 'DnsUpdateProxyMemberships', 'NonADIZones', 'QueryResolutionPolicys', 'TombstonedNodes', 'ZoneScopes', 'TestedADILegacyZones', 'TestedADIInsecureUpdateZones', 'TestedDynamicUpdateServiceAccounts', 'TestedForwarderConfigurations', 'TestedGlobalQueryBlockLists', 'TestedSocketPoolSizes', 'TestedWildcardRecords', 'TestedWPADRecords', 'TestedZoneScopeContainers' ) $TitleHashtable = @{ 'Section' = 'Friendly Name' 'ConditionalForwarders' = 'All Conditional Forwarders' 'DanglingSPNs' = 'All Dangling SPNs' 'DnsAdminsMemberships' = 'DnsAdmins Membership (per-domain)' 'DnsUpdateProxyMemberships' = 'DnsUpdateProxy Membership (per-domain)' 'NonADIZones' = 'All Non-ADI Zones' 'QueryResolutionPolicys' = 'All Query Resolution Policies' 'TombstonedNodes' = 'All Tombstoned Nodes' 'ZoneScopes' = 'Tested Zone Scopes' 'TestedADILegacyZones' = 'Legacy ADI Zones' 'TestedADIInsecureUpdateZones' = 'ADI Zones not configured for Secure Updates' 'TestedDynamicUpdateServiceAccounts' = 'DHCP Servers not configured to use Dynamic Update Service Accounts' 'TestedForwarderConfigurations' = 'All Configured Forwarders' 'TestedGlobalQueryBlockLists' = 'All Global Query Block Lists' 'TestedSecurityDescriptorACEs' = 'Possibly Dangerous ACEs on DNS Objects' 'TestedSecurityDescriptorOwners' = 'Possibly Dangerous Owners of DNS Objects' 'TestedSocketPoolSizes' = 'Socket Pool Sizes Less Than Maximum' 'TestedWildcardRecords' = 'Missing or Invalid Wildcard Records' 'TestedWPADRecords' = 'Missing or Invalid WPAD Records' 'TestedZoneScopeContainers' = 'Empty Zone Scope Containers' } $DescriptionHashtable = @{ 'Section' = "Description" 'ConditionalForwarders' = "Check this list of conditional forwarders.`nAre they still in use?" 'DanglingSPNs' = "Dangling SPNs are Service Principal Names where the 'Host' portion of the SPN does not resolve to an IP address.`nDangling SPNs can be used by an attacker to coerce Kerberos authentication." 'DnsAdminsMemberships' = "The DnsAdmins group remains incredibly powereful.`nKeep this group empty if possible.`nIf not possible, ensure the members are protected like Tier 0 assets." 'DnsUpdateProxyMemberships' = "Members of DnsUpdateProxy group can update ADI DNS records regardless of existing ownership.`nWhile doing so, they grant Authenticated Users the right to modify the DNS record.`nThis group should be kept empty if possible." 'NonADIZones' = "Non-ADI Zones have their information stored on each DNS server/DC instead of in AD.`nNon-ADI Zones create inconsistency across resolvers." 'QueryResolutionPolicys' = "Query Resolution Policies are configured per-server and do not appear in the DNS snap-in.`nAudit these entries to ensure they are apppropriate." 'TombstonedNodes' = "Tombstoned Nodes can be updated by any security principal in the forest.`nRemove these nodes." 'ZoneScopes' = "Zone Scopes can be used to create a fully ADI split-brain DNS.`nEnsure these scopes are appropriate for your environment" 'TestedADILegacyZones' = "ADI Zones can be replicated in 3 ways: forest-replicated, domain-replicated, and Windows 2000-compatible mode (aka Legacy).`nLegacy Zones are not protected in the same manner as other zones and inherit ACEs from the domain root.`nThese zones should be converted to one of the other types." 'TestedADIInsecureUpdateZones' = "[TODO]" 'TestedDynamicUpdateServiceAccounts' = "Out-of-the-box, AD-joined computers that receive an IP address from a Windows DHCP server can create and update their own DNS nodes.`nA more secure method of creating these nodes is to configure a Dynamic Update Service Account on each DHCP server.`nWhen configured, Dynamic Update Service Accounts can be used to create DNS records on behalf of computers.`nThis makes auditing DACLs easier.`n`nThe following DNS servers do not use a Dynamic Update Service Account:" 'TestedForwarderConfigurations' = "When a local DNS server cannot resolve a request, they send a request to a Forwarder.`nCheck the following list to ensure the Forwarders are approriate for your environment." 'TestedGlobalQueryBlockLists' = "Despite the name, Global Query Block Lists are configured per-server.`nEach GQBL contains a list of names that the DNS server will not resolve.`nThis list should contain the 'wpad' and 'isatap' records at a minimum." 'TestedSecurityDescriptorACEs' = "The following DNS objects have possibly dangerous Access Control Entries.`nNote: if Dynamic Update Service Accounts are not configured on each DHCP server, this section will be very noisy." 'TestedSecurityDescriptorOwners' = "The following DNS objects have possibly dangerous Owners.`nNote: if Dynamic Update Service Accounts are not configured on each DHCP server, this section will be very noisy." 'TestedSocketPoolSizes' = "When making requests to Forwarders, DNS servers randomize their source ports to minimize AITM attacks.`nThe number of ports used is configured on each DNS Server via the Socket Pool Size value.`nBy default, this is 2500 ports, but the maximum is 10,000.`nConfigure each DNS server to use the maximum value." 'TestedWildcardRecords' = "If a Wildcard Record does not exist in a domain, an attacker can create one which points at a device they control.`nAny DNS requests that do not match an existing DNS entry will resolve to the IP of the attacker-controlled machine." 'TestedWPADRecords' = "WPAD is used to allow clients to auto-discover web proxy servers in their environment.`nIf a WPAD Record does not exist in a domain, an attacker can create one which points at a device they control.`nThis configuration could redirect all web traffic to the attacker-controller machine" 'TestedZoneScopeContainers' = "Zone Scope Containers hold Zone Scopes.`nIf a Zone Scope Container is empty, this may be an indicator of fuckery (IOF)." } if ($ShowSecurityDescriptors) { $Sections += 'TestedSecurityDescriptorACEs', 'TestedSecurityDescriptorOwners' } if ($Section = 'All') { foreach ($entry in $Sections) { $Title = $TitleHashtable[$entry] $Description = $DescriptionHashtable[$entry] if ($null -ne (Get-Variable $entry).Value) { if ($Demo) { Clear-Host } Write-Host "/--------------- $Title ---------------\" -ForegroundColor Red Write-Host $Description (Get-Variable $entry).Value | Format-List Write-Host "\--------------- $Title ---------------/" -ForegroundColor Red Read-Host "Press Enter to load the next section" } } } else { $Title = $TitleHashtable[$Section] $Description = $DescriptionHashtable[$Section] if ($Demo) { Clear-Host } Write-Host "/--------------- $Title ---------------\" -ForegroundColor Red Write-Host $Description if ($null -eq (Get-Variable $Section).Value) { Write-Host "No data collected for $Title" -ForegroundColor Yellow } else { (Get-Variable $Section).Value } Write-Host "\--------------- $Title ---------------/" -ForegroundColor Red } } function Test-BTADIInsecureUpdateZone { [CmdletBinding()] param ( [Parameter()] [array]$ADIZones ) if ($null -eq $ADIZones) { $ADIZones = Get-BTADIZone } $FailedADIZoneList = @() foreach ($adizone in $ADIZones) { if ( ($adizone.'Zone Type' -ne 'Stub') -and ($adizone.'Dynamic Update' -ne 'Secure') ) { $FailedADIZoneList += $adizone } } $FailedADIZoneList } function Test-BTADILegacyZone { [CmdletBinding()] param ( [Parameter()] [array]$ADIZones ) if ($null -eq $ADIZones) { $ADIZones = Get-BTADIZone } $FailedADIZoneList = @() foreach ($adizone in $ADIZones) { if ($adizone.'Zone Type' -eq 'Legacy') { [string]$domainDN = (Get-ADDomain $adizone.Domain).DistinguishedName try { $zoneDN = Get-ADObject -Identity "DC=$($adizone.'Zone Name'),CN=MicrosoftDNS,CN=System,$domainDN" -Server $adizone.Domain -Properties DistinguishedName -ErrorAction SilentlyContinue $AddToList = [PSCustomObject]@{ 'Domain' = $adizone.Domain 'Zone Name' = $adizone.'Zone Name' 'Zone Type' = $adizone.'Zone Type' 'Is Reverse?' = $adizone.'Is Reverse?' 'Zone DN' = $zoneDN } } catch { } $FailedADIZoneList += $AddToList } } $FailedADIZoneList } function Test-BTDynamicUpdateServiceAccount { [CmdletBinding()] param ( [Parameter()] [array]$DynamicUpdateServiceAccounts ) if ($null -eq $DynamicUpdateServiceAccounts) { $DynamicUpdateServiceAccounts = Get-BTDynamicUpdateServiceAccount } $FailedDynamicUpdateServiceAccount = @() foreach ($dynamicupdateserviceaccount in $DynamicUpdateServiceAccounts) { if ( ($dynamicupdateserviceaccount.'Service Account Name' -eq 'Not Configured') -and ($dynamicupdateserviceaccount.'Service Account Domain' -eq 'N/A') ) { $FailedDynamicUpdateServiceAccount += $dynamicupdateserviceaccount } } $FailedDynamicUpdateServiceAccount } function Test-BTForwarderConfiguration { [CmdletBinding()] param ( [Parameter()] [array]$ForwarderConfigurations ) if ($null -eq $ForwarderConfigurations) { $ForwarderConfigurations = Get-BTForwarderConfiguration } $AuditedForwarderConfiguration = @() $ForwarderHashtable = @{ 'AdGuard Primary' = '94.140.14.14' 'AdGuard Secondary' = '94.140.15.15' 'Alternate Primary' = '76.76.19.19' 'Alternate Secondary' = '76.223.122.150' 'CleanBrowsing Primary' = '185.228.168.9' 'CleanBrowsing Secondary' = '185.228.169.9' 'Cloudflare Primary' = '1.1.1.1' 'Cloudflare Secondary' = '1.0.0.1' 'Cloudflare Primary (Malware Filtered)' = '1.1.1.2' 'Cloudflare Secondary (Malware Filtered)' = '1.0.0.2' 'Cloudflare Primary (Malware/Adult Filtered)' = '1.1.1.3' 'Cloudflare Secondary (Malware/Adult Filtered)' = '1.0.0.3' 'Comodo Secure Primary' = '8.26.56.26' 'Comodo Secure Secondary' = '8.20.247.20' 'Control D Primary' = '76.76.2.0' 'Control D Secondary' = '76.76.10.0' 'Google Primary' = '8.8.8.8' 'Google Secondary' = '8.8.4.4' 'OpenDNS Home Primary' = '208.67.222.222' 'OpenDNS Home Secondary' = '208.67.220.220' 'Quad9 Primary' = '9.9.9.9' 'Quad9 Secondary' = '149.112.112.112' } foreach ($forwarderconfiguration in $ForwarderConfigurations) { foreach ($forwarder in $forwarderconfiguration.Forwarders) { $resolveForwarder = Resolve-DnsName -Name $forwarder -ErrorAction Ignore $forwarderName = 'N/A' $wellKnown = $false $wellKnownName = 'N/A' if ($resolveForwarder) { $forwarderName = $resolveForwarder.NameHost foreach ($h in $ForwarderHashtable.GetEnumerator() ) { if ($h.Value -eq $forwarder) { $wellKnown = $true $wellKnownName = $h.Name } } } $AddToList = [PSCustomObject]@{ 'Server Name' = $forwarderconfiguration.'Server Name' 'Server IP' = $forwarderconfiguration.'Server IP' 'Forwarder IP' = $forwarder 'Forwarder Name' = $forwarderName 'Well-Known?' = $wellKnown 'Service' = $wellKnownName } $AuditedForwarderConfiguration += $AddToList } } $AuditedForwarderConfiguration } function Test-BTGlobalQueryBlockList { [CmdletBinding()] param ( [Parameter()] [array]$GlobalQueryBlockLists ) if ($null -eq $GlobalQueryBlockLists) { $GlobalQueryBlockLists = Get-BTGlobalQueryBlockList } $FailedGlobalQueryBlockList = @() foreach ($globalqueryblocklist in $GlobalQueryBlockLists) { $wpadExists = $true $isatapExists = $true if ($globalqueryblocklist.GQBL -notcontains 'wpad') { $wpadExists = $false } if ($globalqueryblocklist.GQBL -notcontains 'isatap') { $isatapExists = $false } if ( ($globalqueryblocklist.'Enabled?' -eq $false) -or ($wpadExists = $false) -or ($isatapExists -eq $false) ) { $AddToList = [PSCustomObject]@{ 'Server Name' = $globalqueryblocklist.'Server Name' 'Server IP' = $globalqueryblocklist.'Server IP' 'Enabled?' = $globalqueryblocklist.'Enabled?' 'WPAD Exists' = $wpadExists 'ISATAP Exists' = $isatapExists } } $FailedGlobalQueryBlockList += $AddToList } $FailedGlobalQueryBlockList } function Test-BTSecurityDescriptorACE { [CmdletBinding()] param ( [Parameter()] [array]$SecurityDescriptors, [Parameter()] [array]$DynamicUpdateServiceAccounts, [Parameter()] [array]$Domains ) if ($null -eq $SecurityDescriptors) { $SecurityDescriptors = Get-BTSecurityDescriptor } if ($null -eq $DynamicUpdateServiceAccounts) { $DynamicUpdateServiceAccounts = Get-BTDynamicUpdateServiceAccount } if ($null -eq $Domains) { $Domains = Get-BTTarget } $FailedSecurityDescriptorACE = @() $SafeSIDs = 'S-1-5-9|S-1-5-10|S-1-5-18|S-1-5-32-544' $RootDomain = (Get-ADForest $Domains[0]).RootDomain $EnterpriseAdminsSID = "$((Get-ADDomain $rootDomain).domainSID.Value)-519" $SafeSIDs += "|$EnterpriseAdminsSID" # Need to loop through domains $KeyAdminsSID = "$((Get-ADDomain $rootDomain).domainSID.Value)-526" $EnterpriseKeyAdminsSID = "$((Get-ADDomain $rootDomain).domainSID.Value)-527" foreach ($domain in $Domains) { $DomainSID = (Get-ADDomain $domain).DomainSID.Value $SafeGroupRIDs = @('-512') foreach ($rid in $SafeGroupRIDs ) { $SafeGroupSID = $DomainSID + $rid $SafeSIDs += "|$SafeGroupSID" } } # $DomainAdminsSIDs = foreach ($domain in $Domains) { # "$((Get-ADDomain $domain).domainSID.Value)-512" # } # foreach ($sid in $DomainAdminsSIDs) { # $SafeSIDs += "|$sid" # } foreach ($domain in $Domains) { $DomainSID = (Get-ADDomain $domain).DomainSID.Value $SafeGroupRIDs = @('-516') foreach ($rid in $SafeGroupRIDs ) { $DomainControllersSID = $DomainSID + $rid $SafeSIDs += "|$DomainControllersSID" $members = @() $members += (Get-ADGroupMember $DomainControllersSID -Server $domain -Recursive).SID.Value foreach ($member in $members) { $SafeSIDs += "|$member" } } } $DangerousRights = 'GenericAll|WriteDacl|WriteOwner|WriteProperty' foreach ($dynamicupdateserviceaccount in $DynamicUpdateServiceAccounts) { if ( ($dynamicupdateserviceaccount.'Service Account Name' -ne 'Not Configured') -and ($dynamicupdateserviceaccount.'Service Account Domain' -ne 'N/A') ) { $identityreference = "$($dynamicupdateserviceaccount.'Service Account Domain')\$($dynamicupdateserviceaccount.'Service Account Name')" $dynamicupdateserviceaccountSID = ConvertFrom-IdentityReference -Object $identityreference $SafeSIDs += "|$dynamicupdateserviceAccountSID" } } foreach ($securitydescriptor in $SecurityDescriptors) { foreach ($ace in $securitydescriptor.Access) { $aceName = $securitydescriptor.Owner.split('\')[1] if ($aceName.EndsWith('$')) { $aceName = $aceName.TrimEnd('$') } $aceSID = ConvertFrom-IdentityReference -Object $ace.IdentityReference if ( ($aceSID -notmatch $SafeSIDs) -and ($ace.ActiveDirectoryRights -match $DangerousRights) -and ($securitydescriptor.DistinguishedName -notmatch $aceName) -and ( ($aceSID -notmatch "$EnterpriseKeyAdminsSID|$KeyAdminsSID") -and ($ace.'Object Type' -ne '5b47d60f-6090-40b2-9f37-2a4de88f3063') ) ) { $AddToList = [PSCustomObject]@{ Name = $securitydescriptor.Name 'Identity Reference' = $ace.IdentityReference 'Active Directory Rights' = $ace.ActiveDirectoryRights 'Object Type' = $ace.ObjectType 'Inherited Object Type' = $ace.InheritedObjectType } $FailedSecurityDescriptorACE += $AddToList } } } $FailedSecurityDescriptorACE } function Test-BTSecurityDescriptorOwner { [CmdletBinding()] param ( [Parameter()] [array]$SecurityDescriptors, [Parameter()] [array]$DynamicUpdateServiceAccounts, [Parameter()] [array]$Domains ) if ($null -eq $SecurityDescriptors) { $SecurityDescriptors = Get-BTSecurityDescriptor } if ($null -eq $DynamicUpdateServiceAccounts) { $DynamicUpdateServiceAccounts = Get-BTDynamicUpdateServiceAccount } if ($null -eq $Domains) { $Domains = Get-BTTarget } $FailedSecurityDescriptorOwner = @() $SafeSIDs = 'S-1-5-18' $RootDomain = (Get-ADForest $Domains[0]).RootDomain $EnterpriseAdminsSID = "$((Get-ADDomain $rootDomain).domainSID.Value)-519" $SafeSIDs += "|$EnterpriseAdminsSID" $DomainAdminsSIDs = foreach ($domain in $Domains) { "$((Get-ADDomain $domain).domainSID.Value)-512" } foreach ($sid in $DomainAdminsSIDs) { $SafeSIDs += "|$sid" } foreach ($dynamicupdateserviceaccount in $DynamicUpdateServiceAccounts) { if ( ($dynamicupdateserviceaccount.'Service Account Name' -ne 'Not Configured') -and ($dynamicupdateserviceaccount.'Service Account Domain' -ne 'N/A') ) { $identityreference = "$($dynamicupdateserviceaccount.'Service Account Domain')\$($dynamicupdateserviceaccount.'Service Account Name')" $dynamicupdateserviceaccountSID = ConvertFrom-IdentityReference -Object $identityreference $SafeSIDs += "|$dynamicupdateserviceAccountSID" } } foreach ($securitydescriptor in $SecurityDescriptors) { $owner = $securitydescriptor.Owner $ownerName = $securitydescriptor.Owner.split('\')[1] if ($ownerName.EndsWith('$')) { $ownerName = $ownerName.TrimEnd('$') } $ownerSID = ConvertFrom-IdentityReference -Object $owner if ( ($ownerSID -notmatch $SafeSIDs) -and ($securitydescriptor.DistinguishedName -notmatch $ownerName) ) { $FailedSecurityDescriptorOwner += $securitydescriptor | Select-Object Name, Owner, DistinguishedName } } $FailedSecurityDescriptorOwner } function Test-BTSocketPoolSize { [CmdletBinding()] param ( [Parameter()] [array]$SocketPoolSizes ) if ($null -eq $SocketPoolSizes) { $SocketPoolSizes = Get-BTSocketPoolSize } $FailedSocketPoolSize = @() foreach ($socketpoolsize in $SocketPoolSizes) { if ($socketpoolsize.'Socket Pool Size' -lt 10000) { $FailedSocketPoolSize += $socketpoolsize } } $FailedSocketPoolSize } function Test-BTWildcardRecord { [CmdletBinding()] param ( [Parameter()] [array]$WildcardRecords ) if ($null -eq $WildcardRecords) { $WildcardRecords = Get-BTWildcardRecord } if ($WildcardRecords -eq 1) { $correctType = 'A' } else { $correctType = 'Txt' } $FailedWildcardRecord = @() foreach ($wildcardrecord in $WildcardRecords) { if ($wildcardrecord.'Wildcard Type' -ne $correctType) { $AddToList = [PSCustomObject]@{ 'Domain' = $wildcardrecord.'Domain' 'Wildcard Exists?' = $wildcardrecord.'Wildcard Exists?' 'Current Wildcard Type' = $wildcardrecord.'Wildcard Type' 'Correct Type' = $correctType } } $FailedWildcardRecord += $AddToList } $FailedWildcardRecord } function Test-BTWPADRecord { [CmdletBinding()] param ( [Parameter()] [array]$WPADRecords ) if ($null -eq $WPADRecords) { $WPADRecords = Get-BTWPADRecord } if ($WPADRecords -eq 1) { $correctType = 'A' } else { $correctType = 'Txt' } $FailedWPADRecord = @() foreach ($wpadrecord in $WPADRecords) { if ($wpadrecord.'WPAD Type' -ne $correctType) { $AddToList = [PSCustomObject]@{ 'Domain' = $wpadrecord.'Domain' 'WPAD Exists?' = $wpadrecord.'WPAD Exists?' 'Current WPAD Type' = $wpadrecord.'WPAD Type' 'Correct Type' = $correctType } } $FailedWPADRecord += $AddToList } $FailedWPADRecord } function Test-BTZoneScopeContainer { [CmdletBinding()] param ( [Parameter()] [array]$ZoneScopeContainers ) if ($null -eq $ZoneScopeContainers) { $ZoneScopeContainers = Get-BTZoneScopeContainer } $FailedZoneScopeContainerList = @() foreach ($zoneScopeContainer in $ZoneScopeContainers) { if ( (Get-ADObject -Filter * -SearchBase $zoneScopeContainer.'Zone Scope Container DN' -Server $zoneScopeContainer.Domain).Count -gt 0) { break } else { $AddToList = [PSCustomObject]@{ 'Domain' = $zoneScopeContainer.Domain 'Zone Name' = $zoneScopeContainer.'Zone Name' 'Zone Type' = $zoneScopeContainer.'Zone Scope Container DN' } } $FailedZoneScopeContainerList += $AddToList } $FailedZoneScopeContainerList } function ConvertFrom-IdentityReference { [CmdletBinding()] param( [Parameter(Mandatory = $true)] $Object ) $Principal = New-Object System.Security.Principal.NTAccount($Object) if ($Principal -match '^(S-1|O:)') { $SID = $Principal } else { $SID = ($Principal.Translate([System.Security.Principal.SecurityIdentifier])).Value } return $SID } function ConvertTo-IdentityReference { [CmdletBinding()] param( [Parameter(Mandatory = $true)] $SID ) $Principal = New-Object System.Security.Principal.SecurityIdentifier($SID) $IdentityReference = $Principal.Translate([System.Security.Principal.NTAccount]).Value $IdentityReference } function Export-Results { <# .SYNOPSIS Export the results to text files. .DESCRIPTION Export the results to text files. .PARAMETER Name Name of the file. .PARAMETER Data Data that will be exported to a file. .PARAMETER FilePath Path in which the file will be created. .EXAMPLE Export-Results -Name "Tested $item" -Data $TestedData.$Item #> [Cmdletbinding()] param ( [string]$Name, $Data, [string]$FilePath = (Join-Path -Path $pwd -ChildPath "BlueTuxedo $Name $(Get-Date -f 'yyyyMMddhhmmss').txt") ) Out-File -FilePath $FilePath -Encoding utf8 -InputObject $Data } function Invoke-BlueTuxedo { [CmdletBinding()] param ( [string]$Forest = (Get-ADForest).Name, [string]$InputPath, [switch]$ShowSecurityDescriptors = $false, [string[]]$Exclude, [switch]$ExportCollectedData = $false, [switch]$ExportTestedData = $false, [switch]$Demo = $false ) if ($Demo) { Clear-Host } Show-BTLogo -Version '2024.10' $Domains = Get-BTTarget -Forest $Forest -InputPath $InputPath #region Get Data Write-Host 'Please hold. Collecting DNS data from the following domains:' -ForegroundColor Green Write-Host $Domains.split(' ') -ForegroundColor Yellow Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting ADI Zones" -Verbose $ADIZones = Get-BTADIZone -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Conditional Forwarders" -Verbose $ConditionalForwarders = Get-BTConditionalForwarder -Domains $Domains -Exclude $Exclude Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Dangling SPNs" -Verbose $DanglingSPNs = Get-BTDanglingSPN -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting DNS Admins Memberships" -Verbose $DnsAdminsMemberships = Get-BTDnsAdminsMembership -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting DNS Update Proxy Memberships" -Verbose $DnsUpdateProxyMemberships = Get-BTDnsUpdateProxyMembership -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Dynamic Update Service Accounts" -Verbose $DynamicUpdateServiceAccounts = Get-BTDynamicUpdateServiceAccount -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Forwarder Configuration" -Verbose $ForwarderConfigurations = Get-BTForwarderConfiguration -Domains $Domains -Exclude $Exclude Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Global Query Blocklists" -Verbose $GlobalQueryBlockLists = Get-BTGlobalQueryBlockList -Domains $Domains -Exclude $Exclude # Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Name Protection Configuration Lists" -Verbose # $NameProtectionConfigurationLists = Get-BTNameProtectionConfiguration -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Non ADI Zones" -Verbose $NonADIZones = Get-BTNonADIZone -Domains $Domains -Exclude $Exclude Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Query Resolution Policies" -Verbose $QueryResolutionPolicys = Get-BTQueryResolutionPolicy -Domains $Domains -Exclude $Exclude # Write-Verbose "[$(Get-Date -format 'yyyy-MM-dd hh:mm:ss')] Collecting Security Descriptors" -Verbose # $SecurityDescriptors = Get-BTSecurityDescriptor -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Socket Pool Sizes" -Verbose $SocketPoolSizes = Get-BTSocketPoolSize -Domains $Domains -Exclude $Exclude Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Tombstoned Nodes" -Verbose $TombstonedNodes = Get-BTTombstonedNode -Domains $Domains -Exclude $Exclude Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Wildcard Records" -Verbose $WildcardRecords = Get-BTWildcardRecord -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting WPAD Records" -Verbose $WPADRecords = Get-BTWPADRecord -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Zone Scopes" -Verbose $ZoneScopes = Get-BTZoneScope -Domains $Domains -Exclude $Exclude Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Collecting Zone Scope Containers" -Verbose $ZoneScopeContainers = Get-BTZoneScopeContainer -ADIZones $ADIZones Write-Host 'Finished collecting DNS data from the following domains:' -ForegroundColor Green Write-Host $Domains.split(' ')-ForegroundColor Yellow $CollectedData = [ordered]@{ 'ADIZones' = $ADIZones 'ConditionalForwarders' = $ConditionalForwarders 'DanglingSPNs' = $DanglingSPNs 'DnsAdminsMemberships' = $DnsAdminsMemberships 'DnsUpdateProxyMemberships' = $DnsUpdateProxyMemberships 'DynamicUpdateServiceAccounts' = $DynamicUpdateServiceAccounts 'ForwarderConfigurations' = $ForwarderConfigurations 'GlobalQueryBlockLists' = $GlobalQueryBlockLists # 'NameProtectionLists' = $NameProtectionConfigurationLists 'NonADIZones' = $NonADIZones 'QueryResolutionPolicys' = $QueryResolutionPolicys # 'SecurityDescriptors' = $SecurityDescriptors 'SocketPoolSizes' = $SocketPoolSizes 'TombstonedNodes' = $TombstonedNodes 'WildcardRecords' = $WildcardRecords 'WPADRecords' = $WPADRecords 'ZoneScopes' = $ZoneScopes 'ZoneScopeContainers' = $ZoneScopeContainers } #endregion Get Data # Export the collected data to an individual file for each test if ($ExportCollectedData) { foreach ($item in $CollectedData.Keys) { if ($CollectedData.$Item -and $CollectedData.item.ToString().Length -gt 0) { Export-Results -Name "Collected $item" -Data $($CollectedData.$Item) } } } # Display All Collected Data $show = Read-Host 'Show all collected DNS data? [Y]/n' if (($show -eq 'y') -or ($show -eq '') -or ($null -eq $show) ) { if ($Demo) { Show-BTCollectedData -Demo @CollectedData } elseif ($ShowSecurityDescriptors) { Show-BTCollectedData -ShowSecurityDescriptors @CollectedData } elseif ($Demo -and $ShowSecurityDescriptors) { Show-BTCollectedData -ShowSecurityDescriptors -Demo @CollectedData } else { Show-BTCollectedData @CollectedData } } # Test Data if ($Demo) { Clear-Host } Write-Host 'Currently testing collected DNS data to identify possible issues...' -ForegroundColor Green Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing ADI Legacy Zones" $TestedADILegacyZones = Test-BTADILegacyZone -ADIZones $ADIZones Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing ADI Insecure Update Zones" $TestedADIInsecureUpdateZones = Test-BTADIInsecureUpdateZone -ADIZones $ADIZones Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing Dynamic Update Service Accounts" $TestedDynamicUpdateServiceAccounts = Test-BTDynamicUpdateServiceAccount -DynamicUpdateServiceAccounts $DynamicUpdateServiceAccounts Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing Forwarder Configurations" $TestedForwarderConfigurations = Test-BTForwarderConfiguration -ForwarderConfigurations $ForwarderConfigurations Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing Global Query Block Lists" $TestedGlobalQueryBlockLists = Test-BTGlobalQueryBlockList -GlobalQueryBlockLists $GlobalQueryBlockLists # Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing Security Descriptor ACE" # $TestedSecurityDescriptorACEs = Test-BTSecurityDescriptorACE -SecurityDescriptors $SecurityDescriptors -DynamicUpdateServiceAccounts $DynamicUpdateServiceAccounts -Domains $Domains # Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing Security Descriptor Owner" # $TestedSecurityDescriptorOwners = Test-BTSecurityDescriptorOwner -SecurityDescriptors $SecurityDescriptors -DynamicUpdateServiceAccounts $DynamicUpdateServiceAccounts -Domains $Domains Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing Socket Pool Sizes" $TestedSocketPoolSizes = Test-BTSocketPoolSize -SocketPoolSizes $SocketPoolSizes Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing Wildcard Records" $TestedWildcardRecords = Test-BTWildcardRecord -WildcardRecords $WildcardRecords Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing WPAD Records" $TestedWPADRecords = Test-BTWPADRecord -WPADRecords $WPADRecords Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Testing Zone Scope Containers" $TestedZoneScopeContainers = Test-BTZoneScopeContainer -ZoneScopeContainers $ZoneScopeContainers Write-Host "Finished testing collected DNS data to identify possible issues.`n" -ForegroundColor Green $TestedData = [ordered]@{ 'ConditionalForwarders' = $ConditionalForwarders 'DanglingSPNs' = $DanglingSPNs 'DnsAdminsMemberships' = $DnsAdminsMemberships 'DnsUpdateProxyMemberships' = $DnsUpdateProxyMemberships 'NonADIZones' = $NonADIZones 'QueryResolutionPolicys' = $QueryResolutionPolicys 'TombstonedNodes' = $TombstonedNodes 'ZoneScopes' = $ZoneScopes 'TestedADILegacyZones' = $TestedADILegacyZones 'TestedADIInsecureUpdateZones' = $TestedADIInsecureUpdateZones 'TestedDynamicUpdateServiceAccounts' = $TestedDynamicUpdateServiceAccounts 'TestedForwarderConfigurations' = $TestedForwarderConfigurations 'TestedGlobalQueryBlockLists' = $TestedGlobalQueryBlockLists 'TestedSecurityDescriptorACEs' = $TestedSecurityDescriptorACEs 'TestedSecurityDescriptorOwners' = $TestedSecurityDescriptorOwners 'TestedSocketPoolSizes' = $TestedSocketPoolSizes 'TestedWildcardRecords' = $TestedWildcardRecords 'TestedWPADRecords' = $TestedWPADRecords 'TestedZoneScopeContainers' = $TestedZoneScopeContainers } # Export the tested data to individual files for each test if ($ExportTestedData) { foreach ($item in $TestedData.Keys) { if ($TestedData.$Item -and $TestedData.item.ToString().Length -gt 0) { Export-Results -Name "Tested $item" -Data $TestedData.$Item } } } # Display All Tested Data $show = Read-Host 'Show possible DNS issues in the environment? [Y]/n' if (($show -eq 'y') -or ($show -eq '') -or ($null -eq $show) ) { if ($Demo) { Show-BTTestedData -Demo @TestedData } elseif ($ShowSecurityDescriptors) { Show-BTTestedData -ShowSecurityDescriptors @TestedData } elseif ($Demo -and $ShowSecurityDescriptors) { Show-BTTestedData -ShowSecurityDescriptors -Demo @TestedData } else { Show-BTTestedData @TestedData } } # Display Fixes $show = Read-Host 'Show fixes for identified issues? [Y]/n' if (($show -eq 'y') -or ($show -eq '') -or ($null -eq $show) ) { if ($Demo) { Show-BTFixes -Demo @TestedData } elseif ($ShowSecurityDescriptors) { Show-BTFixes -ShowSecurityDescriptors @TestedData } elseif ($Demo -and $ShowSecurityDescriptors) { Show-BTFixes -ShowSecurityDescriptors -Demo @TestedData } else { Show-BTFixes @TestedData } } # Return the TestedData Write-Host 'Tested Data: ' return $TestedData } # Export functions and aliases as required Export-ModuleMember -Function @('Invoke-BlueTuxedo') -Alias @() |