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 (1122 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

Independent Consultant | Contractor | Microsoft & Citrix Specialist | Desktop Virtualization Specialist at J House Consulting
Jeremy is a highly respected, IT Professional, with over 30 years’ experience in the industry. He is an independent IT consultant providing expertise to enterprise, corporate, higher education and government clients. His skill set, high ethical standards, integrity, morals and attention to detail, coupled with his friendly nature and exceptional design and problem solving skills, makes him one of the most highly respected and sought after Microsoft and Citrix technical resources in Australia. His alignment with industry and vendor best practices puts him amongst the leaders of his field.
Jeremy Saunders
Jeremy Saunders
Jeremy Saunders
  • Hi Jon,

    Firstly, whilst the script get’s all GPOs (306), by default it will only process those that have user settings (217). If you want it to process all 306 GPOs, then use the -All parameter as per the syntax examples in the script header.

    Secondly, Yes, there was a bug in the last few lines that reported the outcome incorrectly. That has now been updated.

    Thirdly, The 9 GPOs in the CSV file represent the 217 processed that have user settings. I’m sure that if you ran the script again with the -All parameter, all 19 GPOs would be listed as expected.

    The only issue with the script was the output to screen at the end if the last GPO processed did not require remediation. There was a logic issue. The data collected in the CSV file is correct. Please read the script instructions in the header of the script.

    Cheers,
    Jeremy

    • Jon Pemberton

      Thanks for the reply, all set now, tyvm!
      Jon

  • Pingback: Microsoft: KB3159398 macht Probleme bei der Gruppenrichtlinenverarbeitung | blog.friedlandreas.net()

  • Shawn P. Lemay

    Jeremy – I tried running this script today on an SBS2008 server and get a bunch of errors –

    Import-Module : The specified module ‘ActiveDirectory’ was not loaded because no valid module file was found in any module directory.
    + Import-Module <<<< ActiveDirectory -WarningAction SilentlyContinue

    + CategoryInfo : ResourceUnavailable: (ActiveDirectory:String) [Import-Module], FileNotFoundException

    + FullyQualifiedErrorId : Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand

    Import-Module : The specified module 'GroupPolicy' was not loaded because no valid module file was found in any module directory.+ Import-Module <<<< GroupPolicy -WarningAction SilentlyContinue

    + CategoryInfo : ResourceUnavailable: (GroupPolicy:String) [Import-Module], FileNotFoundException

    + FullyQualifiedErrorId : Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand

    The term 'Get-ADGroup' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
    There are others – but that gives you an idea. I read that the script looks at the OS and makes changes – but as I'm not a scripter, is it possible the script wasn't correctly updated to run on SBS2008 servers? Thanks,
    Shawn

    • Hi Shawn,

      It will only run on an OS with the Active Directory and Group Policy PowerShell Modules, this is why you are getting the “Import-Module” errors. All other errors are related to that. You’ll need to run it on Windows 7/2008R2 and above with the RSAT (Remote Server Administration Tools) installed including the Group Policy Management Console.

      Cheers,
      Jeremy

  • Selahattin

    Jeremy – thanks for the script it would be helpful if it can handle a multi-domain forest as well, e. g. by providing a domain parameter or can we specify a domain controller. Because I have 200’s of DCs.

    • Hi Selahattin,

      Added as requested 🙂

      You can now pass multiple domains as a parameter. Have a look at the syntax example in the script header.

      Let me know if there are any problems as my test environment is limited.

      Cheers,
      Jeremy

  • Frank Pruss

    The script is not signed, making it hard for noobs with a locked down PS Execution Policy… 🙁

    • Yes, you are right Frank; and one of these days I’ll focus on signing all the scripts I publish. However, there are at least 2 – 3 methods of running unsigned scripts in an environment with a locked down PS environment. I won’t give you those methods here, as it’s inappropriate for me to publish, but you should be able to work it out. If not, copy and past the guts of the script into a PowerShell environment.

      • Frank Pruss

        Yes, the fast way was to edit your script in PowerShell ISE and save it locally (no changes). Very nice.

        • Aha…it sounds like it may just be the blocking. When you download a file from the Internet you typically have to right click on it, select properties, and then select unblock.

  • Trevor Bruss

    The script has a problem when running against multiple domains, because the Domain Computers group from the domain of the computer you are running the script from gets populated instead of the Domain Computers group from the domain you are working on. You only reference the name of the group, and therefore without qualifying the DOMAINDomain Computers you effectively add the Domain Computers of your currently logged in context.

    • Thanks for letting me know Trevor. I missed that during testing!

      I just changed: $TargetName = $DomainComputersName
      To: $TargetName = “$Domain$DomainComputersName”

      That should be okay now.

      Cheers,
      Jeremy

      • Trevor Bruss

        Still not working as intended because the reading of the policy permissions also needs to account for the NetBIOS name. My suggestion is to add the following line after getting the Domain SID:
        $DomainNetBIOS = (Get-ADDomain $Domain).NetBIOSName

        And then modifying the Domain Computers name line to:
        $DomainComputersName = $DomainNetBIOS + “” + (Get-ADGroup -server $Domain “$($DomainSID)-515”).Name

        Then the rest of the code wouldn’t require changing. That is to say you must change back the TargetName to:

        $TargetName = “$DomainComputersName”

        The only other change is your description of using -Domains in the blog post. You mention separating them by commas, but instead the code looks for a space.

        Great script! Has saved me tons of manual work in some of our domains.

        • Nice! I’ve updated with your suggested changes and attributed the fix to you.

          Different versions of PowerShell may process that command line parameter differently. Using commas will automatically create an array. I’ll do some further testing on Monday when I’m on-site with a customer that has different OS’s and versions of POSH to test on.

          Cheers,
          Jeremy

Previous post:

Next post: