PowerShell

Finding GPOs missing permissions that may prevent GPOs from working correctly

I've been in IT for a long time now. I've made my fair share of mistakes and misconfigurations. One of those misconfigurations was removing Authenticated Users from Security filtering in Group Policy Objects. While it worked fine at some point, Microsoft rolled out a Hotfix MS16-07 on June 14th, 2016.

MS16-072 changes the security context with which user group policies are retrieved. This by-design behavior change protects customers’ computers from a security vulnerability. Before MS16-072 is installed, user group policies were retrieved by using the user’s security context. After MS16-072 is installed, user group policies are retrieved by using the computer's security context.

This means that if you have ever removed Authenticated Users from Security Filtering in GPO in a wrong way your GPO may not work anymore.

To resolve this issue, use the Group Policy Management Console (GPMC.MSC) and follow one of the following steps:

  • Add the Authenticated Users group with Read Permissions on the Group Policy Object (GPO).

  • If you are using security filtering, add the Domain Computers group with read permission.

This means that if we want to remove Authenticated Users from Security Filtering, we should do it on the Delegation Tab.

After switching to delegation tab, choose Advanced, find Authenticated Users and remove checkbox from Apply group policy. After doing so Authenticated Users will be removed from Security filtering.

Get-WinADGPOMissingPermissions function for the rescue

If you've always worked this way, and your colleagues also did the same, you've nothing to worry about. However, it's still good to test things that are what they seem. Even today, almost four years after that patch, I find Domains missing permissions, so a lot of Domain Admins are not aware of this requirement. And if you have lots of GPO's things like missing permissions are usually last on the list to check. Therefore I've prepared a function that automates this process for me. While it may seem like there's a lot of code this is because I often use additional functions for other purposes and reuse them when I can.

$MissingPermissions = Get-WinADGPOMissingPermissions -Mode Either
$MissingPermissions | Format-Table -AutoSize

The function returns only GPO's that are missing required permissions. It has three modes – AuthenticatedUsers, DomainComputers, Either. By default, it scans the whole forest and all domains within it. But you can also target only specific Domain. Once the function outputs “broken GPOs” you need to go ahead and fix them. Keep in mind that if that GPO had broken permissions, it's possible it wasn't working. Now that you set it and it starts working, it may apply GPO you didn't expect to apply. Make sure to review GPO's while you fix them.

Portable version

This function requires Administrative privileges (Domain Admin or similar) because it needs to see permissions. If you removed Authenticated Users from a GPO, you wouldn't be able to see this GPO as a standard user. You also need to have ActiveDirectory and GPO module installed from RSAT. That's a standard for Domain Admins.

function Get-WinADGPOMissingPermissions { 
    <#
    .SYNOPSIS
    Short description

    .DESCRIPTION
    Long description

    .PARAMETER Domain
    Parameter description

    .EXAMPLE
    An example

    .NOTES
    Based on https://secureinfra.blog/2018/12/31/most-common-mistakes-in-active-directory-and-domain-services-part-1/
    #>
    [cmdletBinding()]
    param([alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $SkipRODC,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [validateset('AuthenticatedUsers', 'DomainComputers', 'Either')][string] $Mode = 'Either',
        [switch] $Extended)
    if (-not $ExtendedForestInformation) { $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC } else { $ForestInformation = $ExtendedForestInformation }
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        if ($Extended) {
            $GPOs = Get-GPO -All -Domain $Domain -Server $QueryServer | ForEach-Object { [xml] $XMLContent = Get-GPOReport -ID $_.ID.Guid -ReportType XML -Server $ForestInformation.QueryServers[$Domain].HostName[0] -Domain $Domain
                Add-Member -InputObject $_ -MemberType NoteProperty -Name 'LinksTo' -Value $XMLContent.GPO.LinksTo
                if ($XMLContent.GPO.LinksTo) { Add-Member -InputObject $_ -MemberType NoteProperty -Name 'Linked' -Value $true } else { Add-Member -InputObject $_ -MemberType NoteProperty -Name 'Linked' -Value $false }
                $_ | Select-Object -Property Id, DisplayName, DomainName, Owner, Linked, GpoStatus, CreationTime, ModificationTime,
                @{label        = 'UserVersion'
                    expression = { "AD Version: $($_.User.DSVersion), SysVol Version: $($_.User.SysvolVersion)" }
                },
                @{label        = 'ComputerVersion'
                    expression = { "AD Version: $($_.Computer.DSVersion), SysVol Version: $($_.Computer.SysvolVersion)" }
                }, WmiFilter, Description, User, Computer, LinksTo }
        } else { $GPOs = Get-GPO -All -Domain $Domain -Server $QueryServer }
        $DomainInformation = Get-ADDomain -Server $QueryServer
        $DomainComputersSID = $('{0}-515' -f $DomainInformation.DomainSID.Value)
        $MissingPermissions = @(foreach ($GPO in $GPOs) {
                $Permissions = Get-GPPermission -Guid $GPO.Id -All -Server $QueryServer -DomainName $Domain | Select-Object -ExpandProperty Trustee
                if ($Mode -eq 'Either' -or $Mode -eq 'AuthenticatedUsers') { $GPOPermissionForAuthUsers = $Permissions | Where-Object { $_.Sid.Value -eq 'S-1-5-11' } }
                if ($Mode -eq 'Either' -or $Mode -eq 'DomainComputers') { $GPOPermissionForDomainComputers = $Permissions | Where-Object { $_.Sid.Value -eq $DomainComputersSID } }
                if ($Mode -eq 'Either') { If (-not $GPOPermissionForAuthUsers -and -not $GPOPermissionForDomainComputers) { $GPO } } elseif ($Mode -eq 'AuthenticatedUsers') { If (-not $GPOPermissionForAuthUsers) { $GPO } } elseif ($Mode -eq 'DomainComputers') { If (-not $GPOPermissionForDomainComputers) { $GPO } }
            })
        $MissingPermissions
    }
 }
function Get-WinADForestDetails { 
    [CmdletBinding()]
    param([alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [string] $Filter = '*',
        [switch] $TestAvailability,
        [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All',
        [int[]] $Ports = 135,
        [int] $PortsTimeout = 100,
        [int] $PingCount = 1)
    if ($Global:ProgressPreference -ne 'SilentlyContinue') {
        $TemporaryProgress = $Global:ProgressPreference
        $Global:ProgressPreference = 'SilentlyContinue'
    }
    $Findings = [ordered] @{ }
    try { if ($Forest) { $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest } else { $ForestInformation = Get-ADForest -ErrorAction Stop } } catch {
        Write-Warning "Get-WinADForestDetails - Error discovering DC for Forest - $($_.Exception.Message)"
        return
    }
    if (-not $ForestInformation) { return }
    $Findings['Forest'] = $ForestInformation
    $Findings['ForestDomainControllers'] = @()
    $Findings['QueryServers'] = @{ }
    $Findings['QueryServers']['Forest'] = $DC
    $Findings.Domains = foreach ($_ in $ForestInformation.Domains) {
        if ($IncludeDomains) {
            if ($_ -in $IncludeDomains) { $_.ToLower() }
            continue
        }
        if ($_ -notin $ExcludeDomains) { $_.ToLower() }
    }
    $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) {
        try { $DC = Get-ADDomainController -DomainName $Domain -Discover -ErrorAction Stop } catch {
            Write-Warning "Get-WinADForestDetails - Error discovering DC for domain $Domain - $($_.Exception.Message)"
            continue
        }
        $Findings['QueryServers']["$Domain"] = $DC
        [Array] $AllDC = try {
            try { $DomainControllers = Get-ADDomainController -Filter $Filter -Server $DC.HostName[0] -ErrorAction Stop } catch {
                Write-Warning "Get-WinADForestDetails - Error listing DCs for domain $Domain - $($_.Exception.Message)"
                continue
            }
            foreach ($S in $DomainControllers) {
                if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } }
                if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } }
                $Server = [ordered] @{Domain = $Domain
                    HostName                 = $S.HostName
                    Name                     = $S.Name
                    Forest                   = $ForestInformation.RootDomain
                    Site                     = $S.Site
                    IPV4Address              = $S.IPV4Address
                    IPV6Address              = $S.IPV6Address
                    IsGlobalCatalog          = $S.IsGlobalCatalog
                    IsReadOnly               = $S.IsReadOnly
                    IsSchemaMaster           = ($S.OperationMasterRoles -contains 'SchemaMaster')
                    IsDomainNamingMaster     = ($S.OperationMasterRoles -contains 'DomainNamingMaster')
                    IsPDC                    = ($S.OperationMasterRoles -contains 'PDCEmulator')
                    IsRIDMaster              = ($S.OperationMasterRoles -contains 'RIDMaster')
                    IsInfrastructureMaster   = ($S.OperationMasterRoles -contains 'InfrastructureMaster')
                    OperatingSystem          = $S.OperatingSystem
                    OperatingSystemVersion   = $S.OperatingSystemVersion
                    OperatingSystemLong      = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion
                    LdapPort                 = $S.LdapPort
                    SslPort                  = $S.SslPort
                    DistinguishedName        = $S.ComputerObjectDN
                    Pingable                 = $null
                    WinRM                    = $null
                    PortOpen                 = $null
                    Comment                  = ''
                }
                if ($TestAvailability) {
                    if ($Test -eq 'All' -or $Test -like 'Ping*') { $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount }
                    if ($Test -eq 'All' -or $Test -like '*WinRM*') { $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status }
                    if ($Test -eq 'All' -or '*PortOpen*') { $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status }
                }
                [PSCustomObject] $Server
            }
        } catch {
            [PSCustomObject]@{Domain     = $Domain
                HostName                 = ''
                Name                     = ''
                Forest                   = $ForestInformation.RootDomain
                IPV4Address              = ''
                IPV6Address              = ''
                IsGlobalCatalog          = ''
                IsReadOnly               = ''
                Site                     = ''
                SchemaMaster             = $false
                DomainNamingMasterMaster = $false
                PDCEmulator              = $false
                RIDMaster                = $false
                InfrastructureMaster     = $false
                LdapPort                 = ''
                SslPort                  = ''
                DistinguishedName        = ''
                Pingable                 = $null
                WinRM                    = $null
                PortOpen                 = $null
                Comment                  = $_.Exception.Message -replace "`n", " " -replace "`r", " "
            }
        }
        if ($SkipRODC) { $Findings[$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { $Findings[$Domain] = $AllDC }
        $Findings[$Domain]
    }
    if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress }
    $Findings
 }
function ConvertTo-OperatingSystem { 
    [CmdletBinding()]
    param([string] $OperatingSystem,
        [string] $OperatingSystemVersion)
    if ($OperatingSystem -like '*Windows 10*') {
        $Systems = @{'10.0 (18363)' = "Windows 10 1909"
            '10.0 (18362)'          = "Windows 10 1903"
            '10.0 (17763)'          = "Windows 10 1809"
            '10.0 (17134)'          = "Windows 10 1803"
            '10.0 (16299)'          = "Windows 10 1709"
            '10.0 (15063)'          = "Windows 10 1703"
            '10.0 (14393)'          = "Windows 10 1607"
            '10.0 (10586)'          = "Windows 10 1511"
            '10.0 (10240)'          = "Windows 10 1507"
            '10.0 (18898)'          = 'Windows 10 Insider Preview'
            '10.0.18363'            = "Windows 10 1909"
            '10.0.18362'            = "Windows 10 1903"
            '10.0.17763'            = "Windows 10 1809"
            '10.0.17134'            = "Windows 10 1803"
            '10.0.16299'            = "Windows 10 1709"
            '10.0.15063'            = "Windows 10 1703"
            '10.0.14393'            = "Windows 10 1607"
            '10.0.10586'            = "Windows 10 1511"
            '10.0.10240'            = "Windows 10 1507"
            '10.0.18898'            = 'Windows 10 Insider Preview'
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) { $System = $OperatingSystem }
    } elseif ($OperatingSystem -like '*Windows Server*') {
        $Systems = @{'5.2 (3790)' = 'Windows Server 2003'
            '6.1 (7601)'          = 'Windows Server 2008 R2'
            '10.0 (18362)'        = "Windows Server, version 1903 (Semi-Annual Channel) 1903"
            '10.0 (17763)'        = "Windows Server 2019 (Long-Term Servicing Channel) 1809"
            '10.0 (17134)'        = "Windows Server, version 1803 (Semi-Annual Channel) 1803"
            '10.0 (14393)'        = "Windows Server 2016 (Long-Term Servicing Channel) 1607"
            '10.0.18362'          = "Windows Server, version 1903 (Semi-Annual Channel) 1903"
            '10.0.17763'          = "Windows Server 2019 (Long-Term Servicing Channel) 1809"
            '10.0.17134'          = "Windows Server, version 1803 (Semi-Annual Channel) 1803"
            '10.0.14393'          = "Windows Server 2016 (Long-Term Servicing Channel) 1607"
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) { $System = $OperatingSystem }
    } else { $System = $OperatingSystem }
    if ($System) { $System } else { 'Unknown' }
 }
function Test-ComputerPort { 
    [CmdletBinding()]
    param ([alias('Server')][string[]] $ComputerName,
        [int[]] $PortTCP,
        [int[]] $PortUDP,
        [int]$Timeout = 5000)
    begin {
        if ($Global:ProgressPreference -ne 'SilentlyContinue') {
            $TemporaryProgress = $Global:ProgressPreference
            $Global:ProgressPreference = 'SilentlyContinue'
        }
    }
    process {
        foreach ($Computer in $ComputerName) {
            foreach ($P in $PortTCP) {
                $Output = [ordered] @{'ComputerName' = $Computer
                    'Port'                           = $P
                    'Protocol'                       = 'TCP'
                    'Status'                         = $null
                    'Summary'                        = $null
                    'Response'                       = $null
                }
                $TcpClient = Test-NetConnection -ComputerName $Computer -Port $P -InformationLevel Detailed -WarningAction SilentlyContinue
                if ($TcpClient.TcpTestSucceeded) {
                    $Output['Status'] = $TcpClient.TcpTestSucceeded
                    $Output['Summary'] = "TCP $P Successful"
                } else {
                    $Output['Status'] = $false
                    $Output['Summary'] = "TCP $P Failed"
                    $Output['Response'] = $Warnings
                }
                [PSCustomObject]$Output
            }
            foreach ($P in $PortUDP) {
                $Output = [ordered] @{'ComputerName' = $Computer
                    'Port'                           = $P
                    'Protocol'                       = 'UDP'
                    'Status'                         = $null
                    'Summary'                        = $null
                }
                $UdpClient = [System.Net.Sockets.UdpClient]::new($Computer, $P)
                $UdpClient.Client.ReceiveTimeout = $Timeout
                $Encoding = [System.Text.ASCIIEncoding]::new()
                $byte = $Encoding.GetBytes("Evotec")
                [void]$UdpClient.Send($byte, $byte.length)
                $RemoteEndpoint = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
                try {
                    $Bytes = $UdpClient.Receive([ref]$RemoteEndpoint)
                    [string]$Data = $Encoding.GetString($Bytes)
                    If ($Data) {
                        $Output['Status'] = $true
                        $Output['Summary'] = "UDP $P Successful"
                        $Output['Response'] = $Data
                    }
                } catch {
                    $Output['Status'] = $false
                    $Output['Summary'] = "UDP $P Failed"
                    $Output['Response'] = $_.Exception.Message
                }
                $UdpClient.Close()
                $UdpClient.Dispose()
                [PSCustomObject]$Output
            }
        }
    }
    end { if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } }
 }
function Test-WinRM { 
    [CmdletBinding()]
    param ([alias('Server')][string[]] $ComputerName)
    $Output = foreach ($Computer in $ComputerName) {
        $Test = [PSCustomObject] @{Output = $null
            Status                        = $null
            ComputerName                  = $Computer
        }
        try {
            $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop
            $Test.Status = $true
        } catch { $Test.Status = $false }
        $Test
    }
    $Output
 }
$MissingPermissions = Get-WinADGPOMissingPermissions -Mode Either
$MissingPermissions | Format-Table -AutoSize

This post was last modified on May 17, 2022 15:15

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Upgrade Azure Active Directory Connect fails with unexpected error

Today, I made the decision to upgrade my test environment and update the version of…

5 days ago

Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster

Have you ever looked at your Active Directory and wondered, "Why do I still have…

4 months ago

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

8 months ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

1 year ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

1 year ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

1 year ago