This is an allow-list map, not an S4U cookbook. Target is a fake lab forest LAB.INTERNAL with a two-tier app: WEB01$ may present delegated credentials to HTTP/api.lab.internal and CIFS/fs01.lab.internal. Goal: setspn the accounts, klist a real user TGT plus the HTTP ticket from a browser hit, and draw who may hop where. I do not run Rubeus / Kekeo, I do not request S4U2Self, I do not forge a ticket, I do not /ptt.
1Figure 1. Constrained delegation is an allow-list on the front-end. Protocol transition is a separate bit.
2labuser --Kerberos--> HTTP/web01
3 WEB01$ msDS-AllowedToDelegateTo:
4 HTTP/api.lab.internal
5 CIFS/fs01.lab.internal
6 (TrustedToAuthForDelegation = False)
Lab layout
1labs/kcd_lab/
2 setspn.txt
3 klist-labuser.txt
4 getad-web01.txt
5 4769.txt
Domain LAB.INTERNAL, DC dc01.lab.internal. Person labuser. Front-end computer WEB01$. Back-ends API01$ (HTTP/api.lab.internal) and FS01$ (CIFS/fs01.lab.internal). Isolated VMs. SIDs [REDACTED].
Artifact: setspn
1C:\lab> setspn -L WEB01
2Registered ServicePrincipalNames for CN=WEB01,OU=servers,DC=lab,DC=internal:
3 HTTP/web01.lab.internal
4 HTTP/web01
5 HOST/web01.lab.internal
6 HOST/web01
7
8C:\lab> setspn -L API01
9Registered ServicePrincipalNames for CN=API01,OU=servers,DC=lab,DC=internal:
10 HTTP/api.lab.internal
11 HTTP/api
12 HOST/api01.lab.internal
13 HOST/api01
14
15C:\lab> setspn -L FS01
16Registered ServicePrincipalNames for CN=FS01,OU=servers,DC=lab,DC=internal:
17 CIFS/fs01.lab.internal
18 CIFS/fs01
19 HOST/fs01.lab.internal
20 HOST/fs01
21
22C:\lab> setspn -Q HTTP/api.lab.internal
23Checking domain DC=lab,DC=internal
24CN=API01,OU=servers,DC=lab,DC=internal
25 HTTP/api.lab.internal
26 HTTP/api
27Existing SPN found!
One object per HTTP SPN. A duplicate setspn -Q hit would be a map error, not a KCD feature.
Artifact: the allow-list on WEB01$
1PS C:\lab> Get-ADComputer WEB01 -Properties TrustedForDelegation,
2 TrustedToAuthForDelegation, msDS-AllowedToDelegateTo,
3 userAccountControl |
4 fl Name, TrustedForDelegation, TrustedToAuthForDelegation,
5 msDS-AllowedToDelegateTo, userAccountControl
1Name : WEB01
2TrustedForDelegation : False
3TrustedToAuthForDelegation : False
4msDS-AllowedToDelegateTo : {HTTP/api.lab.internal, CIFS/fs01.lab.internal}
5userAccountControl : 4096
Decode I write on the ticket:
| Field | Value | Meaning |
|---|---|---|
TrustedForDelegation | False | not unconstrained |
TrustedToAuthForDelegation | False | no protocol transition (no S4U2Self for arbitrary users from this bit) |
msDS-AllowedToDelegateTo | HTTP/api, CIFS/fs01 | classic constrained allow-list |
UAC | 4096 | WORKSTATION_TRUST_ACCOUNT |
RBCD on the targets, for the same hop counted from the other end:
1PS C:\lab> Get-ADComputer API01,FS01 -Properties msDS-AllowedToActOnBehalfOfOtherIdentity |
2 select Name, msDS-AllowedToActOnBehalfOfOtherIdentity
3Name msDS-AllowedToActOnBehalfOfOtherIdentity
4---- ----------------------------------------
5API01 # empty in this lab
6FS01 # empty in this lab
Classic constrained only. The other note in this series covers RBCD leftovers. This map is one plane.
Artifact: klist as the user (no forged tickets)
Logon as labuser on LABPC01, browse https://web01.lab.internal/. Cache:
1C:\lab> klist
2Current LogonId is 0:0x[REDACTED]
3
4Cached Tickets: (2)
5
6#0> Client: labuser @ LAB.INTERNAL
7 Server: krbtgt/LAB.INTERNAL @ LAB.INTERNAL
8 KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
9 Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent
10 Start Time: 3/17/2020 10:41:02 (local)
11 End Time: 3/17/2020 20:41:02 (local)
12 Renew Time: 3/24/2020 10:41:02 (local)
13 Session Key Type: AES-256-CTS-HMAC-SHA1-96
14 Cache Flags: 0x1 -> PRIMARY
15 Kdc Called: dc01.lab.internal
16
17#1> Client: labuser @ LAB.INTERNAL
18 Server: HTTP/web01.lab.internal @ LAB.INTERNAL
19 KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
20 Ticket Flags 0x40a10000 -> forwardable renewable pre_authent
21 Start Time: 3/17/2020 10:41:18 (local)
22 End Time: 3/17/2020 20:41:02 (local)
23 Session Key Type: AES-256-CTS-HMAC-SHA1-96
24 Cache Flags: 0
25 Kdc Called: dc01.lab.internal
#0 is the TGT (initial). #1 is the front-end service ticket. There is no HTTP/api ticket on the workstation. The hop, if the app is working, happens on WEB01 with the constrained allow-list. I do not dump klist as WEB01$ after an S4U. I dump 4769 on the DC instead.
Linux klist from the same user (SSSD lab join), same times:
1$ klist
2Ticket cache: FILE:/tmp/krb5cc_[REDACTED]
3Default principal: labuser@LAB.INTERNAL
4
5Valid starting Expires Service principal
603/17/20 10:41:02 03/17/20 20:41:02 krbtgt/LAB.INTERNAL@LAB.INTERNAL
7 renew until 03/24/20 10:41:02
803/17/20 10:41:18 03/17/20 20:41:02 HTTP/web01.lab.internal@LAB.INTERNAL
4769: the hop as the DC saw it
A healthy app request, one user, one front-end, then one back-end. Redacted:
1Event 4769 A Kerberos service ticket was requested.
2Account Information:
3 Account Name: labuser@LAB.INTERNAL
4 Account Domain: LAB.INTERNAL
5 Logon ID: 0x[REDACTED]
6Service Information:
7 Service Name: HTTP/web01.lab.internal
8 Service ID: S-1-5-21-[REDACTED]-1112
9Network Information:
10 Client Address: ::ffff:10.0.10.50
11 Client Port: [REDACTED]
12Additional Information:
13 Ticket Options: 0x40810000
14 Ticket Encryption Type: 0x12
15 Failure Code: 0x0
16 Transited Services: -
Second 4769, a few milliseconds later, from WEB01’s address, still Account Name: labuser@LAB.INTERNAL, Service Name: HTTP/api.lab.internal. That pair is the constrained hop: same user, different client address, service on the allow-list. I do not mint the second ticket by hand.
1Event 4769
2 Account Name: labuser@LAB.INTERNAL
3 Service Name: HTTP/api.lab.internal
4 Client Address: ::ffff:10.0.10.20 # WEB01, not LABPC01
5 Ticket Encryption Type: 0x12
6 Failure Code: 0x0
7 Transited Services: HTTP/web01.lab.internal # present on some builds
If Transited Services is populated, it names the front-end. If it is -, I still have client address = WEB01 plus service = allow-list member.
A failed hop is also useful. I pointed the lab app at CIFS/dc01.lab.internal, which is not on msDS-AllowedToDelegateTo. The DC answered:
1Event 4769
2 Account Name: labuser@LAB.INTERNAL
3 Service Name: cifs/dc01.lab.internal
4 Client Address: ::ffff:10.0.10.20
5 Failure Code: 0xC # KDC_ERR_BADOPTION (constrained deny)
6 Ticket Encryption Type: 0xFFFFFFFF
0xC here is the allow-list working. I do not then widen the list to make the call succeed.
What I do not file as success: a 4769 for krbtgt with odd flags, a 4769 for CIFS/dc01 from WEB01$ with result 0, a ticket I created with a forged PAC.
Map I actually keep
1principal edges
2----------------- --------------------------------------------------
3labuser AS -> krbtgt/LAB ; TGS -> HTTP/web01
4WEB01$ AllowedToDelegateTo -> HTTP/api , CIFS/fs01
5 TrustedToAuthForDelegation = False
6API01$ SPN HTTP/api.lab.internal ; RBCD empty
7FS01$ SPN CIFS/fs01.lab.internal ; RBCD empty
8who can write Domain Admins on these attributes in this lab
9 (if Account Operators can write them, that is a finding)
S4U2Self / S4U2Proxy as ideas, not as commands:
- S4U2Self: front-end asks a ticket to itself for a given user. Needed when the front door was not Kerberos. Requires
TrustedToAuthForDelegationin the classic model. False here, so this lab app must receive Kerberos from the user (it did —klist#1). - S4U2Proxy: front-end asks a ticket to a different SPN on the allow-list. That is the second 4769.
Analysts who can fill the table do not need a tool screenshot to argue with an app owner.
Mitigation
- Prefer an allow-list of two SPNs with owners over a dump of thirty.
- Keep
TrustedToAuthForDelegationoff unless the front door is documented non-Kerberos. This lab’s False is the desired default. - 5136 on
msDS-AllowedToDelegateToand onuserAccountControlbits 0x80000 / 0x1000000. - 4769 from a mid-tier host for many distinct
Account Namevalues toward a new SPN: investigate. I do not generate that pattern. - Protect write ACL on those attributes like privileged-group membership.
- Unconstrained (
TrustedForDelegation) is a different plane — see the delegation-edge-cases note. This map assumes it is False on WEB01 (it is).
1PS C:\lab> Get-ADComputer -Filter { msDS-AllowedToDelegateTo -like '*' } |
2 select Name, msDS-AllowedToDelegateTo
3Name msDS-AllowedToDelegateTo
4---- ------------------------
5WEB01 {HTTP/api.lab.internal, CIFS/fs01.lab.internal}
One row in a tiny lab. Production: rank rows by whether the target SPN is a DC, SQL, or a file server with secrets.
What I file after this lab
setspn -LWEB01 / API01 / FS01;-Q HTTP/apione hit- WEB01: constrained to
HTTP/api,CIFS/fs01; no unconstrained; no protocol transition klistlabuser: TGT +HTTP/web01, AES-256, no forged tickets- 4769 pair:
HTTP/web01from10.0.10.50, thenHTTP/apifrom10.0.10.20 - Negative: 4769
cifs/dc01from WEB01 →Failure Code 0xC(not on allow-list) - Out of scope: S4U2Self/S4U2Proxy tooling, PAC forge,
/ptt, RBCD write
Commands appendix
1setspn -L WEB01
2setspn -Q HTTP/api.lab.internal
3klist
4Get-ADComputer WEB01 -Properties TrustedForDelegation, TrustedToAuthForDelegation,
5 msDS-AllowedToDelegateTo
6Get-ADComputer -Filter { msDS-AllowedToDelegateTo -like '*' } |
7 select Name, msDS-AllowedToDelegateTo
8wevtutil qe Security /q:"*[System[(EventID=4769)]]" /c:8 /f:text