User-to-User Authentication: Down the Rabbit Hole – Part 1
TL;DR: This blog post covers Windows internals and how Kerberos user-to-user (U2U) authentication works under the hood versus showing how to execute an attack.
U2U authentication came into the spotlight after the Active Directory Certificate Services (ADCS), UnPAC-the-Hash, and Shadow Credentials attacks. While the attack technique is popular and common amongst red teamers, there’s limited information distilling how and why the technique works. In this blog post, we will cover the intricacies of U2U authentication and how it differs from traditional Kerberos.
Kerberos Primer
Kerberos authenticates clients with tickets so the client never sends their passwords over the wire. The client starts by sending an authentication server (AS) request (AS-REQ) to the Key Distribution Center (KDC) that contains a timestamp encrypted with a key derived from the client’s password. The username itself is sent in plaintext so the KDC can look up the matching encryption key in the Active Directory (AD) database. The KDC uses that key to decrypt and verify the timestamp, which proves the client knows the password.
If verification succeeds, the KDC builds a ticket-granting ticket (TGT), embeds the user’s metadata inside the Privileged Attribute Certificate (PAC), and encrypts the TGT with the KRBTGT encryption key. It also generates a session key and places one copy inside the TGT and a second copy in the EncASRepPart of the AS response (AS-REP), which is encrypted with the client’s encryption key. The client receives the AS-REP, decrypts the EncASRepPart with its own key, retrieves the session key, and caches both the TGT and the session key in Local Security Authority (LSA) Subsystem Service (LSASS).
When the client needs to access a service, it sends a ticket-granting service (TGS) request (TGS-REQ) to the KDC containing the TGT, the target ServicePrincipalName (SPN), and an authenticator encrypted with the session key. The authenticator proves the client actually possesses the session key, not just the TGT. The KDC has no record of past sessions, so the authenticator is what convinces it the requester is the same client the TGT was originally issued to. The KDC decrypts the TGT with the KRBTGT key, extracts the session key, uses it to decrypt the authenticator, and if everything checks out it issues a service ticket (ST). That ST is encrypted with the target service’s long-term key, contains a fresh session key within EncTGSRepPart, and carries the same PAC copied over from the TGT in the ticket-granting service (TGS) response (TGS-REP).
With the service ticket in hand, the client sends an application request (AP-REQ) message containing the service ticket and a new authenticator encrypted with the service ticket’s session key to the target service. The service decrypts the ticket using its own long-term key, recovers the ST’s session key from inside the ticket, and uses it to decrypt the authenticator. The PAC contains information to support authorization decisions like group membership and access rights. Lastly, the service responds with an authentication reply (AP-REP) message encrypted with the session key.

U2U Background
In Windows, services typically run as either built-in local accounts like NT AUTHORITY\SYSTEM or under a dedicated service account. A service account is just a regular user account used to run a Windows service, but it has a SPN registered on it. SPNs are usually assigned to computer or service accounts; regular AD user accounts don’t have one. The KDC uses that SPN to look up the service account and use its long-term key to encrypt the service tickets.
There are cases where a service needs to run under an account that has no SPN. The common reason is peer-to-peer (P2P) authentication, where a user needs to authenticate to a service that another user is hosting on their machine. Remember, regular user accounts don’t have an SPN, so KDC will not be able to locate the associated long-term key and simply return the KDC_ERR_S_PRINCIPAL_UNKNOWN error.
U2U was designed to solve this problem, where a user could still authenticate using Kerberos authentication in P2P scenarios. U2U addresses this problem by encrypting the service ticket using the hosting user’s TGT session key. In the standard Kerberos exchange, service tickets are encrypted with the associated service account’s long-term key. Service tickets issued in a U2U exchange are encrypted with the session key from the additional ticket supplied in the TGS-REQ message.
How does a client get the TGT of the user running the service?
The client first sends a KERB-TGT-REQUEST message directly to the target service. The service responds with a KERB-TGT-REPLY containing its own TGT. The client then submits this TGT to the KDC in the additional-tickets field of a modified TGS-REQ with the ENC-TKT-IN-SKEY flag set. The KDC decrypts the additional TGT using the KRBTGT key, extracts the session key, and uses it to encrypt the new U2U service ticket. The ENC-TKT-IN-SKEY flag tells the KDC to encrypt the service ticket using a session key rather than the service’s long-term key.

Isn’t it dangerous to hand out TGTs?
Not quite. The TGT is encrypted with the KRBTGT account’s long-term key, so the session key inside it is completely inaccessible to anyone without that key. AD randomly generates the KRBTGT password and users and administrators never set it, making it computationally infeasible to crack. If an attacker did crack the KRBTGT key, they could forge TGTs for any account in the domain, which is exactly what the Golden Ticket attack does. That threat exists independently of U2U.
A TGT by itself is unusable because the client needs to possess the session key associated with it. The session key is what encrypts the authenticator in the TGS-REQ, and without it the TGT cannot be exchanged for ST. This is also why the base64 blob you see in Rubeus output is not just a ticket. It is a KRB-CRED structure, which contains the ticket along with the session key, flags, expiration time, and other metadata needed to actually use it. Rubeus exports both TGTs and STs in the same KRB-CRED format for that reason. A raw Kerberos ticket on its own would be incomplete without a session key.
Why session keys are better than long-term keys
The KDC randomly generates session keys and delivers them to the client inside the AS-REP’s EncASRepPart, encrypted with the user’s own key. The KDC does not store them. Their validity is tied to the TGT lifetime, which defaults to 10 hours. When a TGT expires, the account receives a new TGT with a different session key, limiting the usefulness of any U2U ticket. Because session keys are randomly generated and not derived from a password, there is no dictionary or brute-force attack path against them, so an attacker capturing a U2U ticket off the wire would have at most ten hours to crack a random key, which is not a realistic attack surface.
How does the client know U2U is required?
The draft-swift-win2k-krb-user2user draft describes three scenarios in which U2U authentication is initiated, each representing a different point in the authentication flow at which the decision to use U2U is made.
The first scenario is where the client already knows upfront that it needs to use U2U. Instead of attempting a standard TGS-REQ, the client goes directly to the target service and sends a KERB-TGT-REQUEST message. The service responds with a KERB-TGT-REPLY containing its own TGT. The client then uses that TGT to submit a TGS-REQ to the KDC, setting the ENC-TKT-IN-SKEY flag in the additional-tickets field, instructing the KDC to encrypt the resulting service ticket with the session key from that additional TGT rather than the service’s long-term key.
A real-world example of this is when a remote desktop protocol (RDP) server is configured with Network Level Authentication (NLA), in which the client deliberately initiates U2U without waiting for an error from the KDC or the server. Typically, Windows initiates a full login screen for anyone who connects via RDP, so NLA prevents it and requires it to first authenticate before an RDP session initiates.
The second scenario is where the KDC enforces U2U via account policy. The draft states that implementations “MAY” apply a policy on a user account such that the KDC will not issue a conventional service ticket for it. When a client sends a standard TGS-REQ for such an account configured with U2U and the ticket request doesn’t contain the ENC-TKT-IN-SKEY flag and an additional ticket, the KDC returns KDC_ERR_MUST_USE_USER2USER (0x1B). The client must then fall back to the U2U path to obtain the user account’s TGT via KERB-TGT-REQUEST, include it as the additional ticket, and retry the TGS-REQ with ENC-TKT-IN-SKEY set.
The third scenario is where the server enforces U2U after the client completes the TGS exchange with the KDC. In this case, the client does not know U2U is required, requests a conventional service ticket, and presents it to the server in a KRB_AP_REQ. The server rejects it and responds with a KRB_AP_ERR_USER_TO_USER_REQUIRED error (0x45), optionally embedding its own TGT in the error data. The client then restarts the exchange using the U2U path.
Where the draft falls short of reality
To our knowledge, Windows has not implemented all scenarios described in the draft RFC. The first scenario is where the client already knows that it needs to get the TGT from the target service, which must be hard-coded in the application. The client proactively requests the target service’s TGT via a KERB-TGT-REQUEST message, then does a U2U exchange. While researching U2U, we identified that this exact scenario was implemented in RDP with an NLA setting.
The second and third scenarios do not hold up as described in the draft. The draft states that implementations “MAY” apply a per-account policy that forces U2U, but in Windows, there is no such policy. There is no attribute on a user object, no userAccountControl flag, or no Group Policy setting that tells the KDC to enforce U2U for a specific account.
Upon further investigation of KDC_ERR_MUST_USE_USER2USER in kerberos.dll, we found that the error does not exist. We manually tried to make the KDC return the error, but it failed to trigger. We requested a TGS for a user account by providing the UserPrincipalName (UPN) in the SPN field and tried an empty SPN, but the KDC kept returning KDC_ERR_S_PRINCIPAL_UNKNOWN.
The KDC_ERR_MUST_USE_USER2USER error, by itself, does not tell the client much. The client still needs the TGT for the target service to construct the U2U TGS-REQ, but the KDC cannot provide it. KDC doesn’t keep TGTs after issuance. Once the AS-REP message is sent, the KDC doesn’t track where the TGT goes or which machine the user logged into. The client could in theory take a guess and reach out to the hostname embedded in the SPN it originally requested, on the assumption that the service is running on that host and might be willing to hand over its TGT through a KERB-TGT-REQUEST. That guess would often be right, but it relies on the client inferring intent from the SPN rather than the KDC pointing it anywhere explicitly. The error tells the client that U2U is required without giving it any direct path to the material it needs to actually perform U2U.
The third scenario, where the server returns KRB_AP_ERR_USER_TO_USER_REQUIRED in response to an AP-REQ, is more conditional than the draft implies. In a standard Windows environment, the KDC detects the missing SPN condition during the TGS exchange and returns KDC_ERR_S_PRINCIPAL_UNKNOWN before a service ticket is ever issued, so the client never reaches the point of presenting an AP-REQ to the server. However, this server-side error path is not entirely fictional. It applies when a service is hardcoded to require U2U and explicitly returns KRB_AP_ERR_USER_TO_USER_REQUIRED upon receiving a conventional AP-REQ, regardless of how the user obtained a TGS, whether the KDC checked for it. In that case, the client receives the error directly from the service rather than the KDC, and must restart the exchange using U2U. The distinction matters because the trigger here is application-level logic inside the service, not a missing SPN or a KDC policy.
The draft was written as a general specification for Kerberos implementations, and Microsoft chose to implement U2U enforcement earlier in the flow rather than at the AP layer. So, while all three scenarios are technically defined, only the first scenario, where the client deliberately initiates U2U, is the path you will consistently observe in Windows.
The Curious Case of RDP With NLA
The most common place to observe legitimate U2U in the wild is RDP when the target server has NLA enabled. NLA requires authentication to complete before the server renders a login screen, which protects it from spinning up a full desktop session for an unauthenticated connection. To do this, NLA relies on CredSSP to forward the user’s credentials to the server in encrypted form over a protected channel.
To understand exactly how U2U fits in, I configured a host with NLA in my lab, captured traffic in Wireshark, and traced the full exchange.
After the initial RDP handshake and TLS tunnel are established, the client requests Kerberos tickets and begins CredSSP negotiation. The first thing that stands out is the RDP client makes a standard TGS-REQ for TERMSRV/hostname.domain.local and receives a valid service ticket in the TGS-REP.

That ticket is then never used or cached in the logon session. The client immediately follows up by contacting the RDP server directly to request its TGT, which is the actual start of the U2U exchange. One thing to note here is that there are no errors from the RDP server or the KDC that tell the client to use U2U. The client initiates U2U because CredSSP calls InitializeSecurityContext with the ISC_REQ_USE_SESSION_KEY flag. This flag requires a new session key to be used for a security context.

The RDP server responds with a KERB-TGT-REPLY containing the computer account’s TGT:

Both the KERB-TGT-REQUEST and KERB-TGT-REPLY messages are wrapped inside SPNEGO tokens, which are carried inside CredSSP TSRequest structures over the established TLS channel. This means the TGT exchange happens entirely within the encrypted CredSSP negotiation and is not visible as raw Kerberos traffic on port 88/TCP. The client sends the request as part of its first SPNEGO token, and the server’s TGT comes back in the server’s SPNEGO response, all before any credentials are forwarded.
With the RDP server’s TGT in hand, the client sends a TGS-REQ to the KDC. This request includes the RDP server’s TGT in the additional-tickets field and sets the ENC-TKT-IN-SKEY flag in kdc-options, which tells the KDC to encrypt the resulting service ticket using the session key from that additional TGT rather than the server’s long-term key. One interesting detail here is that the sname field still references a valid SPN, TERMSRV/castelblack, but the KDC ignores it entirely once ENC-TKT-IN-SKEY is set. The ticket is issued for the castelblack$ computer account and encrypted with the session key from its TGT.


The TGS-REP carries back the service ticket with the same sname that was in the TGS-REQ, so it still reads TERMSRV/castelblack. This is worth noting because the expired draft specifies that the sname in a U2U exchange should reflect the UPN of the target service, which in this case would be castelblack$@north.sevenkingdoms.local. Further testing with Rubeus confirmed that whatever value is supplied as the target is what gets reflected back in the sname of the issued ticket.

Lastly, the client sends the AP-REQ message to the RDP server. The AP-REQ message would be wrapped inside the CredSSP protocol and the AP-REQ message indicates that the service ticket is encrypted with a session key. The AP-OPTIONS inside the AP-REQ message sets USE-SESSION-KEY to true, letting the server receiving the ticket know which encryption keys to use to decrypt the ticket. So, the RDP server obtains the session key from LSASS and decrypts the AP-REQ message to authenticate the user, which encryption keys to use to decrypt the AP-REQ message.

The need for U2U in RDP with NLA is debatable because a client can already request a service ticket using the TERMSRV/CASTELBLACK SPN. So, why use U2U authentication? The CredSSP protocol does unconventional things like this, and I believe the only legitimate reason for doing so is the non-cacheable service tickets. U2U tickets are not cached and are short-lived because their lifetime is tied to the TGT lifetime. The session key is randomly generated by the KDC and is generated fresh whenever a ticket is requested.
As mentioned, CredSSP calls the InitializeSecurityContext Windows API with the ISC_REQ_USE_SESSION_KEY flag in order to obtain a new session key, and the way Windows does this is by performing a U2U exchange. The new session key is then used to deliver the client’s plaintext credentials. If an existing, cached session key were reused, there would be a risk of an attacker extracting it, and if they were also intercepting the traffic, they could decrypt the CredSSP exchange.
Lastly, when a host is configured with NLA before any RDP session is spawned, the user must authenticate. It prevents pre-auth RDP vulnerabilities like BlueKeep. Before anything spawns, the user must first authenticate.
UnPAC-the-Hash
When a user logs in via Public Key Cryptography for Initial Authentication (PKINIT) authentication, the user’s NT hash is included in the PAC inside the TGT. The NT hash is included to support NTLM-based authentication for legacy services that don’t support Kerberos authentication.
In PKINIT, the user authenticates with a certificate rather than a password. The LSA on the client host obtains a TGT from the KDC using the certificate. Because the user never provided a password, the client host has no NT hash to cache locally for the msv1_0 authentication package. So Microsoft decided to include the user’s NT hash into the PAC only if the TGT is obtained via certificate. The PAC is inside the TGT or service tickets and is encrypted with either KRBTGT or the service account’s encryption keys, respectively. Inside the PAC is the PAC_CREDENTIAL_INFO structure, which holds the NT hash and is itself encrypted with a separate key commonly known as “AS reply key”. In the screenshot below, you can see that the PAC_CREDENTIAL_DATA structure is in encrypted form because we need the “AS” reply key to decrypt it and this key is only temporarily stored in the LSASS.

The AS reply key comes out of the key derivation function (KDF) after feeding in the Diffie-Hellman shared secret along with two nonces (server and client). This key encrypts the EncASRepPart of the AS-REP message in PKINIT authentication. Traditionally, the EncASRepPart is encrypted with the user’s encryption key to deliver the session key, which the client uses to encrypt the authenticator in subsequent ticket requests. In PKINIT, the AS reply key is used to deliver the session key.
After the AS exchange, the LSA on the client host derives the AS reply key from the Diffie-Hellman (DH) shared secret and the two nonces it collected during the PKINIT exchange. With the AS reply key, it decrypts the EncASRepPart of the AS-REP and retrieves the TGT session key. At this point, the LSA holds the TGT, the TGT session key, and the AS reply key, but the PAC inside the TGT is still inaccessible because the TGT is encrypted with the KRBTGT key. To decrypt the PAC_CREDENTIAL_INFO, the LSA requests a service ticket for itself using the computer account’s SPN (HOST/FQDN). The below image shows that immediately after an AS-REP message the client did a TGS-REQ message with the HOST/workstation01.essos.local SPN.

The KDC issues that ticket by copying the PAC from the TGT into the new service ticket and re-signing it, then encrypts the ticket with the computer account’s long-term key. The LSA decrypts the service ticket using that key, accesses the PAC, and decrypts PAC_CREDENTIAL_INFO with the AS reply key to retrieve the NT hash. The NT hash is then cached in the logon session for NTLM authentication. The AS reply key lives in LSASS memory only for as long as the LSA needs it to complete the decryption process of the service ticket.
The same primitives that make U2U authentication work are also what make the UnPAC-the-Hash attack possible. To perform this attack, we must first obtain a certificate issued by the domain’s Certificate Authority (CA), which is typically done by exploiting one of the ADCS ESC or shadow credentials attacks or by dumping a TGT obtained via PKINIT from a workstation.
Once we have obtained the target user’s certificate and its corresponding private key, we initiate our own PKINIT exchange against the KDC. During this exchange, we generate an ephemeral DH key pair that is entirely under our control. The KDC responds with an AS-REP, and from that response we derive the AS reply key using the same process that the Windows LSA performs natively. This gives us a valid TGT along with its associated session key.
With the TGT and session key in hand, we then request a U2U service ticket for ourselves. Because this is a U2U request, the KDC encrypts the service ticket using our TGT’s session key rather than a long-term service account key. Since we already hold the TGT session key, we can decrypt the service ticket entirely on our own.
Once decrypted, we gain access to the PAC which is embedded within the ticket. The PAC_CREDENTIAL_INFO structure is inside the PAC, which contains the user’s NT hash. This structure is encrypted using the AS reply key, which we previously derived during the PKINIT exchange. In the image below, we used this key to decrypt PAC_CREDENTIAL_INFO and retrieve the user’s NT hash.

Many of the ADCS ESC attacks chain naturally into UnPAC-the-Hash, and once the NT hash is recovered, it can be used for Pass-the-Hash (PtH) or to request Kerberos tickets directly, if RC4 encryption is enabled.
Ohhhhhh wait there is more about U2U but we will cover that in part 2 of this blog post. Thanks for reading and stay tuned for the next blog.
Special thanks to Duane Michael and Elad Shamir.