Performing Kerberoasting without SPNs

Author

Penetration Testing Expert

Service principal names (SPNs) are records in an Active Directory (AD) database that show which services are registered to which accounts:

An example of an account that has SPNs

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 remove the need for SPNs in this attack. I’ll show how it could be done, how it works, and when it could be useful.

Kerberos Basics

Kerberos is an open source protocol developed by MIT. 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 perform the Kerberoasting attack:

Performing the Kerberoasting attack in a lab environment

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 chose “MSSQLSvc/sp-sql:1433” SPN to request a ticket.

It’s not important whether chosen services are functioning; the existence of an SPN is sufficient for the attack.

I recorded a traffic dump of the GetUserSPNs.py tool to demonstrate all the protocol structure in detail:

Traffic dump of the Kerberoasting attack

We’ll use a built-in Wireshark dissector to explore Kerberos, since it’s a binary protocol that uses ASN.1 format.

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 for reducing the number of needed authentications, and there is no way to bypass it and request a service ticket immediately from the KDC.

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:

Content of the unauthenticated AS-REQ packet (#7)

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 account is set.

As you will see in the next section, in the response of AS-REQs there is a structure that is encrypted and signed with the client account’s password, so if AS-REQs worked without any authentication, anyone would be able to brute force anyone else’s password.

This is called an ASREPRoasting attack, and in Impacket it can be performed by the GetNPUsers.py script:

Performing an ASREPRoasting attack using GetNPUsers.py from Impacket

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.

Content of the KRB-ERR packet (#8)

It is worth noting that 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:

Content of the authenticated AS-REQ packet (#9)

It’s basically the same request as the first AS-REQ, but it contains data which could authorize the client. This data is a special structure that contains the current timestamp and is encrypted and signed with the client’s kerberos key.

Kerberos keys are 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
  • DES: the key is calculated directly from the password

The same algorithm is in use for calculation of service Kerberos keys.

Using a client principal name in the request, the KDC tries to look up the client’s account in the Active Directory database, extract its 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 file from:

Content of the AS-REP packet (#10)

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 file, 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.

Let’s extract the TGT session key manually using Impacket:

Decrypting the TGT session key

This is the same operation that is used in the ASREPRoasting attack.

How clients get Service Tickets

After a client accesses a TGT, 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:

Content of the TGS-REQ packet (#11)

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:

  1. The KDC checks if the TGT is still valid;
  2. If more than 15 minutes have passed since the TGT was issued, the KDC recalculates the sent PAC, and check if the client has not been disabled in the Active Directory;
  3. The KDC looks up an account that the sent service principal name is resolving to;
  4. The KDC extracts the kerberos key of the discovered account;
  5. The KDC constructs a service ticket, which consists of the PAC and the new session key; the service ticket is encrypted and signed with the service account’s kerberos key.

After the service ticket is constructed, 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 new session key are included in the TGS-REP packet:

Content of the TGS-REP packet (#12)

The encrypted part of the service ticket is the part that is used in the Kerberoasting attack.

Exploring formats of Principal Names

Every Kerberos packet contains principal names. As you saw, they are crucial for Kerberos to function, so I’ve included definitions for related terms:

A principal is a named client or server entity that participates in a network communication by Kerberos.
A principal name is an identifier of a principal.
A client principal name is an identifier of a principal that participates in a network communication on the client side.
A service principal name is an identifier of a principal that participates in a network communication on the server side.

Let’s examine principal names in the AS-REQ packet we gathered before:

An example of principal names in Kerberos traffic

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:

RFC 4120: The Kerberos Network Authentication Service (V5)

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)

It was discovered that Windows treats 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

For instance, it’s possible to set a sAMAccountName value in the sname field of the TGT-REQ packet:

An example of a TGT-REQ packet with a sAMAccountName

So, if you have a principal name somewhere in a Kerberos packet, you can substitute it to any other principal name that resolves to the same account, and nothing will break.

Exploring Principal Name Types

Since a principal name type is always a part of a principal name structure in the Kerberos protocol, I decided to examine how it can affect the processing of the strings which make up principal names.

The RFC 4120 specification defines 9 possible values of this integer:

An excerpt from RFC 4120: 6.2. Principal Names

Based on my research, the following table shows the actual principal name types and their meanings in Windows:

Name TypeValueMeaning
NT-UNKNOWN0Represents SPN and SAN formats
NT-PRINCIPAL1Equal to NT-UNKNOWN
NT-SRV-INST2Equal to NT-UNKNOWN
NT-SRV-HST3Equal to NT-UNKNOWN
NT-SRV-XHST4Represents SPN format
NT-UID5Not supported
NT-X500-PRINCIPAL6Represents DN format
NT-SMTP-NAME7Equal to NT-UNKNOWN
NT-ENTERPRISE10Represents UPN, SAN and multiple DomainName+SAN formats
NT-MS-PRINCIPAL-128Represents SAN and multiple DomainName+SAN formats
NT-MS-PRINCIPAL-AND-ID-129Equal to NT-MS-PRINCIPAL
NT-ENT-PRINCIPAL-AND-ID-130Equal to NT-X500-PRINCIPAL
*Equal to NT-UNKNOWN

I found the NT-ENTERPRISE type the most useful. It supports every possible principal name format except DN and SPN:

  • 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 are NT-X500-PRINCIPAL and NT-ENT-PRINCIPAL-AND-ID. They support DNs in the RFC 1779 structure:

RFC 1779: A String Representation of Distinguished Names

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

All three of these strings represent the same object in the Active Directory. Unfortunately, tests have shown that this name type is not supported across forest trusts.

Technique’s application in Kerberoasting

I’ve added the -usersfile option to GetUserSPNs.py, which requests tickets for each line from the specified file using the NT-ENTERPRISE type, and changed the default behavior from usage of service principal names to usage of SAM Account Names.

Let’s see three common scenarios when these changes are mandatory for the Kerberoasting attack 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:

Performing Kerberoasting by a user list using the new GetUserSPNs.py

The users in the specified file could be in any format the NT-ENTERPRISE type supports.

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:

Kerberoasting an account with an incorrect SPN

For the “user03” account the criteria of SPNs existence for Kerberoasting is met, but since the KDC validates SPNs before returning tickets, the attack fails.

If you construct a principal name that does not rely on SPNs and maps to the same account, the attack will succeed and you will get your $krb5tgs$ hash:

Kerberoasting an account with an incorrect SPN using the new GetUserSPNs.py

The new GetUserSPNs.py uses the “DomainFQDN\sAMAccountName” principal name format with the NT-MS-PRINCIPAL principal name type.

The “\” character was 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:

Kerberoasting an account with a NetBIOS Name SPN via a Forest Trust

Since the target domain name is always a part of a principal name in the new GetUserSPNs.py, you will get $krb5tgs$ hashes for such services:

Kerberoasting an account with a NetBIOS Name SPN via a Forest Trust using the new GetUserSPNs.py

If an SPN in the target domain has the wrong syntax, or multiple accounts are using it, the new GetUserSPNs.py will show its $krb5tgs$ hash as well.

Getting The Tools

Impacket

The updated GetUserSPNs.py script is now 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