Rubeus is a C# Kerberos abuse toolkit that started as a port of @gentilkiwi‘s Kekeo toolset and has continued to evolve since then. For more information on Rubeus, check out the “From Kekeo to Rubeus” release post, the follow up “Rubeus – Now With More Kekeo”, or the recently revamped Rubeus README.md.
I’ve made several recent enhancements to Rubeus, which included me heavily revisiting its Kerberoasting implementation. This resulted in some modifications to Rubeus’ Kerberoasting approach(es) as well as an explanation for some previous “weird” behaviors we’ve seen in the field. Since Kerberoasting is such a commonly used technique, I wanted to dive into detail now that we have a better understanding of its nuances.
If you’re not familiar with Kerberoasting, there’s a wealth of existing information out there, some of which I cover in the beginning of this post. Much of this post won’t make complete sense if you don’t have a base understanding of how Kerberoasting (or Kerberos) works under the hood, so I highly recommend reading up a bit if you’re not comfortable with the concepts. But here’s a brief summary of the Kerberoasting process:
- A attacker authenticates to a domain and gets a ticket-granting-ticket (TGT) from the domain controller that’s used for later ticket requests.
- The attacker uses their TGT to issue a service ticket request (TGS-REQ) for a particular servicePrincipalName (SPN) of the form sname/host, e.g. MSSqlSvc/SQL.domain.com. This SPN should be unique in the domain, and is registered in the servicePrincipalName field of a user or computer account. During this request process, the attacker can specify what Kerberos encryption types they support (RC4_HMAC, AES256_CTS_HMAC_SHA1_96, etc).
- If the attacker’s TGT is valid, the DC extracts information from the TGT stuffs it into a service ticket. Then the domain controller looks up which account has the requested SPN registered in its servicePrincipalName field. The service ticket is encrypted with the hash of the account with the requested SPN registered, using the highest level encryption key that both the attacker and the service account support. The ticket is sent back to the attacker in a service ticket reply (TGS-REP).
- The attacker extracts the encrypted service ticket from the TGS-REP. Since the service ticket was encrypted with the hash of the account linked to the requested SPN, the attacker can crack this encrypted blob offline to recover the account’s plaintext password.
A note on terminology. The three main encryption key types we’re going to be referring to in this post are RC4_HMAC_MD5 (ARCFOUR-HMAC-MD5, where an account’s NTLM hash functions as the key), AES128_CTS_HMAC_SHA1_96, and AES256_CTS_HMAC_SHA1_96. For conciseness I’m going to refer to these as RC4, AES128, and AES256.
Also, all examples here are run from a Windows 10 client, against a Server 2012 domain controller with a 2012 R2 domain functional level.
Kerberoasting generally takes two general approaches:
- A standalone implementation of the Kerberos protocol that’s used through a device connected on a network, or via piping the crafted traffic in through a SOCKS proxy. Examples would be Meterpreter or Impacket. This requires credentials for a domain account to perform the roasting, since a TGT needs to be requested for use in the later service ticket requests.
- Using built-in Windows functionality on a domain-joined host (like the .NET KerberosRequestorSecurityToken class) to request tickets which are then extracted from the current logon session with Mimikatz or Rubeus. Alternatively, a few years ago @machosec realized the GetRequest() method can be used to carve out the service ticket bytes from KerberosRequestorSecurityToken, meaning we can forgo Mimikatz for ticket extraction. Another advantage of this approach is that the existing user’s TGT is used to request the service tickets, meaning we don’t need plaintext credentials or a user’s hash to perform the Kerberoasting.
With Kerberoasting, we really want RC4 encrypted service ticket replies, as these are orders of magnitude faster to crack than their AES equivalents. If we implement the protocol on the attacker side, we can choose to indicate we only support RC4 during the service ticket request process, resulting in the easier to crack hash format. On the host side, I used to believe that the KerberosRequestorSecurityToken approach requested RC4 tickets by default as this is typically what is returned, but in fact the “normal” ticket request behavior occurs where all supported ciphers are supported. So why are RC4 hashes usually returned by this approach?
Time for a quick detour.
One defensive indicator we’ve talked about in the past is “encryption downgrade activity”. As modern domains (functional level 2008 and above) and computers (Vista/2008+) support using AES keys by default in Kerberos exchanges, the use of RC4 in any Kerberos ticket-granting-ticket (TGT) requests or service ticket requests should be an anomaly. Sean Metcalf has an excellent post titled “Detecting Kerberoasting Activity” that covers how to approach DC events to detect this type of behavior, though as he notes “false positives are likely.”
The full answer of why false positives are such a problem with this approach also explains some of the “weird” behavior I’ve seen over the years with Kerberoasting.
To illustrate, let’s say we have a user account sqlservice that has MSSQLSvc/SQL.testlab.local registered in its servicePrincipalName (SPN) property. We can request a service ticket for this SPN with powershell -C “Add-Type -AssemblyName System.IdentityModel; $Null=New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList ‘MSSQLSvc/SQL.testlab.local'”. However, the resulting service ticket applied to the current logon session specifies RC4, despite the requesting user’s (harmj0y) TGT using AES256.
As stated previously, for a long time I thought the KerberosRequestorSecurityToken approach for some reason specifically requested RC4. However, looking at a Wireshark capture of the TGS-REQ (Kerberos service ticket request) from the client we see that all proper encryption types including AES are specified as supported:
The enc-part in the returned TGS-REP (service ticket reply) is properly encrypted with the requesting client’s AES256 key as we would expect. However the enc-part part we care about for Kerberoasting (contained within the returned service ticket) is encrypted with the RC4 key of the sqlservice account, NOT its AES key:
So what’s going on?
It turns out that this has nothing to do with the KerberosRequestorSecurityToken method. This method requests a service ticket specified by the supplied SPN so it can build an AP-REQ containing the service ticket for SOAP requests, and we can see above that it performs proper “normal” requests and states it supports AES encryption types.
This behavior is due to the msDS-SupportedEncryptionTypes domain object property, something that was talked about a bit by Jim Shaver and Mitchell Hennigan in their DerbyCon “Return From The Underworld: The Future Of Red Team Kerberos” talk. This property is a 32-bit unsigned integer defined in [MS-KILE] 2.2.7 that represents a bitfield with the following possible values:
According to Microsoft’s [MS-ADA2], “The Key Distribution Center (KDC) uses this information [msDS-SupportedEncryptionTypes] while generating a service ticket for this account.” So even if a domain supports AES encryption (i.e. domain functional 2008 and above) the value of the msDS-SupportedEncryptionTypes field on the account with the requested SPN registered is what determines the encryption level for the service ticket returned in the Kerberoasting process.
According to MS-KILE 126.96.36.199 the default value for this field is 0x1C (RC4_HMAC_MD5 | AES128_CTS_HMAC_SHA1_96 | AES256_CTS_HMAC_SHA1_96 = 28) for Windows 7+ and Server 2008R2+. This is why service tickets for machines nearly always use AES256, as the highest mutually supported encryption type will be used in a Kerberos ticket exchange. We can confirm this the result of doing a dir \\primary.testlab.local\C$ command followed by Rubeus.exe klist :
However, this property is only set by default on computer accounts, not user accounts. If this property is not defined, or is set to 0, [MS-KILE] 188.8.131.52 tells us the default behavior is to use a value of 0x7, meaning RC4 will be used to encrypt the service ticket. So in the previous example for the MSSQLSvc/SQL.testlab.local SPN that’s registered to the user account sqlservice we received a ticket using the RC4 key.
If we select “This account supports AES [128/256] bit encryption” in Active Directory Users and Computers, then the msDS-SupportedEncryptionTypes is set to 24, specifying only AES 128/256 encryption should be supported.
When I first was looking at this, I assumed that this meant that since the msDS-SupportedEncryptionTypes value was non-null, and the RC4 bit was NOT present, that if you specify only RC4 when requesting a service ticket (via the /tgtdeleg flag here) for an account configured this way the exchange would error out.
But guess what? We still get an RC4 (type 23) encrypted ticket that we can crack!
A Wireshark capture confirms that RC4 is the only supported etype in the request, and that the ticket enc-part is indeed encrypted with RC4.
I’m assuming that this is for failsafe backwards compatibility reasons, and I ran this scenario in multiple test domains with the same result. However someone else I asked to recreate wasn’t able to, so I’m not sure if I’m missing something or if this accurately reflects normal domain behavior. If anyone has any more information on this, or is/isn’t about to recreate, please let me know!
Why does the above matter? If true, it implies that there doesn’t seem to be an easy way to disable RC4_HMAC on user accounts. This means that even if you enable AES encryption for user accounts with servicePrincipalName fields set, these accounts are still Kerberoastable with the hacker-friendly RC4 flavor of encryption keys!
After a bit of testing, it appears that if you disable RC4 at the domain/domain controller level as described in this post, then requesting a RC4 service ticket for any account will fail with KDC_ERR_ETYPE_NOTSUPP. However, TGT requests will no longer work with RC4 either. As this might cause lots of things to break, definitely try this in a lab environment first before making any changes in production.
Sidenote: the msDS-SupportedEncryptionTypes property can also be set for trustedDomain objects that represent domain trusts, but it is also initially undefined. This is why inter-domain trust tickets end up using RC4 by default:
However, like with user objects, this behavior can be changed by modifying the properties of the trusted domain object, specifying that the foreign domain supports AES:
This sets msDS-SupportedEncryptionTypes on the trusted domain object to a value of 24 (AES128_CTS_HMAC_SHA1_96 | AES256_CTS_HMAC_SHA1_96), meaning that AES256 inter-domain trust tickets will be issued by default:
Trying to Build a Better Kerberoast
Due to the way we tend to execute engagements, we often lean towards abusing host-based functionality versus piping in our own protocol implementation from an attacker server. We often times operate over high-latency command and control, so for complex multi-party exchanges like Kerberos our personal preference has traditionally been the KerberosRequestorSecurityToken approach for Kerberoasting. But as I mentioned in the first section, this method requests that highest supported encryption type when requesting a service ticket. For user accounts that have AES enabled, this default method will return ticket with an encryption type of AES256 (type 18 in the hash):
Now, an obvious alternative method for Rubeus’ Kerberoasting would be to allow an existing TGT blob/file to be specified that would then be used in the ticket requests. If we have a real TGT and are implementing the raw TGS-REQ/TGS-REP process and extracting out the proper encrypted parts manually, we can specify whatever encryption type support we want when issuing the service ticket request. So if we have AES-enabled accounts, we can still get an RC4 based ticket to crack offline! This approach is in fact now implemented in Rubeus with the /ticket:<blob/file.kirbi> parameter for the kerberoast command.
So what’s the disadvantage here? Well, you need a ticket-granting-ticket to build the raw TGS-REQ service ticket request, so you need to either a) be elevated on a system and extract out another user’s TGT or b) have a user’s hash that you use with the asktgt module to request a new TGT. If you’re curious why a user can’t extract out a usable version of their TGT without elevation, check out the explanation in the “Rubeus – Now With More Kekeo” post.
The solution is @gentilkiwi’s Kekeo tgtdeleg trick, that uses the Kerberos GSS-API to request a “fake” delegation for a target SPN that has unconstrained delegation enabled (e.g. cifs/DC.domain.com). This was previously implemented in Rubeus with the tgtdeleg command. This approach allows us to extract a usable TGT for the current user, including the session key. Why don’t we then use this “fake” delegation TGT when performing out TGS-REQs for “vulnerable” SPNs, specifying RC4 as the only encryption algorithm we support?
The new kerberost /tgtdeleg option does just that!
There have also been times in the field where the default KerberosRequestorSecurityToken Kerberoasting method has just failed- we’re hoping that the /tgtdeleg option may work in some of these situations.
If we want to go a bit further and avoid the possible “encryption downgrade” indicator, we can search for accounts that don’t have AES encryption types supported, and then state we support all encryption types in the service ticket request. Since the highest supported encryption type for the results will be RC4, we’ll still get crackable tickets. The kerberoast /rc4opsec command executes the tgtdeleg trick and filters out any of these AES-enabled accounts:
If we want the opposite and only want AES enabled accounts, the /aes flag will do the opposite LDAP filter. While we don’t currently have tools to crack tickets that use AES (and even once we do, speeds will be thousands of times slower due to the AES key derivation algorithms), progress is being made.
Another advantage of the /tgtdeleg approach for Kerberoasting is that since we’re building and parsing the TGS-REQ/TGS-REP traffic manually, the service tickets won’t be cache on the system we’re roasting from. The default KerberosRequestorSecurityToken method results in a service ticket cached in the current logon session for every SPN we’re roasting. The /tgtdeleg approach results in a single additional cifs/DC.domain.com ticket being added to the current logon session, minimizing a potential host-based indicator (i.e. massive numbers of service tickets in a user’s logon session).
As a reference, in the README I built a table comparing the different Rubeus Kerberoasting approaches:
As a final note, Kerberoasting should work much better over domain trusts as of this commit. Two foreign trusted domain examples have been added to the kerberoast section of the README.
Hopefully this cleared up some of the confusion some (like me) may have had surrounding different encryption support in regards to Kerberoasting. I’m also eager for people to try out the new Rubeus roasting options to see how they work in the field.
As always, if I made some mistake in this post, let me know and I’ll correct it as soon as I can! Also, if anyone has insight on the RC4-tickets-still-being-issued-for-AES-only-accounts situation, please shoot me an email (will [at] harmj0y.net) or hit me up in the BloodHound Slack.
4 thoughts on “Kerberoasting Revisited”
Pingback: Passwords, Splunk, & Nest Microphones - Paul's Security Weekly #595 - Security Weekly
Pingback: MachineAccountQuota is USEFUL Sometimes: Exploiting One of Active Directory's Oddest Settings
Pingback: Active Directory Password Encryption | Florian Sailer's Knowledge Space
Thanks for the write-up. Two questions; once you crack said service accounts, how do you know which hosts they can be used on? Secondly, is there a way to do this in reverse? What I mean is, say there are 100 hosts I am interested in getting access on in a domain of tens of thousands of machines. Is there a way to enumerate service accounts viable for those 100 hosts, only, or do I have to always enumerate all service accounts over the entire (humongous) domain?