ADGPO-020: PowerShell Execution Policy via GPO
- Platform
- Active Directory
- Category
- AD Group Policy
- Severity
- Medium
- Zero Trust pillar
- Governance (weight 1)
- Golden fixtures
- 2
- Branch coverage
- Observed: fixtures prove the verdicts they exercise
- Provenance
- baseline
What it checks
The check reads two registry value paths populated by the "Turn on Script Execution" administrative template: HKLM\Software\Policies\Microsoft\Windows\PowerShell\EnableScripts (DWORD) HKLM\Software\Policies\Microsoft\Windows\PowerShell\ExecutionPolicy (REG_SZ) It evaluates each GPO that defines these values and records: the GPO display name and GUID, the registry value pair, the policy scope (Computer Configuration or User Configuration), every OU and site the GPO is linked to, and the GpoStatus. A GPO passes when ExecutionPolicy is set to AllSigned or RemoteSigned and EnableScripts is 1, indicating that script execution is permitted but signature checking is enforced. A GPO fails when ExecutionPolicy is set to Unrestricted or Bypass, when EnableScripts is 1 with no ExecutionPolicy value (which leaves the resolved policy at the machine default and overrides any safer per-user setting), or when the value is set under HKCU\Software\Policies in a way that overrides a stricter machine policy. The check also flags the absence of any execution policy GPO in scope: in that case the resolved policy falls back to the registry default (Restricted on workstations, RemoteSigned on Windows Server), which is informational rather than a failure but is captured for context. Supplementary metadata includes whether the linked scope contains Tier-0 systems (Domain Controllers OU, Tier-0 OUs), whether PowerShell ScriptBlock logging and Module logging are also configured in the same scope (ADLOG checks), and whether Constrained Language Mode is enforced via AppLocker or WDAC in the same OU.
Why it matters
Microsoft is explicit that execution policy is not a security control: it is a usability fence designed to prevent a user from running a script by accident. Five documented bypasses exist on every modern Windows host: powershell.exe -ExecutionPolicy Bypass, powershell.exe -Command (passing the script body directly), reading the file with Get-Content and piping to Invoke-Expression, base64-encoded -EncodedCommand payloads, and loading PowerShell through System.Management.Automation.dll from a custom host (the technique behind PowerPick and unmanaged PowerShell runners). An attacker who already has code execution on the host can run any script regardless of the policy setting. The policy still matters for two reasons. First, it is a tripwire for interactive and semi-interactive vectors: a user double-clicking a .ps1 from an HTML smuggling drop, a macro-launched Wscript-to-PowerShell chain, or a LOLBin-launched script will succeed silently on a Bypass host and fail with a console prompt or block on a RemoteSigned/AllSigned host. Second, the value pushed by GPO is consumed by every PowerShell session that does not explicitly override it, including scheduled tasks, login scripts, software-deployment wrappers, and third-party PowerShell hosts that do not pass -ExecutionPolicy. Setting Unrestricted or Bypass via GPO across an OU turns thousands of endpoints into environments where every PowerShell child process inherits a permissive default, every Internet-zone script runs without the AlternateDataStream MOTW warning, and every signed-script enforcement requirement built into vendor tooling is silently nullified. The blast radius compounds when the GPO is linked to the Domain Controllers OU or any Tier-0 OU: a Bypass policy on a DC means any script staged through SYSVOL, a write-able share, or a scheduled-task creation path runs without challenge in LSASS context.
Attack path
1. Reconnaissance: the attacker, holding any authenticated domain user, enumerates GPOs with Get-DomainGPO (PowerView), parses the Registry.pol files inside SYSVOL under \\domain\SYSVOL\<domain>\Policies\{GUID}\Machine\Registry.pol, and reads the ExecutionPolicy and EnableScripts values directly. BloodHound (CollectionMethod GPOLocalGroup) shows which OUs each GPO is linked to. 2. Target selection: the attacker prioritizes GPOs that set ExecutionPolicy to Unrestricted or Bypass and are linked to OUs containing workstations, jump servers, or domain controllers. A Bypass policy on the workstations OU means every endpoint is one .ps1 drop away from execution. 3. Initial access: the attacker delivers a .ps1 payload through a phishing attachment that uses HTML smuggling to write the file to %TEMP%, through a watering-hole download that lands the file with the MOTW Internet-zone marker, or through a SharePoint/OneDrive sync that ignores zone identifiers. On a Bypass host the script runs on double-click. On a Restricted host the user sees a console prompt; on a RemoteSigned host the Internet-zone marker blocks execution; on an AllSigned host the script is blocked unless signed by a trusted publisher. 4. Lateral movement: once the attacker has code execution on one host, they enumerate other hosts in the same OU and reuse the same .ps1 payload. WMI, WinRM, and PSExec all invoke powershell.exe with the GPO-resolved default policy on the remote host, so a Bypass policy in scope on the target means no -ExecutionPolicy flag is required and no Defender heuristic for "explicit policy override" fires. 5. Tradecraft: with Bypass in scope the attacker does not need to call powershell.exe -ExecutionPolicy Bypass, which is a high-signal pattern that EDR vendors alert on. The attacker can call powershell.exe -File script.ps1 or powershell.exe -Command ". \\share\script.ps1" and inherit the GPO default. This reduces detection surface in environments that rely on commandline analytics. 6. Persistence: the attacker drops a .ps1 in a writeable startup folder or registers a scheduled task that invokes powershell.exe -File. With Bypass in the GPO, the task runs at every logon without any policy-related event in the PowerShell operational log or in Defender telemetry. 7. Tier-0 escalation: if the same GPO is linked to the Domain Controllers OU, the attacker who reaches a DC (via a separate path: print spooler, ADCS ESC, weak ACL) can run unsigned PowerShell directly in LSASS context, including DSInternals, Mimikatz wrappers, and DCSync wrappers, without ever toggling the policy or leaving an artifact in the GPO change log.
How Guerrilla assesses it
Guerrilla loads the GroupPolicy and ActiveDirectory modules, calls Get-GPO -All -Domain $env:USERDNSDOMAIN, then for each GPO calls Get-GPRegistryValue -Guid $gpo.Id -Key "HKLM\Software\Policies\Microsoft\Windows\PowerShell" -ErrorAction SilentlyContinue and the corresponding HKCU path. Each returned value pair is normalized into an ExecutionPolicy state (Restricted, AllSigned, RemoteSigned, Unrestricted, Bypass, Undefined) and an EnableScripts boolean. Linked scopes are resolved via Get-ADOrganizationalUnit -LDAPFilter "(gPLink=*$($gpo.Id)*)" and Get-ADObject -SearchBase "CN=Sites,CN=Configuration,$($domain.DistinguishedName)" -LDAPFilter "(gPLink=*$($gpo.Id)*)" plus the gPLink on the domain root. The check classifies each GPO as Pass, Fail, or Informational. Fail conditions: ExecutionPolicy in {Unrestricted, Bypass}; EnableScripts = 1 with ExecutionPolicy undefined; HKCU policy weaker than HKLM policy in the same scope; any of the above in a scope that includes Domain Controllers or Tier-0 OUs (severity escalated). Output rows include DisplayName, Id, Scope (Computer/User), ExecutionPolicy, EnableScripts, LinkedScopes, TierZeroLinked (bool), CoConfiguredLogging (bool: whether ScriptBlock/Module logging is also set), and Notes. The check also enumerates the resolved policy on the auditing host with Get-ExecutionPolicy -List as a sanity check that the GPO state matches what the Group Policy engine actually applies.
Recommended value
PowerShell execution policy set to AllSigned or RemoteSigned via GPO; not set to Unrestricted or Bypass
Remediation
Review GPO settings under Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell > Turn on Script Execution. Set the execution policy to AllSigned for high-security environments or RemoteSigned for standard environments. Implement code signing for authorized PowerShell scripts. Avoid setting Bypass or Unrestricted via GPO.
Fixture-proven verdicts
Every verdict below is proven by a golden fixture in the module's gating test suite. This table derives from the last green run; it cannot be edited by hand.
| Scenario | Expected verdict |
|---|---|
| informational | WARN |
| throttled | Not Assessed |
Framework mappings
- NIST SP 800-53
- CM-6, CM-7, SI-7
- MITRE ATT&CK
- T1059.001, T1484.001