Kerberos Love Story

· Active Directory Kerberos Microsoft · 18 min read

Contents

Intro

I originally wanted to write a blog post about the U2U authentication, since I felt its a topic that isn’t covered very often. However, as I started writing about U2U, it quickly escalated into me explaining how Kerberos works overall ^^'.

So instead of focusing only on U2U, this blog post will walk through Kerberos in general, the S4U extensions AND the U2U authentication! Let’s dive in I’d say ^^.

As always, feel free to correct me if I’m wrong about anything!

Kerberos TLDR (or not)

Before diving into U2U, let me do a refresher on how a (typical) Kerberos ticket exchange looks. If you’re already comfortable with Kerberos, feel free to skip to U2U Authentication.

I assume you know what Kerberos is and what it does at a high level overview.

You can split the whole “getting a service ticket” flow into six logical steps:

  1. AS-REQ (Authentication Request)
  2. AS-REP (Authentication Response)
  3. TGS-REQ (Ticket Granting Service Request)
  4. TGS-REP (Ticket Granting Service Response)
  5. AP-REQ (Application Request)
  6. AP-REP (Application Response)

A lot of those steps seem to be similar, and in fact, some steps share a similar packet structure (AS-REP and TGS-REP for example).

Ticket Exchange Deep Dive

Steps 1 & 2 ensures that the user authenticates itself to the KDC (Key Distribution Center). The client sends the following:

  • Its name (the client principal name)
  • The realm name
  • The desired service, which is in case of AS-REQ always KRBTGT
  • A randomly generated value (called a Nonce)
  • And finally, if pre-authentication is configured, an encrypted timestamp with the clients secret

So the first three points are easy to understand. The Nonce exists to prevent replay attacks. This value should ONLY exist in that specific AS-REQ/REP exchange, and in no other one. If it does, some sort of replay attack has been performed.

This is not entirely true. Technically you could re-use a valid pre-auth blob within a tiny window. The KDC won’t be able to tell if it was freshly generated or not. To address this issue, RFC 8070 introduced the PA-AS-FRESHNESS token.

Pre-authentication is what everyone refers as when an account is “ASREP roastable” or not. In an Active Directory environment, this is a checkbox you can see when opening the properties of an user. Pre-authentication is per default always required (as it should be!). 1

Here’s what a flow looks like in my home lab:

# Request a TGT
kinit ulqiorra

This may seem confusing at first, so let’s break it down.

The first AS-REQ doesn’t contain any PDATA value, which is the pre-authentication we talked about.

So the KDC replies with KRB5KDC_ERR_PREAUTH_REQUIRED, meaning that pre-authentication is needed for this user account. In that same reply, the KDC also indicates what encryption algorithm and what salt it should use to create (the better term would be “to derive”) the key from the user’s password.

If you’re curious, this is located in the PA-ETYPE-INFO2 structure.

That key is then used to encrypt the pre-authentication data, which is simply the current timestamp. So a new AS-REQ is built, but this time with the pre-authentication data filled (focus on the cipher entry):


The KDC response is KRB5KRB_ERR_RESPONSE_TOO_BIG which occurs because the whole exchange was through UDP. The AS-REQ is sent again (for a third time bruh), but this time via TCP. Finally, the KDC responds with an AS-REP, containing our long awaited Ticket Granting Ticket (TGT).

In modern environments, DCs (Domain Controllers) use directly TCP instead of UDP.

The second enc-part structure can be decrypted by using the derived key that the client created earlier. This contains among other things the session key, which will be used in further exchanges. The actual TGT (enc-part in the ticket structure) cannot be decrypted, since it has been encrypted with the KRBTGT account long term key (password basically). It’s worth noting that a copy of the session key is also stored inside the ENC-PART structure of the TGT.

By using a keytab file, you could feed that into Wireshark and actually see the contents of those encrypted structures. But I was too lazy for that, sry.

Now the client can happily request service tickets (STs), which are steps 3 & 4 of our initial six steps flow.

So let’s request a service ticket:

# Requesting a ST
kvno 'cifs/dc01.hueco.mundo'

This whole process with the session key was needed for exactly this moment. When you request a ST (for example CIFS/DC01.HUECO.MUNDO), you’ll find a new structure called the Authenticator in the TGS-REQ.

This whole structure is encrypted with that specific session key. Inside it, you’ll find information such as the realm name, the principal’s name (client name) and the current timestamp (again..to counter replay attacks).

You’ll also notice the REQ-BODY structure, which contains (among other things) the service we want a ticket for and for how long you want it to be valid.

You’ll notice that the REQ-BODY structure is NOT encrypted. Alberto Solino discovered that you can manipulate the SNAME field and inject whatever value you want. This is what the /altservice command in Rubeus does when you request a Service Ticket.

At this point, since the KDC decrypted the TGT (proving it came from the KDC) and the Authenticator (proving it’s really you who sent the TGS-REQ), it provides a service ticket within the TGS-REP (finally!).

Here comes the slightly confusing part: inside the ENC-PART (the structure we can decrypt with the first session key), there’s a brand new session key.
That same key is also included in the ENC-PART of the ticket (the one we cannot decrypt). So yeah.. there are now two session keys - but for different purposes.

The new session key is gonna be used when interacting with the service itself (steps 5 & 6 from the six steps flow). In my case that’s CIFS/DC01.HUECO.MUNDO.

The same logic as before applies here: the difference is that the client sends an AP-REQ to the service directly (no more interaction with the KDC).

The ticket structure is decrypted by the service, giving it access to that new session key. With that key, the service can decrypt the Authenticator structure and verify everything else. If all goes well, you’re in: access granted to CIFS on DC01.HUECO.MUNDO! ^^

S4U2Self

S4U2Self (and S4U2Proxy) are extensions to the Kerberos protocol. They are Microsoft-specific and are not part of the Kerberos RFC.

S4U2Self allows a service to obtain a service ticket to itself on behalf of another user.

This means a service account (machine accounts count as service accounts btw) can impersonate any user for that specific service.

From an offensive perspective, this means that once we control a service account (or a machine account!), we can impersonate whoever we want in the context of that service (or the whole machine).

S4U2Self still works for users in the Protected Users group. Protected Users blocks delegation (S4U2Proxy / forwarding) but does not prevent a service from requesting a service ticket to itself on behalf of a user. In practice this means you can still get a ticket containing the user’s PAC, but you won’t be able to get a forwardable ticket for proxy/delegation purposes.

Let’s assume we’ve compromised the machine account KAGAMINO$:

nxc smb 10.10.50.153 -u 'kagamino$' -H 14e8f5bd514dcc4dfd3de8854bc16744

We’ve effectively gained admin-like power over that machine because we can now request a service ticket to ourselves on behalf of another user. Thus, we can perform an S4U2Self to impersonate the Domain Administrator, for example:

getST.py -self -impersonate 'aizen' -altservice 'CIFS/KAGAMINO.HUECO.MUNDO' -hashes :14e8f5bd514dcc4dfd3de8854bc16744 hueco.mundo/'kagamino$'

It should be clear by now but as a reminder: S4U2Self (almost) always requires a valid SPN to work!

We can verify the ticket works:

KRB5CCNAME=aizen@CIFS_KAGAMINO.HUECO.MUNDO@HUECO.MUNDO.ccache nxc smb kagamino.hueco.mundo -u aizen --use-kcache

You can also do this in one step with NetExec:

nxc smb 10.10.50.153 -u 'kagamino$' -H 14e8f5bd514dcc4dfd3de8854bc16744 --delegate 'aizen' --self

At the packet level, you’ll see that Microsoft added a PA-FOR-USER structure in the TGS-REQ:

If the user aizen is confirmed (through username + realm), the KDC will issue a service ticket.

Worth mentioning: S4U2Self doesn’t require any special AD configuration. It works “by default”.

S4U2Proxy

S4U2Proxy allows a service to obtain a service ticket on behalf of a user.. but for another service (hence the denomination “proxy”).

This is effectively known as delegation:

  • Constrained Delegation
  • Resource-based Constrained Delegation

In Active Directory, a delegation could look like this:

This specific configuration shows that the computer account KARAKURA is allowed to impersonate (almost) whoever it wants on the CIFS service on SRV1. This is known as Constrained Delegation (with protocol transition).

Users in the Protected Users group are not allowed to be delegated, so they cannot be impersonated. There’s also a single checkbox on a user’s properties that can disallow delegation without adding them to the Protected Users group.

I won’t dive deeply into what the other delegation types do. I’d rather focus on what’s happening behind the scenes when an S4U2Proxy occurs.

Let’s first see what happens from a high-level perspective:

  1. The first step is to obtain a forwardable service ticket. This is commonly done through an S4U2Self. (Later we’ll see it’s also possible to obtain one through U2U.) Through S4U2Self we can impersonate another user in this step.
  2. Now the actual S4U2Proxy step happens. Remember: S4U2Proxy gets a service ticket on behalf of another user for another service.
  3. A new TGS-REQ is made.
  4. Inside this request, there’s a structure called additional-tickets. This effectively includes the service ticket we requested earlier. The sname field contains the target service.
  5. The KDC performs checks. If everything is OK, it returns a new service ticket. With that ticket, the requester can authenticate to the remote service.

This schema summarizes the process:

This whole process (without step 5 from the earlier list) looks like this in Wireshark:

Steps 0-2 should be clear now. Let’s look at step 3:

We can see the following:

  • The ticket, which includes the TGT of service 1 (in my case, KARAKURA$)
  • The authenticator structure, used for validation
  • The sname structure, containing the target service 2 (in my case, SRV1)
  • The new structure we talked about: additional-tickets, containing the service ticket from step 2

First the KDC validates our own TGT (decrypts the ticket structure, extracts the session key, and then decrypts the authenticator). Once the KDC knows who is making the request, it parses and decrypts the additional-tickets structure. For each ticket in additional-tickets, it:

  • Decrypts it using the secret key of service 1
  • Pulls out the user identity, times, flags, and the copy of the PAC

The KDC also checks if service 1 is trusted for delegation and if the user is delegable. If all checks pass, we receive a new service ticket!

From an offensive perspective, this is just typical delegation abuse:

getST.py -spn 'CIFS/SRV1.HUECO.MUNDO' -impersonate 'ulqiorra' -hashes :38b3aaa27a941e47a47ff8b1cdd47019 hueco.mundo/'karakura$'

We can also abuse this directly with NetExec:

nxc smb srv1.hueco.mundo -u 'karakura$' -H 38b3aaa27a941e47a47ff8b1cdd47019 --delegate 'ulqiorra'

At this point you should (hopefully) understand what S4U2Self and S4U2Proxy are doing.

If constrained delegation is configured without protocol transition, then the S4U2Self will produce a service ticket with the NOT FORWARDABLE flag. This means that S4U2Proxy will automatically fail. I will not go into much details, but one technique to “bypass” this behavior is to get the initial service ticket through resource-based constrained delegation (RBCD). RBCD always produces forwardable service tickets.

You can find more info here: https://www.thehacker.recipes/ad/movement/kerberos/delegations/constrained

U2U Authentication

U2U is not an extension, it’s actually part of the Kerberos RFC and has nothing to do with S4U. The RFC defines it like this:

User-to-User authentication provides a method to perform authentication when the verifier does not have a access to long-term service key. This might be the case when running a server (for example, a window server) as a user on a workstation. Source: RFC 4120 §3.7

In other words: imagine a scenario where user A hosts a service on a server, and another user B wants to access that service. User B can use U2U, which is just a variation of TGS-REQ to request access to that service. This is needed because user A (the service) doesn’t have a long-term secret.

Instead of issuing a service ticket encrypted with the service’s long-term secret, the KDC issues a ticket encrypted with the target user’s TGT session key.

Let’s see an example:

I’m defining Alice’s TGT as TGT_A and Bob’s TGT as TGT_B

  1. Alice wants to access a service that Bob is hosting.

  2. Alice builds a TGS-REQ to the KDC asking for a service ticket to access the service hosted by Bob.

    • Inside the TGS-REQ, a new flag ENC-TKT-IN-SKEY is set to indicate this is a U2U authentication.
    • There’s also an additional-tickets structure containing TGT_B.
  3. The KDC proceeds the following way:

    1. It decrypts TGT_A and extracts the session key.
    2. It decrypts the authenticator of TGT_A and verifies her identity.
    3. It decrypts the TGT_B.
    4. It builds a service ticket but encrypts it with the session key of TGT_B.
    5. It builds the enc-part for Alice and encrypts it the session key of TGT_A and returns a TGS-REP.
  4. Alice now holds a service ticket that only Bob can decrypt.

As you can see, the flow is very similar to a normal TGS-REQ. Now the question is: where does Alice get Bob’s TGT? In a real world scenario,that usually comes via the application-layer (whatever the service is basically). The service (Bob) requests its own TGT and provides it to the client during the application handshake.

Let’s see how the flow looks like.

I simulate a scenario where the user grimmjow is running a service with the SPN ilove/bleach.

setspn.exe -U -S ilove/bleach grimmjow

Next we need the TGTs of grimmjow and another user kon.

getTGT.py -dc-ip 10.10.50.4 hueco.mundo/grimmjow:'PJ9p[p23'
getTGT.py -dc-ip 10.10.50.4 hueco.mundo/kon:'wekjfhasjkdcvbakj'

We can now use the u2u.py script to simulate a U2U authentication and get a service ticket.

You can find my script on my github, if you wanna play around U2U aswell:
Link: https://github.com/mochabyte0x/kerberos-playground

KRB5CCNAME=kon.ccache python3 u2u.py -s 'ilove/bleach' -a grimmjow.ccache --dc-ip 10.10.50.4 --save u2u_service_ticket.ccache

We can then inspect the ticket:

describeTicket.py u2u_service_ticket.ccache

You can indeed see that the service ticket was issued for the user kon and the service ilove/bleach.

Let’s also take a look at Wireshark:


In Wireshark you can clearly see:

  • The additional-tickets structure populated with the correct TGT.
  • The ENC-TKT-IN-SKEY flag set in kdc-options.

The TGS-REP itself looks normal from the client’s perspective. But remember: the ticket’s enc-part is encrypted under TGT_B’s session key, so only the holder of TGT_B (Bob) can decrypt it.

I could now present that ticket to the service hosted by grimmjow and get access to it.

Offensive U2U

From an offensive perspective, U2U has relatively limited usage. There are some interesting use cases however that I will cover. Starting with the first one:

UnPAC The Hash

I didn’t talk much about the PAC (Privilege Attribute Certificate) yet, so let’s catch up quickly.

The PAC is a Microsoft-specific structure (not part of the Kerberos RFC) that contains a bunch of information about an account like group memberships, alternate credentials for non-Kerberos auth protocols, and more. Windows uses the PAC to build the user’s access token. The PAC is present in the AS-REP and is copied into the TGS-REP later.

There’s a lot more to say about PACs, but that’s enough to understand the UnPAC The Hash trick.

When a user obtains a TGT via PKINIT (certificate-based authentication), the KDC includes a special structure called PAC_CREDENTIAL_INFO inside the PAC. Now..what’s special about PAC_CREDENTIAL_INFO? It actually contains the user’s NTLM hash (encrypted).

typedef struct _PAC_CREDENTIAL_INFO {
   ULONG Version;
   ULONG EncryptionType;
   UCHAR SerializedData[1];
} PAC_CREDENTIAL_INFO, *PPAC_CREDENTIAL_INFO;

The NTLM inside PAC_CREDENTIAL_INFO is encrypted using the AS-REP encryption key. That normally makes it basically impossible for an attacker to extract, because the TGT part that we’re interested in is encrypted with the krbtgt secret.

So here comes U2U!

The PAC lives in the AS-REP reply as stated earlier, more specifically in the encrypted part with the krbtgt secret. This means it’s practically impossible to recover the PAC_CREDENTIAL_INFO structure. By using U2U to ourselves, the TGS-REP part is being encrypted with our TGT session key, which we have access to! This means we can decrypt the structure, get the PAC and then parse the PAC_CREDENTIAL_INFO structure!

This is exactly what getnthash.py (from the PKINIT toolkit) or Certipy’s auth are doing.

https://github.com/dirkjanm/PKINITtools/blob/master/getnthash.py#L192 https://github.com/ly4k/Certipy/blob/main/certipy/commands/auth.py#L723

Pretty cool trick!

If you have access to a user account and want to recover its NT Hash (and PKINIT + RC4 are enabled), you could then always use this trick.

  1. Shadow Credential attack to ourselves
  2. Use the PKINIT toolkit to get the NT Hash

Sapphire Ticket

Sapphire Tickets are very similar to Diamond Tickets, the only difference resides in the PAC. In Diamond Tickets, the legitimate PAC is being modified (imo still very unlikely to be detected, but who knows). The Sapphire Ticket doesn’t modify the PAC, it instead uses S4U2Self + U2U to get the PAC of another powerful user. This PAC is then replacing the one in the legitimate ticket. This makes the Sapphire Ticket the most difficult ticket attack to detect.

We can use ticketer.py to create one:

ticketer.py -request -impersonate 'Administrator' -domain HUECO.MUNDO -user grimmjow -password 'PJ9p[p23' -nthash 55e001d4590fdc2b3e8864a134c185fd -aesKey b1fb831505c389750ae0786d1ee2d6d7efd4ebd24938b8d2cd1036149a7fe020 -user-id '500' -domain-sid 'S-1-5-21-3940589903-3445663124-3948096515' -dc-ip 10.10.50.4 sapphire

This whole process looks like this in Wireshark:

The AS-REQ & AS-REP are for the user grimmjow in this case. This is later used to authenticate to the KDC.

The interesting part is in the TGS-REQ:

As you can see, the TGS-REQ is a mix of S4U2Self and U2U. We can confirm this by looking at the kdc-options aswell:

U2U is used here to request a service ticket to ourselves, and to tell the KDC to encrypt the enc-part of that service ticket with the TGT session key of grimmjow. This way, its then possible for us to decrypt the resulting service ticket and get access to the PAC.

I also mentioned earlier that S4U2Self always requires a valid SPN. Well, not in combination with U2U, since it eliminates the need of a long term secret.

You can also forge Sapphire Tickets for users being part of the Protected Users Group. It doesn’t matter in this case, because we aren’t doing anything related to delegations that requires a forwardable TGT.

Here I forged a Sapphire Ticket for the user Aizen, which is part of the Protected Users group as you can see:

And I can indeed uses this ticket to log-in wherever I want:

SPN-less Delegation

TLDR: https://www.tiraniddo.dev/2022/05/exploiting-rbcd-using-normal-user.html https://www.thehacker.recipes/ad/movement/kerberos/delegations/rbcd#rbcd-on-spn-less-users

In 2022, James Forshaw showed that it’s possible to abuse RBCD without the SPN requirement by using a normal user. This trick can be very handy if MachineAccountQuota is set to 0.

This attack can be performed in certain Constrained Delegation and certain RBCD scenarios. Contrary to some beliefs, Constrained Delegation & RBCD perform the same Kerberos-related steps. The only difference is where the delegation right is configured.

On a high level overview, when performing RBCD, you write to a computer account’s msDS-AllowedToActOnBehalfOfOtherIdentity attribute and add another account (with an SPN!) there. That added account can later act on behalf of others for that computer account.

Now, the standard S4U2Self → S4U2Proxy delegation path expects a service target (an SPN). If you try S4U2Self for a user UPN (a user principal name) without an SPN, you’ll typically get an error like KDC_ERR_S_PRINCIPAL_UNKNOWN. The KDC doesn’t know which long-term key to use for that principal.

The workaround is to use U2U. The KDC will then encrypt the returned ticket with the target user’s TGT session key. Recall the S4U2Self + U2U combo from the Sapphire Ticket discussion. That means it’s possible to get a service ticket for the user even without an SPN.

Now there’s a new problem: the S4U2Proxy step fails because the KDC can’t decrypt the service ticket passed in additional-tickets. It’s encrypted with the user’s TGT session key, which the KDC doesn’t have. Re-encrypting/decrypting locally doesn’t help either because the PAC contains a Server Signature that you cannot (easily) reproduce.

So what now? James Forshaw found a neat trick here:

What if the user’s password equals the TGT session key? Then the KDC can decrypt the service ticket (in RC4 scenarios), because the session key and the user’s key are effectively the same.

This attack chain is very specific and has a few drawbacks:

  • It only works if RC4 is enabled (Windows Server 2025 has disabled RC4 per default).
  • You need the user’s password (or NT Hash).
  • You break the user’s account: changing its password to match the session key renders the account unusable.
  • It’s definitely not stealthy.

Detection

U2U is rare in production, so detection should focus on the specific TGS-REQ patterns it creates (instead of generic Kerberos noise). You should also monitor S4U operations and AD modifications that enable delegation.

You can also build detection rules around S4U2Self attempts to unexpected principals (e.g. Domain/Enterprise Administrators).

Same can be said about S4U2Proxy. You can monitor those TGS-REQ to unexpected principals.

Catching RBCD is also a possibility by monitoring LDAP modifications to msDS-AllowedToActOnBehalfOfOtherIdentity attributes. You can also monitor for machine account creation or generally unexpected ACL write modifiction.

So to sum it up, monitor these if possible:

  • Event ID 4769: TGS request.

    • Look for KDCOptions containing enc-tkt-in-skey (U2U).
    • Look for padata entries with PA-FOR-USER (S4U2Self).
  • Event ID 4768: TGT request (useful to correlate TGT issuance to later TGS usage).

  • Event ID 5136: Directory Service Changes (LDAP modify).

    • Monitor changes to msDS-AllowedToActOnBehalfOfOtherIdentity and msDS-AllowedToDelegateTo ACLs.
  • Event ID 4720: User/computer account creation (watch unexpected machine/account creation).

  • Event ID 4662 / 4670 if you capture specific object access/ACL modifications in detail.

Conclusion

I could talk a lot more about U2U or Kerberos in general. Kerberos is complex, and I still have a ton to learn. I hope this post gave you the motivation to run your own Kerberos experiments (or AD in general!). Try the impacket library and build a couple of small tools of your own! Hope you enjoyed it!

If you have any questions, feel free to contact me on Discord, X or Linkedin!

Discord: mocha
X: mochabyte0x
Linkedin: Arthur Minasyan

References

https://datatracker.ietf.org/doc/html/rfc4120 (03.11.2025)
https://web.archive.org/web/20240915121302/http://www.di-srv.unisa.it/~ads/corso-security/www/CORSO-0001/kerberos/ref/kerberos-faq.html#u2uauth (03.11.2025)
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/aceb70de-40f0-4409-87fa-df00ca145f5a (03.11.2025)
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/02636893-7a1f-4357-af9a-b672e3e3de13 (03.11.2025)
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/bde93b0e-f3c9-4ddf-9f44-e1453be7af5a (03.11.2025)
https://datatracker.ietf.org/doc/html/draft-ietf-cat-user2user-02 (03.11.2025)
https://www.thehacker.recipes/ad/movement/kerberos/unpac-the-hash (03.11.2025)
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-apds/a00d0b83-97e3-44ad-ba2d-1221d4f51a35#gt_26456104-0afb-4afe-a92e-ac160a9efdf8 (03.11.2025)