Finding and removing GPP passwords (cpassword)
Find every Group Policy Preferences cpassword in SYSVOL, backups and client caches, map it to the exposed account, rotate it and stop it coming back.
Group Policy Preferences once let administrators set local account passwords, service credentials, scheduled task run-as accounts, mapped drive credentials and data source passwords through a GPO. The password was stored in SYSVOL as a cpassword attribute, AES-256 encrypted with a key Microsoft published in its protocol documentation. SYSVOL is readable by every authenticated user, and tools such as Get-GPPPassword and the equivalent modules in common attack frameworks decrypt the value in a second. The usual payload is a local administrator password shared across every workstation and server, which turns one phished user into lateral movement across the estate.
MS14-025 closed the editor in May 2014 but left existing values in place, and they are still found in assessments today. The Group Policy and SYSVOL pillar shows the basic SYSVOL search. This guide covers the complete job: every place a cpassword can hide, working out which account each one exposes, rotating it properly, and making sure it does not come back.
Where cpassword values live
Preference items that could carry a password write it to these files inside a GPO's Machine\Preferences or User\Preferences folders:
| File | Preference type | Account attribute to read |
|---|---|---|
Groups\Groups.xml | Local Users and Groups | userName, newName |
Services\Services.xml | Services | accountName |
ScheduledTasks\ScheduledTasks.xml | Scheduled Tasks | runAs |
Drives\Drives.xml | Drive Maps | userName |
DataSources\DataSources.xml | Data Sources | username |
Printers\Printers.xml | Printers | username |
The same XML files also exist outside the live Policies folder:
- GPO backups made with GPMC or
Backup-GPO, often on admin file shares or in change-control repositories. - Client-side caches: each client keeps a copy of applied preference XML under
%ProgramData%\Microsoft\Group Policy\History\{GUID}\(computer settings) and%LocalAppData%\Microsoft\Group Policy\History\(user settings), which also ends up in VM templates and disk images. - Stale SYSVOL copies left behind by an FRS-to-DFSR migration or manual copies on DCs (
SYSVOL.bak,SYSVOL_old). - Scripts in
NETLOGONand GPO script folders, which are not cpassword but frequently contain plain-text credentials for the same accounts.
Measure: find every instance
Parse the XML rather than grepping so you can extract which account each value belongs to. The script reports the value's presence, never the value itself.
function Find-GppCpassword {
param([Parameter(Mandatory)] [string[]] $Path)
Get-ChildItem -Path $Path -Recurse -Include Groups.xml, Services.xml, ScheduledTasks.xml,
Drives.xml, DataSources.xml, Printers.xml -ErrorAction SilentlyContinue |
ForEach-Object {
$file = $_.FullName
Select-Xml -Path $file -XPath '//*[@cpassword]' | ForEach-Object {
$n = $_.Node
if ($n.cpassword) {
[pscustomobject]@{
File = $file
GpoGuid = if ($file -match '\{([0-9A-Fa-f-]{36})\}') { $Matches[1] }
Item = $n.ParentNode.name
Account = @($n.userName, $n.accountName, $n.runAs, $n.username, $n.newName) |
Where-Object { $_ } | Select-Object -First 1
Changed = $n.ParentNode.changed
}
}
}
}
}
$dns = (Get-ADDomain).DNSRoot
$hits = Find-GppCpassword -Path "\\$dns\SYSVOL\$dns\Policies", 'D:\GPOBackups'
$hits | ForEach-Object {
$gpo = if ($_.GpoGuid) { Get-GPO -Guid $_.GpoGuid -ErrorAction SilentlyContinue }
$_ | Add-Member -NotePropertyName GpoName -NotePropertyValue $gpo.DisplayName -PassThru
} | Format-Table GpoName, Item, Account, Changed, File -AutoSizeEmpty cpassword="" attributes are ignored because they carry no secret. A hit with no resolvable GpoName usually points to a backup or an orphaned GPT folder whose GPC was deleted; orphaned folders are still readable by everyone and must be removed too.
Check for leftover SYSVOL copies on each DC and for cached copies on a sample of clients and on your golden images:
# On each DC
Get-ChildItem C:\Windows -Directory -Filter 'SYSVOL*' | Select-Object FullName
# On a client or a mounted image
Find-GppCpassword -Path "$env:ProgramData\Microsoft\Group Policy\History"Audit: work out what each value exposes
For every hit, answer three questions before touching the GPO:
- Which account? A
Groups.xmlitem withuserName="Administrator"(or a renamed built-in vianewName) exposes the local administrator on every computer in the GPO's scope. AServices.xmlorScheduledTasks.xmlitem usually exposes a domain service account. - Where did it apply? Check the GPO's links and security filtering with
Get-GPOReport -Guid <id> -ReportType Html. A local admin password applied to servers and workstations alike means the same credential works on both tiers. - Is it still valid? For domain accounts compare
pwdLastSetwith the item'schangedtimestamp. If the password was last set before or at that date, the SYSVOL value is almost certainly current.
Get-ADUser svc-backup -Properties pwdLastSet, lastLogonTimestamp, memberOf |
Select-Object Name, @{n='PwdLastSet';e={[datetime]::FromFileTime($_.pwdLastSet)}},
@{n='LastLogon';e={[datetime]::FromFileTime($_.lastLogonTimestamp)}}, memberOfTo size the blast radius of a local administrator entry, count the computers in scope of the GPO. The query below walks every OU where the GPO is linked; adjust it if the GPO is also security-filtered to a group.
$guid = '<GpoGuid from the scan>' # e.g. 6AC1786C-016F-11D2-945F-00C04FB984F9
Get-ADObject -LDAPFilter "(gPLink=*$guid*)" -SearchBase (Get-ADDomain).DistinguishedName |
ForEach-Object {
[pscustomobject]@{
LinkedTo = $_.DistinguishedName
Computers = (Get-ADComputer -SearchBase $_.DistinguishedName -Filter * | Measure-Object).Count
}
}A count that includes servers and workstations together tells you the password bridged tiers: anyone who harvested it from a workstation could log on to servers with the same account. Record that fact in the finding, because it determines how much of the estate you should treat as potentially accessed.
If the exposed account is privileged (a member of any admin group, or a service account with rights on Tier 0 systems), treat this as a potential incident: review its logon history on DCs (events 4624 and 4768) before rotation destroys the evidence of reuse.
Enforce: replace, remove, rotate
Order matters. Replace the mechanism first, otherwise removing the preference breaks whatever it was doing. Plan the work as one change per exposed account rather than one change per GPO: the same service account often appears in several GPOs, and rotating it while one of them still pushes the old password means the next Group Policy refresh either breaks the service or, for local accounts, resets the password back to the exposed value.
- Local administrator passwords → deploy Windows LAPS to the same OUs, confirm passwords are being backed up, then delete the Local Users and Groups item. LAPS immediately rotates the local password on each machine, which is also your rotation step.
- Service and scheduled task accounts → move to a gMSA where the application supports it. Scheduled tasks can run as a gMSA (
New-ScheduledTaskPrincipal -UserId 'CORP\svc-task$' -LogonType Password) or as SYSTEM if the task only needs local rights. - Drive maps and printers with credentials → remove the credential; access should use the user's own identity with share permissions granting what they need.
- Data sources → use integrated authentication or an application-managed secret store.
Then remove the item. Use the Group Policy Management Editor (Computer or User Configuration > Preferences > the relevant node > delete the item) rather than editing XML by hand, so the GPO version number increments and clients process the change. If the GPO contains nothing else, unlink and delete the GPO after backing it up somewhere access-controlled.
Finally, rotate. For domain accounts, reset the password to a new random value and update every consumer; if you suspect the account was used by an attacker, also review what it had access to rather than treating the reset as the end of the matter. For local accounts not covered by LAPS, rotate on every machine in scope. Removing a preference never reverts or changes the password it already set.
Delete or sanitise GPO backups containing the values, remove leftover SYSVOL copies, and rebuild or clean golden images that captured the client-side history.
Verify
Rerun Find-GppCpassword against SYSVOL, the backup locations and a sample of clients after their next Group Policy refresh; all should return nothing. For domain accounts, confirm pwdLastSet is after the rotation date. For LAPS-covered machines, confirm a LAPS password exists and was set recently:
Get-LapsADPassword -Identity WS0142 | Select-Object ComputerName, PasswordUpdateTime, ExpirationTimestampAdd the SYSVOL scan to a scheduled job so a restored backup or an imported legacy GPO is caught within a day. PingCastle also checks for GPP passwords, so it will flag a regression in your periodic assessment.
A useful detection layer is a deliberate decoy: a Groups.xml in an unlinked GPO with a cpassword for a honeytoken account that is never legitimately used. Any authentication attempt for that account (4625, 4771 or 4776 on DCs) means someone is harvesting SYSVOL.
What it breaks
- Removing a Local Users and Groups item before LAPS is deployed leaves machines with a known, unmanaged local admin password. Deploy LAPS first.
- Rotating a domain service account breaks every service, task and application still holding the old password. Inventory consumers with 4624/4768 logon data before resetting.
- Deleting drive map credentials causes access denied for users whose own accounts lack share permissions; fix the share ACLs first.
- Deleting old GPO backups removes your ability to restore those GPOs as they were. Keep a sanitised backup if you need history.
- The decoy GPO will be flagged by your own assessment tools; document it so it is not "fixed" by a colleague.
Related reading: the Group Policy & SYSVOL topic hub, auditing GPO permissions, and password policy that works for the account-side controls that limit the damage of any leaked credential.
Frequently asked questions
If I delete the cpassword from SYSVOL, am I safe?
No. Any authenticated user could read SYSVOL for as long as the value was there, often years, so assume the password is known. Deleting the preference item stops new exposure but does not change the password on any system it was applied to. Rotate the credential on every machine or account that uses it, and check GPO backups, client caches and images for copies.
Did MS14-025 remove existing cpassword values?
No. MS14-025 (KB2962486, May 2014) removed the ability to set passwords in the affected Group Policy Preferences dialogs, so administrators cannot create new ones through the editor. It did not touch XML files already in SYSVOL, and it does not stop someone importing an old GPO backup that contains them. Existing values must be found and removed manually.
What should replace GPP local administrator passwords?
Windows LAPS. It sets a unique, random password for the built-in administrator (or a named account) on every machine, rotates it on a schedule, and stores it in AD or Entra ID with access controlled per OU and optionally encrypted. It removes the shared-password problem entirely, which is what made GPP local admin accounts so damaging in the first place.
Finding and removing GPP passwords (cpassword)