Service principal names (SPNs) are records in an Active Directory (AD) database that show which services are registered to which accounts:
If an account has an SPN or multiple SPNs, you can request a service ticket to one of these SPNs via Kerberos, and since a part of the service ticket will be encrypted with the key derived from the account’s password, you will be able to brute force this password offline. This is how Kerberoasting works.
There is a way to perform the Kerberoasting attack without knowing SPNs of the target services. I’ll show how it could be done, how it works, and when it could be useful.
Kerberos Basics
Kerberos is an open source binary protocol based on the ASN.1 format. The core of Kerberos is key distribution center (KDC) services, which use 88/tcp and 88/udp ports. In the Active Directory environment they are installed on each of the domain controllers.
Let’s run the GetUserSPNs.py tool from Impacket to demonstrate how Kerberoasting works:
First, the tool connects to LDAP, and finds users which have SPNs and which are not machine accounts. Every machine account in the AD has a bunch of SPNs, but their service tickets are not brute-forceable because machine accounts have passwords that are 240 bytes long.
Then, the tool connects to a KDC, and for each of the discovered accounts gets a service ticket using one of its SPNs. In our example only one account was discovered, and the tool chosed “MSSQLSvc/sp-sql:1433” SPN to request a ticket.
It’s not important whether chosen services are functioning; the existence of an SPN in the AD database is sufficient for the attack.
Here is the traffic dump of this GetUserSPNs.py launch, so now we can examine all the described stages in detail:
How clients get TGTs
Each client must authenticate to the KDC and obtain a ticket-granting ticket (TGT), which will allow them to ask for any number of service tickets going forward.
This mechanism is used to reduce the number of needed authentications, and there is no way to bypass it and request a service ticket without having a TGT.
Unauthenticated AS-REQ / Preauth Request
AS-REQ packets serve to ask for TGTs.
In AS-REQ clients specify the special “krbtgt/DomainFQDN” SPN in the sname field, and the principal name of the account to which the TGT is being requested for in the cname field:
The first AS-REQ packet is sent without authentication data to maintain backwards compatibility. It will succeed only if the DONT_REQ_PREAUTH flag in the Active Directory for the target account is set.
The response for AS-REQs should contain a structure that is encrypted and signed with the key derived from the client account’s password, so if AS-REQs worked without any authentication, anyone would be able to brute force anyone else’s password offline.
This is called an ASREPRoasting attack, and in Impacket it can be performed by the GetNPUsers.py script:
One application of ASREPRoasting is Targeted Kerberoasting. It relies on intentionally setting the DONT_REQ_PREAUTH flag for accounts you control in the AD, and getting their $krb5asrep$ hashes.
Since the “Administrator” account we used doesn’t have the DONT_REQ_PREAUTH flag set, the KDC sent a KRB-ERR packet to the client with the KRB_PREAUTH_REQURED error. This packet is called Preauth Request.
If the “Administrator” account didn’t exist, we would get the KDC_ERR_C_PRINCIPAL_UNKNOWN error. This is the feature that is used in Kerberos User Enumeration attacks.
Authenticated AS-REQ
Let’s examine the next AS-REQ packet:
The next AS-REQ is basically the same request as the first one, but it contains data which could authorize the client. This data is a special structure that contains the current timestamp, and this structure is encrypted and signed with the key derived from the account’s password.
Keys derived from account’s passwords are known as Kerberos Keys, and they’re calculated differently depending on the utilized encryption algorithm:
- AES-128 and AES-256: the key is calculated from the PBKDF2 hash of the password
- RC4: the key is calculated from the NT hash of the password (always used with the Pass-The-Hash attack)
- DES: the key is calculated directly from the password
Using a client principal name in the request, the KDC tries to look up the client’s account in the AD database, extract its precalculated Kerberos keys, and verify the client’s identity.
AS-REP
After the KDC verifies the client’s identity, it sends an AS-REP packet that contains data the client can construct a TGT memory object from:
The TGT itself is encrypted and signed with the kerberos key of the krbtgt account, so it’s intended to be unpacked only on KDC sides. It contains a session key, metadata, and the client’s Privileged Attribute Certificate (PAC). A PAC includes the client’s name, security identifier (SID), and groups.
In order for a client to use a TGT, it needs to construct a TGT memory object, which will contain the TGT itself, its session key, and all the metadata. Clients extract the session key from the part of an AS-REP that is encrypted by their keys.
How clients get Service Tickets
After a client constructs a TGT memory object, it can ask for any number of service tickets using TGS-REQ packets. The KDC will respond with TGS-REP packets when these requests are accepted.
TGS-REQ
A TGS-REQ contains a service principal name that the ticket is requesting for, a TGT, and a structure encrypted with the TGT session key and containing the current timestamp:
When the KDC receives a TGS-REQ, it decrypts the TGT, extracts the session key, and checks the client’s identity.
TGS-REP
TGS-REP packets are used to transfer service tickets to KDC clients.
After the KDC verifies the client’s identity, the following steps are happening:
- The KDC checks if the TGT is still valid according to the decrypted timestamps;
- If more than 15 minutes have passed since the TGT was issued, the KDC recalculates the decrypted PAC, and check if the client has not been disabled in the Active Directory;
- The KDC looks up an account that the sent service principal name is resolving to;
- The KDC extracts the kerberos key of the discovered account;
- The KDC constructs a service ticket, which consists of the PAC and the service ticket session key; the service ticket is encrypted and signed with the service account’s kerberos key;
- The KDC creates a structure with the service ticket session key and encrypts and signs it with the TGT session key.
Both the service ticket and the structure with the service ticket session key are included in the TGS-REP packet:
The encrypted part of the service ticket is the part that is brute forced in the Kerberoasting attack.
Exploring formats of Principal Names
Let’s examine principal names in the AS-REQ packet we gathered before:
Client principal names are passed in cname fields, and service principal names are sent in sname fields. All principal names are accompanied by an integer called the principal name type.
Principal names are usually split by the “/” character into a sequence of strings. For example, the principal name krbtgt/CONTOSO.COM in Kerberos traffic consists of two strings: krbtgt and CONTOSO.COM.
According to RFC 4120, cname and sname fields have different purposes, but the structure of these fields is identical:
KDC-REQ-BODY ::= SEQUENCE {
kdc-options [0] KDCOptions,
cname [1] PrincipalName OPTIONAL
realm [2] Realm
sname [3] PrincipalName OPTIONAL,
...
}
PrincipalName ::= SEQUENCE {
name-type [0] Int32,
name-string [1] SEQUENCE OF KerberosString
}
KerberosString ::= GeneralString (IA5String)
The identical structure of cname and sname fields caught my attention, and I decided to test different options of their usage in the Kerberos protocol.
The Kerberos Secret
It was discovered that Windows KDC services treat cname and sname fields by the same function set, and it’s irrelevant which format of a principal name you choose at any given time.
All Principal Names that resolve to the same account are equal
If you have an SPN value in a Kerberos packet, you can substitute it to the SAM Account Name (SAN) value of the account the SPN belongs, and nothing will break:
This way you can perform the Kerberoasting attack without knowing any SPN of the target account. But the existence of at least one SPN for the target account will continue to be needed.
Bonus: Revisiting S4U and AnySPN attacks
I examined Impacket source code, and I found two interesting places which are closely related to the discovered technique, but not related to Kerberoasting.
S4U2Self and S4U2Proxy Requests with SAM Account Names
Let’s try to abuse Resource-Based Constrained Delegation using getST.py form Impacket:
Here we have the “user01” account that has the “http/test” SPN and privileges to delegate access to any SPN of the “SRV02$” account.
According to the specification (S4USelf KRB_TGT_REQ, S4U2Proxy KRB_TGS_REQ), the user01’s service should use its SPN in S4U2Self and S4U2Proxy requests. However, you can see that Impacket uses SANs in such requests:
These requests don’t comply with the specification, but succeed because Windows KDCs are insensitive to given principal name formats.
AnySPN Attack
Impacket implements a thing called AnySPN attack. This attack tries to modify the SPN in the given service ticket file, when it’s different from the target service SPN:
Alberto Solino wrote an excellent article Kerberos Delegation, SPNs and More explaining how it works.
Here is the main section from this article:
Briefly, Benjamin Delpy, Ben Campbell and Alberto Solino noticed that a service ticket for Service A on Host A might work for Service B on Host A.
Actually, if we decrypt any service ticket’s encrypted part, we will see that it doesn’t contain any SPNs:
The service ticket’s encrypted part contains only the ticket’s session key, the metadata, and the authenticating user’s PAC. The service ticket’s SPN is contained in the unencrypted and unsigned part of the protocol, and it may simply not be taken into account by the client.
A Service Ticket is valid for all services run by its service account
So, if you wondered which SPN a service ticket is issued to when it’s requested without an SPN, now you know that the service ticket just don’t contain any.
Bonus: Playing with Principal Name Types
The structure of cname and sname fields contain an integer called Principal Name Type. The RFC 4120 specification defines 9 possible values for it:
I’ve done some research, and I’ve created a table with the actual Principal Name Types values and their meanings in Windows:
Name Type | Value | Meaning |
NT-UNKNOWN | 0 | Represents SPN and SAN formats |
NT-PRINCIPAL | 1 | Equal to NT-UNKNOWN |
NT-SRV-INST | 2 | Equal to NT-UNKNOWN |
NT-SRV-HST | 3 | Equal to NT-UNKNOWN |
NT-SRV-XHST | 4 | Represents SPN format |
NT-UID | 5 | Not supported |
NT-X500-PRINCIPAL | 6 | Represents DN format |
NT-SMTP-NAME | 7 | Equal to NT-UNKNOWN |
NT-ENTERPRISE | 10 | Represents UPN, SAN and multiple DomainName+SAN formats |
NT-MS-PRINCIPAL | -128 | Represents SAN and multiple DomainName+SAN formats |
NT-MS-PRINCIPAL-AND-ID | -129 | Equal to NT-MS-PRINCIPAL |
NT-ENT-PRINCIPAL-AND-ID | -130 | Equal to NT-X500-PRINCIPAL |
* | Equal to NT-UNKNOWN |
I found NT-ENTERPRISE type more valuable than the commonly used NT-UNKNOWN one. It supports the following bunch of name formats:
- userPrincipalName
- sAMAccountName
- sAMAccountName@DomainNetBIOSName
- sAMAccountName@DomainFQDN
- DomainNetBIOSName\sAMAccountName
- DomainFQDN\sAMAccountName
Note that if you use the SRV01 string as a sAMAccountName, and the SRV01 account does not exist, and the SRV01$ account exists, this name will be treated as a principal name of the SRV01$ account.
Other interesting Principal Name Types is NT-X500-PRINCIPAL. It supports DNs in the RFC 1779 structure. Here are three examples of how the same Active Directory object can be written in this structure:
CN=SQL ADMIN,OU=LAB Users,DC=CONTOSO,DC=COM
CN="SQL ADMIN";OU="LAB Users";DC="CONTOSO";DC="COM"
OID.2.5.4.3=SQL ADMIN,OU=LAB Users,DC=CONTOSO,DC=COM
Unfortunately, the NT-X500-PRINCIPAL type is not supported across forest trusts.
The Technique’s Application in Kerberoasting
I’ve added the usage of NT-ENTERPRISE and NT-MS-PRINCIPAL types to Impacket’s GetUserSPNs.py. Let’s see three common scenarios when these changes are necessary for Kerberoasting to succeed.
Kerberoasting with no access to LDAP
You might find yourself in a situation where you have access to a KDC service, you have an account list obtained (for example, via a RID cycling attack), and you don’t have SPNs.
Since you no longer need SPNs, you can request service tickets just by a user list using the new -userfile option:
The -userfile option utilizes the NT-ENTERPRISE type to look up accountd from the specified file.
Kerberoasting accounts with incorrect SPNs
There are two types of SPNs for which KDCs prohibit returning tickets:
- Wrong syntax SPNs
- Duplicate SPNs, i.e. when the same SPN values are assigned to multiple accounts
If a KDC finds that one of these is the case, it returns the KDC_ERR_S_PRINCIPAL_UNKNOWN error as if the passed SPN didn’t exist:
The new GetUserSPNs.py wraps the account list from LDAP to NT-MS-PRINCIPAL type and doesn’t utilize SPNs, so you will get the hashes even from misconstrued SPNs:
Internally the “DomainFQDN\sAMAccountName” format is utilized, and the “\” character is changed to “/” in the output to comply the username with the Impacket format and prevent its escaping in other tools.
Kerberoasting accounts with NetBIOS Name SPNs via Forest Trusts
When you ask for a service ticket for an SPN from another domain, and this SPN has a hostname in a NetBIOS name format, your KDC won’t be able to find the target service:
With the new GetUserSPNs.py file you will never get the KDC_ERR_S_PRINCIPAL_UNKNOWN for such services:
Afterwords
I hope you found the information about requesting service tickets without specifying SPNs useful, and the description of the Kerberos protocol and the “Bonus: Revisiting S4U and AnySPN attacks” section helpful as well.
Below is the list of tools which currently support described in the article techniques.
Impacket
The updated GetUserSPNs.py script is available in the official Impacket repository: https://github.com/SecureAuthCorp/impacket
Thanks @agsolino for merging!
Rubeus
Charlie Clark (@exploitph) added the support of NT-ENTERPRISE principals to Rubeus: PR#60