Script to report on and remediate the Group Policy security change in MS16-072

by Jeremy Saunders on June 22, 2016

Computer can read again!

On June 14th 2016 Microsoft released security update MS16-072 under KB3163622 that changes the behavior of Group Policy processing so that user group policies are now retrieved by using the machine’s security context instead of the user’s security context. This is a by-design behavior change from Microsoft to protect computers from a security vulnerability.

Update 23/06/2016: Microsoft finally released an official response to this patch via the Directory Services team: Deploying Group Policy Security Update MS16-072 \ KB3163622

This is a problem for people that implement security filtering on their Group Policy Objects (GPOs), as it removes the default Authenticated Users group not only from the “Apply group policy” permission, but also from the “Read” permission.

I found a great explanation of Authenticated Users in an old article here:

“But who exactly are Authenticated Users? The membership of this special identity is all security principals that have been authenticated by Active Directory. In other words, Authenticated Users includes all domain user accounts and computer accounts that have been authenticated by a domain controller on the network. So what this means is that by default the settings in a GPO apply to all user and computer accounts residing in the container linked to the GPO.”

So therefore Domain Computers will no longer have the rights to read a Group Policy Object (GPO). This has not necessarily been a problem…until now!

The KB article says that to fix it you can do one of two things:

  • 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 permissions on the Group Policy Object (GPO).

Update 23/06/2016: This is quite confusing as there is no clear instruction here. Which way do you go? After thinking about this further and taking in some of the commentary from various experts in this field, my default option on this is to add Domain Computers with Read permissions instead of Authenticated Users. This is viewed as the minimum required permissions and therefore most secure. You can indeed place computers into groups and apply those groups with Read permissions, but that’s starting to overcomplicate things, and means that you need an overall process to manage those groups.

I wanted a script that could first audit all GPOs that contained user settings and output that to a CSV file, which I could then attached to a change record. Don’t forget about Change Management! I then wanted the same script to perform the fix. By the time I got around to this, the awesome Darren Mar-Elia has produced a blog article and script: Group Policy Patch MS16-072– “Breaks” GP Processing Behavior

Darren also referenced an article from Ian Farr at Microsoft who also put together a script: MS16-072 – Known Issue – Use PowerShell to Check GPOs

Jeremy Moskowitz also put some information together in a blog article: Never a dull moment with Group Policy (or what to do about MS16-072)

The scripts and methods were not 100% what I was looking for. So I took the scripts and comments on their articles and enhanced Darren’s script to provide the output and process that I was after for all my customers.

Download the script and do the following:

  • Run the script with no parameters (in report only mode) and it will report on what GPOs with user settings are missing the Authenticated Users and/or Domain Computers Read permissions.
  • Open the CSV output file in Excel and filter the AuthUserRead for False and DomainComputersRead for False. This will leave you with a list of GPOs that need to be remediated.
  • Manually validate the output against some GPOs to ensure the script output is giving you valid information.
  • Create a Change Record and go through the approval process.
  • Decide if you want to give Domain Computers or Authenticated Users Read Permissions.
    • If Domain Computers, then run with the script using the -Action parameter to fix the GPOs that require remediation.
    • If Authenticated Users, then run with the script using the -Action -AuthUsers parameters to fix the GPOs that require remediation.
  • Run the script again with no parameters (in report only mode) to verify that there are no outstanding GPOs that require further remediation.
  • Complete the Change Record. Got to keep the Change Manager on side!

Update 02/07/2016: There have been several requests to make this script run against multiple domains so I have added a -Domains parameter. Here you can add 1 or more domains using the domains Fully Qualified Domain Name (FQDN) separated by a comma.

Update 30/07/2016: Fixed an issue when running against multiple domains where the script wasn’t using the “remote” domain name when assessing and setting the permissions. Thanks for the awesome feedback and testing by Trevor Bruss.

You don’t want to be concerned with setting these permissions each time a GPO is created, so you want to ensure that Domain Computers is set as a default Read permission. This is controlled by the defaultSecurityDescriptor attribute on the Group-Policy-Container schema class object:

Update 24/06/2016: Thanks to Greg Onstot for finding a bug with the screen output at the end of the script. It would report that “No GPOs require modification” if the $ToBeFixed value was false. Greg provided an update to the script to address this that I have implemented into version 1.4. In my testing across a couple of different customer environments it was always giving me the correct output because the last GPO processed needed to be fixed.

Here is the AddGPOReadPerms.ps1 (2029 downloads)  script:

<#
  Microsoft rolled out security update MS16-072 that changes the behaviour of
  Group Policy processing to address a security vulnerability. This script will
  allow you to audit your current state and address this by adding either the
  Authenticated Users or Domain Computers group the required Read permissions.

  The official response from Microsoft:
  - Deploying Group Policy Security Update MS16-072 \ KB3163622
    https://blogs.technet.microsoft.com/askds/2016/06/22/deploying-group-policy-security-update-ms16-072-kb3163622/

  The original script was released by Darren Mar-Elia as part of a blog article:
  - Group Policy Patch MS16-072– "Breaks" GP Processing Behavior
    
New Group Policy Patch MS16-072– “Breaks” GP Processing Behavior
Have also taken ideas from Ian Farr at Microsoft in the following blog article and script: - MS16-072 – Known Issue – Use PowerShell to Check GPOs
MS16-072 – Known Issue – Use PowerShell to Check GPOs
Jeremy Moskowitz also put some information together in a blog article: - Never a dull moment with Group Policy (or what to do about MS16-072) http://www.gpanswers.com/never-a-dull-moment-with-group-policy-or-what-to-do-about-ms16-072/ The script has been modified to allow for: - It defaults to report only mode so that you can: a) Determine how wide spread the issue may be. b) List the GPOs that need to be changed for the Change Management purposes. - It will write to a CSV file that can be openned with Excel. - Feedback from Erik de Vries via Darren's article so that it works across different OSs. - Runs on non-English systems as commented by Darren for Domain Computers. - As well as the CSV output, it also writes to a transcription log. - Detects the version of Windows you are using so it calls right cmdlets: - Get-GPPermission v Get-GPPermissions - Set-GPPermission v Set-GPPermissions - It gives you the option of applying either Domain Computers or Authenticated Users with Read permissions; depending on your strategy. Domain Computers being the preferred option and is what the script will default to. - Requires a minimum of PowerShell 2.0 - Feedback from Jean-Pierre Paradis to include Authenticated Users for non-English systems. - Improved screen output at start and finish to remove confusion. - Process against multiple domains as specified by the -Domains parameter. - Feedback from Trevor Bruss to fix an issue with the multiple domain environment. Syntax examples: - To execute the script in report only mode and process only the GPOs that have user settings: AddGPOReadPerms.ps1 - To execute the script in report only mode against multiple domains and process only the GPOs that have user settings: AddGPOReadPerms.ps1 -Domains:"corp.tailspintoys.com,corp.contoso.com,staff.adventure-works.com" - To execute the script in report only mode against all GPOs: AddGPOReadPerms.ps1 -All - To execute the script and take action against GPOs that have user settings adding Domain Computers with Read permissions: AddGPOReadPerms.ps1 -Action - To execute the script and take action against GPOs that have user settings adding Authenticated Users with Read permissions instead of Domain Computers: AddGPOReadPerms.ps1 -Action -AuthUsers - To execute the script and take action against all GPOs adding Domain Computers with Read permissions: AddGPOReadPerms.ps1 -Action -All - To execute the script and take action against all GPOs adding Authenticated Users with Read permissions instead of Domain Computers: AddGPOReadPerms.ps1 -Action -All -AuthUsers Script name: AddGPOReadPerms.ps1 Release 1.8 Written by Darren Mar-Elia (darren@sdmsoftware.com) 15th June 2016 Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 30th July 2016 #> #------------------------------------------------------------- param( [string]$Domains="", [switch]$All, [switch]$Action, [switch]$AuthUsers ) # Set Powershell Compatibility Mode Set-StrictMode -Version 2.0 #------------------------------------------------------------- # Use PowerShell to Find Operating System Version # You can use "[System.Environment]::OSVersion.Version", or the PInvoke method as per the following Scripting Guy article: # http://blogs.technet.com/b/heyscriptingguy/archive/2014/04/25/use-powershell-to-find-operating-system-version.aspx Function Get-OSVersion { $signature = @" [DllImport("kernel32.dll")] public static extern uint GetVersion(); "@ Add-Type -MemberDefinition $signature -Name "Win32OSVersion" -Namespace Win32Functions -PassThru } $os = [System.BitConverter]::GetBytes((Get-OSVersion)::GetVersion()) $majorVersion = $os[0] $minorVersion = $os[1] $build = [byte]$os[2],[byte]$os[3] $buildNumber = [System.BitConverter]::ToInt16($build,0) [float]$OSVersion = $majorVersion.ToString() + "." + $minorVersion.ToString() #------------------------------------------------------------- $invalidChars = [io.path]::GetInvalidFileNamechars() $datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-") # Get the script path $ScriptPath = {Split-Path $MyInvocation.ScriptName} $ScriptName = [System.IO.Path]::GetFilenameWithoutExtension($MyInvocation.MyCommand.Path.ToString()) $Logfile = "$ScriptName-$($datestampforfilename).log" $logPath = $(&$ScriptPath) try { Start-Transcript "$logPath\$logFile" } catch { Write-Verbose "This host does not support transcription" -Verbose } #------------------------------------------------------------- # Import the Active Directory Module Import-Module ActiveDirectory -WarningAction SilentlyContinue # Import the Group Policy Module Import-Module GroupPolicy -WarningAction SilentlyContinue #------------------------------------------------------------- $DomainsToProcess = @() if ([String]::IsNullOrEmpty($Domains) -OR $Domains.Trim() -eq "") { $DomainsToProcess += (get-addomain).DNSroot } Else { $DomainsToProcess += $Domains.split(" ") } Write-Verbose "Processing $(($DomainsToProcess | Measure-Object).Count) Domains in total." -Verbose ForEach ($Domain in $DomainsToProcess) { $GPOsProcessed = 0 $GPOsNoAuthUser = 0 Try { # Get the Domain SID. $DomainSID = (Get-ADDomain $Domain).DomainSID # Get the Domain NetBIOS name. $DomainNetBIOS = (Get-ADDomain $Domain).NetBIOSName $IsValidDomain = $True } Catch { $IsValidDomain = $False } If ($IsValidDomain) { # Get the Domain Computers group support for non-English systems. $DomainComputersName = $DomainNetBIOS + "\" + (Get-ADGroup -server $Domain "$($DomainSID)-515").Name # Get the Authenticated Users group support for non-English systems. $AuthUserName = (([System.Security.Principal.SecurityIdentifier]"S-1-5-11").Translate([System.Security.Principal.NTAccount]).Value) $GPOs = Get-GPO $Domain -all $Count = ($GPOs | Measure-Object).Count If ($Count -gt 0) { write-host " " Write-Verbose "There are $Count GPOs to process in the $Domain domain." -Verbose If ($All) { Write-Verbose "- Processing all GPOs." -Verbose } Else { Write-Verbose "- Processing GPOs with user settings only." -Verbose } write-host " " foreach ($gpo in $GPOs) { $ProcessGPO = $False if ($All) { $ProcessGPO = $True } if ($All -eq $False -AND $gpo.user.DSVersion -gt 0) { $ProcessGPO = $True } if ($ProcessGPO) { $AuthUser = $null $DomComp = $null $obj = New-Object -TypeName PSObject $obj | Add-Member -MemberType NoteProperty -Name "Domain" -value $Domain $obj | Add-Member -MemberType NoteProperty -Name "DisplayName" -value $gpo.displayname $obj | Add-Member -MemberType NoteProperty -Name "Name" -value $gpo.id # Read the GPO permissions to find out if Authenticated Users and Domain Computers is missing. if ($OSVersion -ge 6.2) { $AuthUser = Get-GPPermission -Domain $Domain -Guid $gpo.id -TargetName $AuthUserName -TargetType group -ErrorAction SilentlyContinue $DomComp = Get-GPPermission -Domain $Domain -Guid $gpo.id -TargetName $DomainComputersName -TargetType group -ErrorAction SilentlyContinue } Else { $AuthUser = Get-GPPermissions -Domain $Domain -Guid $gpo.id -TargetName $AuthUserName -TargetType group -ErrorAction SilentlyContinue $DomComp = Get-GPPermissions -Domain $Domain -Guid $gpo.id -TargetName $DomainComputersName -TargetType group -ErrorAction SilentlyContinue } If ($AuthUser -eq $null) { # Authenticated Users has been removed $AuthUserRead = $False $CustomAuthUserPerm = $False } Else { If (($AuthUser.Permission -eq "GpoApply") -OR ($AuthUser.Permission -eq "GpoRead")) { # Authenticated Users has Read permissions $AuthUserRead = $True $CustomAuthUserPerm = $False } Else { # Authenticated Users does not have Read Permissions but Custom Permissions have been set $AuthUserRead = $False $CustomAuthUserPerm = $True } } if ($DomComp -eq $null) { # Domain Computers do not have direct permissions $DomainComputersRead = $False } Else { If (($DomComp.Permission -eq "GpoApply") -OR ($DomComp.Permission -eq "GpoRead")) { # Domain Computers has Read permissions $DomainComputersRead = $True } Else { $DomainComputersRead = $False } } $obj | Add-Member -MemberType NoteProperty -Name "AuthUserRead" -value $AuthUserRead $obj | Add-Member -MemberType NoteProperty -Name "CustomAuthUserPerm" -value $CustomAuthUserPerm $obj | Add-Member -MemberType NoteProperty -Name "DomainComputersRead" -value $DomainComputersRead If ($AuthUserRead -eq $False -AND $DomainComputersRead -eq $False) { $GPOsNoAuthUser++ $ToBeFixed = $True Write-Warning "$($gpo.DisplayName)" -Verbose } Else { $ToBeFixed = $False Write-Verbose "$($gpo.DisplayName)" -Verbose } If ($ToBeFixed -AND $Action) { If ($AuthUsers -eq $False) { $TargetName = $DomainComputersName } Else { $TargetName = $AuthUserName } if ($OSVersion -ge 6.2) { Set-GPPermission -Domain $Domain -Guid $gpo.Id -PermissionLevel GpoRead -TargetName $TargetName -TargetType Group } Else { Set-GPPermissions -Domain $Domain -Guid $gpo.Id -PermissionLevel GpoRead -TargetName $TargetName -TargetType Group } } # Set the output file path and name. $OutputFile = $(&$ScriptPath) + "\$ScriptName-$Domain-$($datestampforfilename).csv" # PowerShell V2 doesn't have an Append parameter for the Export-Csv cmdlet. Out-File does, but it's # very difficult to get the formatting right, especially if you want to use quotes around each item # and add a delimeter. However, we can easily do this by piping the object using the ConvertTo-Csv, # Select-Object and Out-File cmdlets instead. if ($PSVersionTable.PSVersion.Major -gt 2) { $obj | Export-Csv -Path "$OutputFile" -Append -Delimiter "," -NoTypeInformation -Encoding ASCII } Else { if (!(Test-Path -path $OutputFile)) { $obj | ConvertTo-Csv -NoTypeInformation -Delimiter "," | Select-Object -First 1 | Out-File -Encoding ascii -filepath "$OutputFile" } $obj | ConvertTo-Csv -NoTypeInformation -Delimiter "," | Select-Object -Skip 1 | Out-File -Encoding ascii -filepath "$OutputFile" -append -noclobber } $obj = $null $GPOsProcessed ++ } } write-host " " Write-Verbose "Summary for the $Domain domain:" -Verbose If ($All) { Write-Verbose "- All $Count GPOs were processed." -Verbose } Else { Write-Verbose "- Only $GPOsProcessed GPOs with user settings were processed." -Verbose } If ($GPOsNoAuthUser -gt 0 -AND $Action) { Write-Verbose "- $GPOsNoAuthUser out of $Count GPOs that have been modified by granting $TargetName Read permissions." -Verbose } Else { If ($GPOsNoAuthUser -gt 0) { Write-Warning "- $GPOsNoAuthUser out of $Count GPOs that need to be modified by granting either $DomainComputersName or $AuthUserName Read permissions." -Verbose } Else { Write-Verbose "- No GPOs require modification." -Verbose } } write-host " " } } Else { Write-Warning "The $Domain domain could not be contacted." -Verbose write-host " " } } #------------------------------------------------------------- try { Stop-Transcript } catch { Write-Verbose "This host does not support transcription" }

Enjoy!

Jeremy Saunders

Jeremy Saunders

Technical Architect | DevOps Evangelist | Software Developer | Microsoft, NVIDIA, Citrix and Desktop Virtualisation (VDI) Specialist/Expert | Rapper | Improvisor | Comedian | Property Investor | Kayaking enthusiast at J House Consulting
Jeremy Saunders is the Problem Terminator. He is a highly respected IT Professional with over 35 years’ experience in the industry. Using his exceptional design and problem solving skills with precise methodologies applied at both technical and business levels he is always focused on achieving the best business outcomes. He worked as an independent consultant until September 2017, when he took up a full time role at BHP, one of the largest and most innovative global mining companies. With a diverse skill set, high ethical standards, and attention to detail, coupled with a friendly nature and great sense of humour, Jeremy aligns to industry and vendor best practices, which puts him amongst the leaders of his field. He is intensely passionate about solving technology problems for his organisation, their customers and the tech community, to improve the user experience, reliability and operational support. Views and IP shared on this site belong to Jeremy.
Jeremy Saunders
Jeremy Saunders

Previous post:

Next post: