Skip to main content

Introduction

Kerberoasting and AS-REP Roasting attacks are well understood and have been thoroughly documented over the years. Despite this, a while ago, we came across an Active Directory environment with an unusual configuration that ended up causing a surprising amount of trouble with existing tooling. In this environment, the RC4 encryption type for Kerberos had been disabled completely, so only AES encryption types worked when performing Kerberoasting and AS-REP Roasting attacks. For Kerberoasting this wasn’t an issue, but for AS-REP Roasting there were problems we ran into with capturing the encrypted data correctly, and when we attempted to crack the captured messages. This led to an interesting investigation into Kerberos, AS-REP messages, some popular tooling implementing AS-REP roasting, and led to the eventual development of Hashcat plugins to support cracking of AES AS-REP messages.

TLDR

Existing AS-REP Roasting tooling did not quite handle Kerberos AS-REP data correctly when only AES encryption types were supported but fortunately, after digging into the detail, I found that this was not difficult to work around (see “What happens when roasting AES AS-REPs?” for details).

In general, it is expected to be quite rare for environments that support only AES encryption types for Kerberos to have accounts present with Kerberos pre-authentication disabled. It may, however, be desirable to be able to perform AS-REP roasting attacks against Active Directory that rely solely on AES message types, as AES Kerberos is more commonly used for the majority of Kerberos traffic in modern Active Directory environments. As a result, AES AS-REP roasting attacks are more likely to blend in with existing network traffic and are thus are an interesting technique for teams performing security monitoring in AD environments to develop detections against.

When it came to cracking AES-encrypted AS-REP data to recover users’ passwords, John the Ripper worked perfectly, but I found that Hashcat only had a plugin for RC4-encrypted AS-REPs. Since Hashcat is what we use on our password cracking rig at MWR, I decided to write Hashcat plugins (see my Hashcat pull request) for the AES encryption types as well. When starting development, the hope was that I could mostly reuse code from the existing Hashcat plugins for RC4 AS-REPs and AES TGS-REPs (Kerberoasting), along with the AS-REP plugin from the “Jumbo” version of John the Ripper (which already supported AES). This did turn out to be the case, although there were also a few issues to investigate and resolve to have everything fully working. So most of the credit for the logic behind these Hashcat plugins should still go to the authors of these tools and other plugins.

I have also submitted pull requests to Impacket (pull request here) and Rubeus (pull request here), the two tools I most often use for Kerberoasting and AS-REP Roasting, to correct a few issues in how they handled AES-encrypted AS-REPs. Impacket’s GetNPUsers.py AS-REP Roasting script supported AES, but did not quite format the output correctly, and the AS-REP Roasting functionality in Rubeus did not support AES encryption types at all.

Background Information

AS-REP Roasting is an attack that can be performed from a completely unauthenticated perspective against Active Directory (AD) accounts that have been configured to not require Kerberos pre-authentication. There are many resources available online that describe the attack and the related Kerberos mechanisms in detail, such as this post by Sean Metcalf (@PyroTek3), and another one by Will Schroeder (@harmj0y). The bottom line is that AS-REP Roasting allows the attacker to obtain an AS-REP response from a Kerberos Key Distribution Center (KDC), which includes a specific part that’s essentially encrypted with the user’s password, called the EncASRepPart. Similar to Kerberoasting attacks, the attacker can then perform offline password guessing against this encrypted data.

As others have already explained, modern Windows Kerberos environments will default to AES256 encryption (AES256-CTS-HMAC-SHA1–96), which takes significantly longer to crack than RC4 (ARCFOUR-HMAC-MD5). However, despite the fact that it defaults to AES256, it will, by default, still happily use RC4 if you request it. Generally, the AS-REP Roasting tools will attempt to request RC4 by default, in order to take advantage of this behaviour.

But what happens when someone is brave enough to disable the RC4 encryption type for Kerberos entirely? I learned that it is actually possible to do so, and as you might expect, is also the sort of thing that can have terrifying unintended consequences in a typical large AD environment. Steve Syfuhs (@stevesyfuhs) explained this very well through a case study in this fascinating blog post. The opening line in the blog post sets the scene quite nicely:
Was pulled in to a fun customer issue last Friday around disabling RC4 in Active Directory. What happened was, as you can imagine, not good: RC4 was disabled and half their environment promptly started having a Very Bad Day.

Despite these dangers, we encountered an environment during a security assessment where they had apparently managed to do this successfully. But this surprisingly aggressive security control was accompanied by an unfortunate misconfiguration – Kerberos pre-authentication was disabled on several user accounts that were still active. We decided to perform an AS-REP Roasting attack against these accounts, and then things got interesting …

What happens when roasting AES AS-REPs?

The AS-REP Roasting attack did not go quite as well as planned. Some of the tools returned errors about unsupported encryption types, while others returned “hashes” that did not seem to be valid. The reason appeared to be that RC4 had somehow been disabled completely, and only AES encryption types could be used in the attack.

At a later stage, I set up a test environment to see if I could replicate this behaviour. The supported Kerberos encryption types can be configured on a Windows machine through the following Group Policy setting:

Computer Configuration -> Policies -> Windows Settings -> Security Settings -> Local Policies -> 
Security Options -> Network security: Configure encryption types allowed for Kerberos

This configuration and some related considerations are explained in this Microsoft Tech Community post. In my case, I applied it to the domain controller in my test environment, since the assumption was that the domain controller acting as the Kerberos KDC would determine what encryption types can and can’t be used. If the KDC doesn’t support RC4, then nothing else that interacts with it could use RC4 either.

Figure 1 shows this setting being configured to only allow AES and “future encryption types” on my test domain controller, dc1.example.com.

Figure 1: Configuring Kerberos encryption types to only allow AES

After applying this setting, I expected that RC4 would no longer be allowed, so it was time to test out the AS-REP Roasting tools. I used the following two tools, both of which are excellent in their own right:

CrackMapExec is also a good option, but it behaved in exactly the same way as GetNPUsers.py, and I believe it uses the Impacket framework for its AS-REP Roasting functionality. So I’ll only be discussing Rubeus and Impacket here.

Rubeus

Unfortunately, under the target configuration, Rubeus only returned the following error:

[X] KRB-ERROR (14) : KDC_ERR_ETYPE_NOTSUPP

The full output is shown in Figure 2.

Figure 2: Rubeus error output when attempting an AS-REP Roasting attack when only AES is allowed

I inspected the code and realised that Rubeus forced the RC4 encryption type when performing AS-REP Roasting, which would usually be a good thing. However, it did not fall back to AES if RC4 was not supported, thus resulting in the error I got in Figure 2. I was able to make a few modifications to the Rubeus source code to specify the desired encryption type through an additional parameter, which was useful for performing further testing. These changes were submitted in a pull request to Rubeus.

Impacket

GetNPUsers.py gave me output that looked more promising.

This was the output it produced in Hashcat format:

$ python3 GetNPUsers.py example.com/user -dc-ip 192.168.56.81 -request -format hashcat
Impacket v0.10.0 – Copyright 2022 SecureAuth Corporation

Password:
[*] Cannot authenticate user, getting its TGT
$krb5asrep$18[email protected]:bde521de05cacdcdf0a073f3e3f8c52e$23a7662ff6849a22b054b726
67ab1b74882c5a4c1ddf240db1b84a1494d5b6ce4264b3bc2451737eb0043fdd9db546c6452deb9fc77dfeac
7b2c5772fc8658646c98145903c0e48cfcc36774bce23b2d52f54412756ba975cdd8baf9e62637c7c7b065c0
c77326a4ac702110d3f1750b178a27f844904996c79316b6d8c1ae8d20d8edfd9875378a9318d7dca8b1c552
3cf86f8412bb327503f49718e0a66ce3933a0ee19f0192090c8e266714bced7f9625b64e5a96f2943181f2e8
b61fe1ff852558750c8951ed94b9331a98ee78945171b7721ed8835fe2b91a8778fdaccf79f6e9510e589bab
c9407a7074b954454db31b5aa409a9e595e7f0098ccc

And in John format:

$ python3 GetNPUsers.py example.com/user -dc-ip 192.168.56.81 -request -format john
Impacket v0.10.0 – Copyright 2022 SecureAuth Corporation

Password:
[*] Cannot authenticate user, getting its TGT
[email protected]:ccc4553256f8d6863eca64ea30e582d2$a61a230f5c9613883d4330198
6892fd92b6fdfc61d030a2d3aa1afe2737a7676e7da720a8e508ce3bd27c5337572586ff3fe0fc51f1cf6c18
8e89bc433c198a907dbc4d0412bb701eeccd4b836e3588b2e6b6dc3c87a17221e8f30ce8e5bda4ecb224cca
842ee06025d1be0a56dee96433d71ec47772029f3dbdf367e8e3c36771b621b41158ab2dc5935ec832ff457
5e829ea1d4402da548eb9e8ef04c5f4a022d5d3104b45150d20e0ce0c537de0894c05bf07440a3bbcb6c815
6967464f4a995105b7b5d77f44c761d4a7422d8485b831b64124695adefb8b5217c5331ba7094a8ee5cfde2
e046bdc6f0c8fdcfb4f316a19c44bad5dc449bf0a51c1c6

The Hashcat output even included “18” in the hash signature ($krb5asrep$18$), which seemed like a good sign. This was because in the other Hashcat plugins related to Kerberos, this part of the hash represents the encryption type (etype) used for the data and the 18 specifically corresponds to AES256. I was ready to plug this hash into Hashcat when I realised there was a problem. Surprisingly, the only hash-mode I could find for AS-REPs was 18200, which was specifically for RC4. Using that hash-mode anyway and hoping for the best obviously didn’t work…

As an aside, it’s worth mentioning that these “hashes” aren’t actually hashed passwords; we’re dealing with AES-encrypted data encrypted with a key derived from the user’s password. But in the context of password cracking, I’ll refer to the formatted data as “hashes” where it makes sense for simplicity.

Returning to the Impacket output, as a sanity check, I decided to try crack the other hash in John the Ripper, but that also didn’t work. I was clearly getting some sort of valid response from the domain controller, but something was going wrong. One of the things I suspected was that the hash wasn’t being formatted correctly for the hash cracking tools. Since Hashcat didn’t appear to have a plugin for AES256 AS-REPs, I investigated the AS-REP Roasting plugin that’s part of the “Jumbo” version of John to see if it really did support AES and, if so, what the correct hash format for it is.

The relevant source code is in the krb5_asrep_fmt_plug.c file in John’s src directory. This file included some comments and test data that was particularly helpful in this case. Firstly, there was a comment showing the different hash formats supported by this plugin:

/*
assuming checksum == edata1, for etype 23

formats are:
checksum$edata2
$krb5asrep$23$checksum$edata2
$krb5asrep$18$salt$edata2$checksum // etype 18
*/

The last hash format was for encryption type 18, which is exactly what we’re looking for. The salt value in this format refers to the cryptographic salt used for the AES encryption algorithm, which is explained as follows in RFC4120, Section 4:

The default salt string, if none is provided via pre-authentication data, is the concatenation of the principal’s realm and name components, in order, with no separators.

In the context of Active Directory, the realm is the domain name in all caps, and the principal’s name is simply the username (in this case sAMAccountName rather than userPrincipalName). For example, an account with the username “user” in the domain “example.com” would have a salt value of “EXAMPLE.COMuser”.

However, there was a problem here, since this hash format didn’t match either of the two hashes output by GetNPUsers.py. The hash from GetNPUsers.py in john format was structured as follows (the output in hashcat format was very similar):

[email protected]:checksum$edata2

This structure technically matched the first example format from John’s source code (checksum$edata2), since everything before the colon is treated as a label and not part of the actual hash. Comparing the hash from GetNPUsers.py to the etype 18 example, there were four main issues:

  1.  The hash signature was $krb5asrep$ instead of $krb5asrep$18$, although this was not an issue when specifying the hashcat output format.
  2. John expected the correctly formatted salt value, rather than accepting the username and domain name separately and constructing the salt value itself. In other words, it expected “EXAMPLE.COMuser”, and not “[email protected]”.
  3. The checksum was first, followed by the encrypted data, which is how the data is normally structured for encryption type 23. The example hash format in John’s source code requires the checksum to be at end.
  4. A fourth issue was identified by comparing our hash to the test hashes in John’s source code. The checksum in the test hashes was 24 characters (or 12 bytes) long, as is also the case for AES TGS-REP hashes. The checksum in our hash was 32 characters (or 16 bytes) long, which is normally the case for RC4 (encryption type 23). So it appeared that GetNPUsers.py was treating the AS-REP data as if it was encryption type 23.
Updating the AES AS-REP John hashes output by Impacket

I inspected the AES AS-REP messages generated when using GetNPUsers.py in Wireshark and verified that it was outputting all the right data, it’s just that the wrong bytes were being selected as the checksum. Thus, it should be possible to restructure our hash into the format required by John. Using Wireshark, I could also see that GetNPUsers.py was selecting the first 32 characters as the checksum rather than the last 24, so all we would need to do is move the separator ‘$’ character to the right place, and rearrange the domain name and username into the right order for the salt value.

The original hash is shown in Figure 3, with all the different parts highlighted.

Figure 3: Illustration of the hash collected by Impacket, with the different components highlighted

Figure 4 shows that hash after rearranging everything into the correct format.

Figure 4: Illustration of the hash collected by Impacket after being corrected

The corrected hash was then provided to John and it cracked successfully! The password was “hashcat”, as shown in Figure 5.

Figure 5: Successfully cracking the corrected hash using John the Ripper

After these modifications, we’re able to crack our hashes and perform our AS-REP Roasting attack against an environment that only supports the AES encryption types. But there were clearly some opportunities for improving the existing tooling:

  • Adding support for AS-REP AES encryption types in Hashcat
  • Adding support for AES encryption types in Rubeus’ AS-REP Roasting module
  • Correcting the output of Impacket’s GetNPUsers.py script for AES encryption types

The rest of this blog post is dedicated towards the first item – developing Hashcat plugins for AS-REP AES encryption types. This seemed like a good opportunity to dive into Hashcat plugin development, and these plugins could be useful to others who encounter an environment with this configuration, but prefer Hashcat over John.

Modifying Hashcat Plugins – Getting Started

From what I understood at the time, the cryptography for AES-encrypted Kerberos data should be virtually identical between AS-REPs and TGS-REPs, so I expected that I would be able to reuse most of the code from the existing AES Kerberoasting plugins. I could also reference the existing AS-REP RC4 plugin to see how it handles the AS-REP data. The plan was thus to start with these existing plugins as a base and make the necessary modifications, rather than developing a new plugin from scratch.

Although it felt intimidating to even create a modified version of a Hashcat plugin, a comprehensive plugin development guide is available that does an excellent job of explaining everything. I also had help from a colleague, Christopher Panayi (@Raiona_ZA), who had developed his own Hashcat plugin recently as part of his SCCM research.

After reviewing the Hashcat plugin development guide, this was the high-level approach I followed:

  1. Start with the plugin for AES256 as a PoC, since AES128 should be very similar and could be implemented quickly once the AES256 plugin is done
  2. Review the source code for the existing Hashcat plugins for AES256 TGS-REPs (hash-mode 19700) and RC4 AS-REPs (hash-mode 18200)
  3. Copy the source code from the Hashcat AES256 TGS-REP plugin and use it as a starting point
  4. Make modifications and swap in code from the AS-REP RC4 plugin where necessary
  5. Once the AES256 plugin is done, implement the AES128 plugin according to the same principles

There was also quite a bit of debugging and further investigation into Kerberos AES encryption throughout this process. For the AS-REP AES256 plugin, I needed to implement two source code files:

  • The module code, which is mostly responsible for parsing and preparing hashes, and passing them to the kernel code
  • The kernel code, which is where all the cryptography is implemented and where the “cracking” happens on a GPU

The Hashcat plugin development guide explains the structure of these code files in great detail, so I won’t go into that here.

I also needed to define the hash format. For the sake of consistency, I decided to make my proposed hash format similar to the AES TGS-REP plugins. For example, the hash format for the TGS-REP AES256 plugin is as follows:

$krb5tgs$18$user$realm$checksum$edata2

And the proposed hash format for my AS-REP AES256 plugin is:

$krb5asrep$18$user$realm$checksum$edata2

I later decided to also add support for John’s hash formats to my plugin for ease of use, since this was apparently an obscure hash format and there were issues with the tooling used to capture the hashes that still needed to be resolved at the time.

Development Environment

I used Kali Linux in a virtual machine for my development environment, since I already had Hashcat running in there. The plugin development guide recommends a development environment that at least roughly matches the environments in which the plugin will ultimately be used (e.g. likely a computer with a discrete AMD or NVIDIA GPU). However, since all of the necessary testing and optimisation would have already been done on the cryptography code for the TGS-REP AES modules, I figured developing and testing the module in a virtual machine should be fine as long as I didn’t end up making any major changes to the cryptography code.

Compiling Hashcat from source was quite simple, so all that needed to be done was adding the source code files for the new plugin. A hash-mode number also had to be chosen, and I went with 33200 for the AES256 plugin according to the guidelines in the plugin development guide. I have submitted a pull request to Hashcat with these new plugins, so they’ll likely reserve different hash-mode numbers that’ll ultimately be used once the plugins meet all their requirements and are accepted.

For a working plugin, only two source code files had to be created:

  • Module code – module_33200.c in the src/modules folder
  • Kernel code – m33200-pure.cl in the OpenCL/ folder

No additional configuration was necessary – when you run ‘make’, all the modules in the src/modules folder will be compiled. The OpenCL kernels are compiled at runtime on the GPU when the corresponding hash-mode is selected.

The plugin development guide even suggests several command line parameters that can be helpful when debugging a plugin’s kernel code, such as disabling Hashcat’s usual self-test feature. For example, in my case I used the following command line parameters in line with the guide’s recommendations:

./hashcat -a 0 -m 33200 test-hashes.txt wordlist.txt –potfile-disable –self-test-disable -n 1 -u 1 -T 1
–backend-vector-width 1 -d 1 –force

Modifying Hashcat Plugins – Module Code

First up was the module code. The module code from the existing plugins was relatively simple and easy enough to follow, mostly because it didn’t contain any cryptographic operations. Starting with the module code from the TGS-REP AES256 plugin (module_19700.c), most of the modifications were made to the following two functions:

  • module_hash_decode: This function decodes the strings containing the hashes provided to the plugin, and stores the important parts of the hash in a struct that is passed to the kernel code.
  • module_hash_encode: This function simply does the reverse of the encode function; it builds a hash string using the data that makes up the hash.

Both of these functions are described in more detail in the Hashcat plugin development guide.

My proposed hash format was very similar to the hash format for the TGS-REP AES256 plugin, and the data that needs to be passed to the kernel code is exactly the same. As a result, very few modifications were necessary in the module code. The main thing that needed to change was the code in the TGS-REP AES256 plugin that handled an alternative hash format containing the full service principal name (SPN). Since SPNs aren’t relevant for AS-REP Roasting, this code was removed. In its place, I added some conditional statements to identify and process hashes in John’s format, which is slightly different, so that my plugin could transparently handle hashes in either format.

Since the main difference with John’s hash format was that it had the checksum at the end, this format could easily be identified by checking if there’s a separator character ‘$’ at the 25th-last position in the string, which would be just before the 24-character checksum.

Additionally, John’s hash format already included the formatted salt value, so there was no need to construct the salt value from separate user and realm fields like with the Hashcat format. Although, by constructing the salt value in this way, the Hashcat format has an advantage in that the realm field isn’t case sensitive, as the module code will always capitalise it automatically. This happens to be an important point to note when using John’s hash format – the domain/realm name has to be in all caps, otherwise the salt won’t be valid and the hash won’t crack.

The modifications to the module_hash_encode function were even simpler – it was just a matter of implementing two different snprintf statements to write the hash data into strings that match the two different hash formats.

Other than that, there were just a few constants that needed to change, which contained things like the plugin name and the test hash:

  • HASH_NAME
  • KERN_TYPE
  • ST_PASS
  • ST_HASH
  • SIGNATURE_KRB5ASREP

While it wasn’t strictly necessary, I also changed the name of the struct containing the data that’s passed to the kernel code from krb5tgs_18 to krb5asrep_18.

Now it was time to get serious and move on to the kernel code.

Modifying Hashcat Plugins – Kernel Code

As with the module code, I started with the TGS-REP AES256 kernel code (m19700-pure.cl) as a base, and started making modifications. The hope was that the cryptography would be the same, and that I wouldn’t need to spend too much time figuring out all the finer details of it (spoiler: I did end up having dig rather deeply into some of the RFCs for Kerberos and AES encryption, at least for parts of the cryptographic process, but more on that later).

The kernel code contains the following three functions:

  • mXXXXX_init
  • mXXXXX_loop
  • mXXXXX_comp

The plugin development guide does a very good job of describing these functions and what they typically do. In the TGS-REP AES256 kernel code, the mXXXXX_init and mXXXXX_loop functions were quite small; the majority of the code was in the mXXXXX_comp function and a few other helper functions. It wasn’t necessary to make any modifications to the mXXXXX_init and mXXXXX_loop functions from the TGS-REP AES256 kernel code. All the changes were made in the mXXXXX_comp function.

My expectation was that the cryptography would be mostly the same, but that there would have to be differences in how the kernel handles and verifies the decrypted data, given that we’re dealing with different types of Kerberos tickets. Fortunately, I also had the existing Hashcat plugin for RC4 AS-REPs to refer to. While the cryptographic operations were obviously different, the high-level flow was the same, and I was able to identify an important difference.

Identifying the Right Structures in the Decrypted Data

These plugins work by attempting to use the candidate password to decrypt the encrypted data in the target “hash”. They check the decrypted data to see if it’s valid, and then calculate the checksum of the decrypted data and compare it to original checksum sent alongside the data. If the checksums match, we know that the password was correct. In order to avoid unnecessary computation, the kernel code for these plugins start by only decrypting the first few blocks. They then check the decrypted data to identify specific Abstract Syntax Notation One (ASN.1) structures, and only continue if valid data is identified.

Kerberos messages are formatted as ASN.1 structures, with Distinguished Encoding Rules (DER) encoding. DER is a type–length–value (TLV) encoding, so the plugins can check for a specific set of types, with some minor known variations in their positions based on the overall length of the message.

This is where the important difference comes in – the TGS-REP AES256 kernel code looks for the ASN.1 structures of the TGS-REP message it’s attempting to decrypt, whereas the AS-REP RC4 code looks for different ASN.1 structures corresponding to a specific part of the AS-REP message. The code had some very helpful comments explaining what data they were looking for. Specifically, the TGS-REP plugins are attempting to decrypt “EncTicketPart” data, which corresponds to “APPLICATION 3” in the ASN.1 data and is represented by the bytes 0x63 for its “type” value. The AS-REP plugin attempts to decrypt the “EncASRepPart” data of an AS-REP message, which corresponds to “APPLICATION 25” in the ASN.1 data and is represented by the bytes 0x79 for its “type” value.

Will Schroeder mentioned a JavaScript ASN.1 decoder that can be used offline in his “Roasting AS-REPs” post. I also found it very useful to help understand and visualise the decrypted data. Crucially, it even has a dark mode.

As an example, Figure 6 shows a screenshot of some decrypted “EncASRepPart” data visualised by this tool.

Figure 6: Screenshot of decrypted and decoded EncASRepPart ASN.1 data in the JavaScript ASN.1 decoder

So to get back to my kernel code, all I needed to do was to change the conditional checks in the TGS-REP AES256 kernel code to look for AS-REP structures instead of TGS-REP structures in the decrypted data. Essentially, the checks in the TGS-REP AES256 kernel code had to be swapped out for those from the RC4 AS-REP kernel code. This was simple enough, since it was really just one large if statement.

At this stage, this was the only obvious thing I could identify that needed to change. So now it was time to test the plugin and start the debugging process. Needless to say, after this one change, the module didn’t work yet, as things are rarely so easy.

Debugging the Kernel Code

The Hashcat plugin development guide warns that there aren’t a lot of options available for debugging OpenCL kernel code, and you’ll basically be using simple print statements to see what’s happening. Since the code will essentially be operating on raw bytes and will often also be swapping between little- and big-endian byte order, the guide also recommends that you print out data through your printf statements using the %08x template. This will allow you to see exactly what all the bytes are, rather than attempting to print the data out as a regular string.

For example, here are the printf statements I used to print out the checksum bytes passed to the kernel code from the module:

printf("esalt_bufs[DIGESTS_OFFSET_HOST].checksum[0]: %08x\n",
esalt_bufs[DIGESTS_OFFSET_HOST].checksum[0]);
printf("esalt_bufs[DIGESTS_OFFSET_HOST].checksum[1]: %08x\n", 
esalt_bufs[DIGESTS_OFFSET_HOST].checksum[1]);
printf("esalt_bufs[DIGESTS_OFFSET_HOST].checksum[2]: %08x\n",
esalt_bufs[DIGESTS_OFFSET_HOST].checksum[2]);

These statements generated the following output for the example hash discussed earlier (the corrected hash that was shown in Figure 4):

esalt_bufs[DIGESTS_OFFSET_HOST].checksum[0]: 19c44bad
esalt_bufs[DIGESTS_OFFSET_HOST].checksum[1]: 5dc449bf
esalt_bufs[DIGESTS_OFFSET_HOST].checksum[2]: 0a51c1c6

This output matches the checksum data in the hash, 19c44bad5dc449bf0a51c1c6, which indicated that the kernel code was at least receiving the correct checksum bytes. Similar printf statements could be used to print out all the other values I needed to inspect, whether they were pieces of data passed to the kernel code by the module, or intermediate values in the cryptography operations.

I attempted to follow a step-by-step process of verifying the data at key points and identifying where the output deviated from what was expected, in order to determine which part of the code was the main problem. The first two steps were:

  1. Print out all the data passed to the kernel code from the module and verify that it matches. This was really just a simple sanity check, but it was an important one, since if the kernel code was getting even slightly wrong data, then it obviously would not work.
  2. Print out the first set of decrypted bytes, at the point where the code inspects it to find the right ASN.1 structures. My reason for starting here was that this was the only place where I had modified the kernel code so far, so it would make sense to ensure my modified conditional statements were working correctly. It was possible that the data was being decrypted correctly, but that the conditional statements were wrong and were not identifying the ASN.1 structures in the decrypted data correctly. Problems with the conditional statements could easily be identified by printing out and manually inspecting the decrypted bytes.

Step 1 was completed quickly – all the data was fortunately being passed through correctly to the kernel code. Step 2 gave me the first clue about where the problem might be. After adding the necessary print statements, it was clear that the decrypted data (when given the correct candidate password) was basically just random bytes and did not contain anything resembling ASN.1 structures. I got the following output for the first 4 decrypted bytes:

decrypted_block[0]: f2965ce5

As discussed earlier, the first byte should be 0x79. And there should also be a 0x30 in there (indicating the start of the next element, which is a “SEQUENCE” type), depending on the length of the message. Random bytes meant that the decryption failed, and since the correct data was being provided, the issue had to be somewhere in the decryption process. I believed that there were two different high-level areas I needed to investigate:

  1. The key derivation process
  2. The actual cryptographic operations after key derivation

Either way, at this point it was abundantly clear that I would first need to do some research into the details of the relevant Kerberos message structures and the Kerberos AES implementation, since it’s not something I was familiar with. Making a few quick modifications to the existing plugins without having to dive into all the details sadly didn’t work out, but at least this was an opportunity to learn a bit more about the inner workings of a very important cog in the Active Directory machine.

Investigating the Key Derivation Process

The key derivation process had quite a few different steps, with several different keys being generated. Although I did not yet understand the process, the comments in the kernel code for the TGS-REP AES256 plugin at least gave me an idea of where to start. One such comment was located around the start of the key derivation process:

/*
  at this point, the output ('seed') will be used to generate AES keys:

  key_bytes = derive(seed, 'kerberos'.encode(), seedsize)

  'key_bytes' will be the AES key used to generate 'ke' and 'ki'
  'ke' will be the AES key to decrypt the ticket
  'ki' will be the key to compute the final HMAC
*/

I found more information regarding these different keys in the following two Kerberos 5 RFCs:

  • RFC3961: Encryption and Checksum Specifications for Kerberos 5
  • RFC3962: Advanced Encryption Standard (AES) Encryption for Kerberos 5

The first RFC contained general information about encryption for Kerberos, while the second one had additional information about AES encryption specifically. Besides getting an understanding of the key derivation process, I also needed to look for any parts of the process that could differ based on the types of Kerberos messages involved, which would hopefully explain why the same cryptographic operations weren’t working for my AS-REPs. At the same time, I also inspected the kernel code for the TGS-REP AES256 plugin to see if I could correlate it with what I read in the RFCs.

There were a few hardcoded static values that stood out in the kernel code, the first of which was part of the derivation of the “key_bytes” mentioned in the comment shown above. It also had a brief comment accompanying it:

// we can precompute _nfold('kerberos', 16)
nfolded[0] = 0x6b657262;
nfolded[1] = 0x65726f73;
nfolded[2] = 0x7b9b5b2b;
nfolded[3] = 0x93132b93;

This precomputed data in the “nfolded” array was one of the inputs to the derivation of “key_bytes”. RFC3962 sheds some light on this in Section 4:
To generate an encryption key from a pass phrase and salt string, we use the PBKDF2 function from PKCS #5 v2.0 ([PKCS5]), with parameters indicated below, to generate an intermediate key (of the same length as the desired final key), which is then passed into the DK function with the 8-octet ASCII string “kerberos” as is done for des3-cbc-hmac-sha1-kd in [KCRYPTO].”

It also includes a definition of these functions:

tkey = random2key(PBKDF2(passphrase, salt, iter_count, keylength))
key = DK(tkey, "kerberos")

There’s quite a bit of additional detail in that RFC, but I’m only going to focus on the directly relevant parts to avoid going into an exhaustive discussion of the cryptography. For reference, “[PKCS5]” in the quote refers to RFC2898, and “[KCRYPTO]” refers to RFC3961, which I mentioned earlier. The PBKDF2 function was already used to generate the intermediate key before this point in the code, so we were here:

key = DK(tkey, "kerberos")

This is our “kerberos” string that also showed up in the code comment above, which is provided alongside the intermediate key to the key derivation function, DK. This function is described in Section 5.1 of RFC3961:

Derived Key = DK(Base Key, Well-Known Constant)
DK(Key, Constant) = random-to-key(DR(Key, Constant))
DR(Key, Constant) = k-truncate(E(Key, Constant, initial-cipher-state))

Once again there’s a lot of detail about this in the RFC, but the important thing for now is that DK is the key derivation function, and that there’s some additional complexity regarding our “kerberos” string, which is the constant. The RFC states that “In this construction, E(Key, Plaintext, CipherState) is a cipher, Constant is a well-known constant determined by the specific usage of this function…” So the string “kerberos” is our well-known constant that is always used for AES in Kerberos, according to the above extract from RFC3962, Section 4.

However, it’s not quite as simple as just inputting the string “kerberos”. RFC3961 also states the following:
If the Constant is smaller than the cipher block size of E, then it must be expanded with n-fold() so it can be encrypted.

It then provides additional information about what “k-truncate” means in the DR algorithm, before explaining our mysterious “n-fold()” algorithm:
n-fold is an algorithm that takes m input bits and ‘stretches’ them to form n output bits with equal contribution from each input bit to the output, as described in [Blumenthal96]

Here “[Blumenthal96]” refers to “A Better Key Schedule for DES-Like Ciphers” by Blumenthal, U. and Bellovin, S., 1996. As described above, the n-fold algorithm takes an input like “kerberos” and “stretches” it, in this case so that it matches the cipher block size of E, the encryption function. This is confirmed by another statement in the RFC:
In this section, n-fold is always used to produce c bits of output, where c is the cipher block size of E

The “stretched” value for “kerberos” would be constant for an output of the same length, which is presumably why it was precomputed and hardcoded in the TGS-REP AES256 plugin’s kernel code. The comment has the value “16” as an input to the n-fold algorithm, which is the size of the cipher block in octets, according to RFC3962. RFC3962, Section 6 states that it uses the “simplified algorithm profile” from RFC3961. This profile is detailed in Sections 5.2 through 5.4 in RFC3961, which also happens to be where the keys Ke and Ki are defined that we saw in that first comment in the kernel code. RFC3962, Section 6 also provides a table summarising its parameters for this profile, where the encryption/decryption functions are defined as “AES in CBC-CTS mode (cipher block size 16 octets)“.

The RFCs indicated that “kerberos” was used as the constant for AES encryption regardless of the message type, and an inspection of John the Ripper’s AS-REP plugin also confirmed that it used the same “n-folded” values, so this was not the source of the problem. The reason why I still discussed the details of the key derivation function is that the same function is used to derive the other keys (Ke and Ki), and a lot of the same considerations apply, like the n-fold algorithm.

Derivation of Ki and Ke

The next part of the kernel code was the derivation of the Ki and Ke keys, which are used for decryption and checksum calculation, respectively. What caught my attention here was that there were also static hardcoded values involved in this derivation process. For example, the following values were used for the derivation of Ki:

// we can precompute _nfold(pack('>IB', 2, 0x55), 16)
nfolded[0] = 0x62dc6e37;
nfolded[1] = 0x1a63a809;
nfolded[2] = 0x58ac562b;
nfolded[3] = 0x15404ac5;

As before, these values were provided as input to the key derivation functions, for Ki in this case, and we now know that they represent the “well-known constant” in the key derivation algorithm. However, it took some time to figure all that out after noticing these values for the first time. And there were a few more unknowns here, like the parameters provided to the n-fold algorithm according to the comment (e.g. “2” and “0x55“).

While I was still trying to understand all the information in the RFCs, I decided to take advantage of the fact that John the Ripper’s AS-REP plugin already had a working implementation. I could use it to perform a relatively quick sanity check to see if it was using the same values, which would give me an indication of whether this is what I needed to investigate further, or if I needed to turn my attention elsewhere.

This turned out to be a good idea – the John plugin calculated the n-fold algorithm rather than using precomputed hardcoded values, so there was a bit more information there, and it also included a really useful comment about the derivation of Ki and Ke, which turned out to be quoted from RFC3961, Section 5.3. The relevant bit of code is from the init function in the krb5_asrep_fmt_plug.c source file:

// generate 128 bits from 40 bits of "kerberos" string
nfold(8 * 8, (unsigned char*)"kerberos", 128, constant);

/* The "well-known constant" used for the DK function is the key usage number,
 * expressed as four octets in big-endian order, followed by one octet indicated below.
 * Kc = DK(base-key, usage | 0x99);
 * Ke = DK(base-key, usage | 0xAA);
 * Ki = DK(base-key, usage | 0x55); */

memset(usage, 0, sizeof(usage));
usage[3] = 0x03;        // key number in big-endian format
usage[4] = 0xAA;        // used to derive Ke
nfold(sizeof(usage) * 8, usage, sizeof(ke_input) * 8, ke_input);

memset(usage, 0, sizeof(usage));
usage[3] = 0x03;        // key number in big-endian format
usage[4] = 0x55;        // used to derive Ki
nfold(sizeof(usage) * 8, usage, sizeof(ki_input) * 8, ki_input);

From this code, we can learn the following:

  1. The parameter 0x55 we saw in the comment in the Hashcat TGS-REP AES256 kernel code was simply a constant value from the RFC, used to derive Ki
  2. That value is combined with something else called a “key usage number” to form the full “well-known constant” value, which is provided as input to the n-fold algorithm, similar to what was done with the “kerberos” string when deriving the base key
  3. The key usage number in the John code was 0x03, which happened to be just slightly different from the other parameter in the Hashcat code comment – a value of “2”

This looked very promising. I could now see where these values came from and everything looked the same except for the key usage number, which could be the small difference between the cryptography for TGS-REP and AS-REP that I was looking for. Since John already had this very convenient nfold function (located in the source code file krb5_common_plug.c if you’re interested), I used it to test my theory. I added some printf statements to print out the output from the nfold function, and changed the key usage number to 0x02. Sure enough, the output was exactly the same as the precomputed values in the Hashcat code, so we’re onto something.

I started feeling lucky, so I changed the key usage number back to 0x03, printed out those values for both Ki and Ke, and pasted them into my Hashcat kernel code in place of the original precomputed values from the TGS-REP AES256 plugin. For example, our precomputed n-fold values for Ki were now as follows:

// we can precompute _nfold(pack('>IB', 3, 0x55), 16)
nfolded[0] = 0x6b60b058;
nfolded[1] = 0x2a6ba80d;
nfolded[2] = 0x5aad56ab;
nfolded[3] = 0x55406ad5;

Clearly, the only logical next step was to immediately run the plugin before worrying about further debugging or print statements. Hashcat responded by printing out something like this:

Figure 7: Successful hash cracking using the new Hashcat AS-REP AES256 plugin

It actually worked! Although it took quite a while to get there, essentially, only two small modifications were required to turn the TGS-REP AES256 kernel code into working AS-REP AES256 kernel code:

  1. Changing the conditional statements looking for ASN.1 structures in the decrypted data to match 0x79, as the first byte instead of 0x63
  2. Swapping out the precomputed constants for the Ki and Ke key derivation for values computed using a key usage number of 3 instead of 2

And that was it, the rest of the code just did the AES256 operations using the derived keys, so it makes sense that it would be the same. But I couldn’t simply stop here though, I needed to know what those key usage numbers were and why they were different.

Key Usage Numbers for Key Derivation

RFC3961 mentions the key usage number and where it is used, like in the extract from Section 5.3 that was included as a source code comment in John’s AS-REP plugin. However, it doesn’t provide any information on what the key usage numbers are, and I also couldn’t find any references to where this information could be found. Eventually I realised that the key usage numbers were described in one of the other RFCs related to Kerberos – RFC4120: “The Kerberos Network Authentication Service (V5)”.

Section 7.5.1 of RFC4120 describes what the different key usage numbers are and in which cases each of them are used. The relevant key usage numbers are defined as follows:

  • 2: “AS-REP Ticket and TGS-REP Ticket (includes TGS session key or application session key), encrypted with the service key
  • 3: “AS-REP encrypted part (includes TGS session key or application session key), encrypted with the client key

This explains why “2” was used for the TGS-REP plugins and “3” was used for the AS-REP plugins. In a Kerberoasting attack, we’re trying to decrypt the ticket included in the TGS-REP message, which is a ticket issued by a ticket-granting service (TGS) encrypted with the target service account’s credentials. An AS-REP message is a bit different – the ticket it contains is a ticket-granting ticket (TGT) for the user specified in the authentication request, which is encrypted with the krbtgt key, so the TGT itself won’t help us to crack the user’s password. But the “encrypted part” of the message (EncASRepPart) is encrypted with the user’s credentials, which is why we target it during an AS-REP Roasting attack. And, as indicated by RFC4120, a key usage number of “3” is used for the AS-REP encrypted part.

Finalising the AES256 Plugin

I had everything I needed to make a working Hashcat plugin for AES256 AS-REPs. All that remained was to do further testing to make sure everything really was working correctly, and to do some cleanup where necessary (like removing all the print statements in the kernel code). Hashcat also has a unit test framework that you can make use of to assist with plugin development. I also built the unit test modules for the AES256 AS-REP plugin, based on a similar process of starting with the AES256 TGS-REP plugin’s unit test code and making the same kinds of modifications described above.

Building unit test modules is optional, but it can be helpful. The main reason I implemented the unit tests was because I planned to submit the plugins to Hashcat and wanted everything to be in place. From what I could see, the plugin development guide didn’t say whether unit tests were strictly required when submitting a plugin, but all the existing plugins had unit tests so I made sure mine had working unit tests as well.

I benchmarked the plugin’s performance on our password cracking rig, which has 3 RTX3080 GPUs. The performance was very similar to the AES TGS-REP plugins, indicating that everything was working as expected.

Developing the AES128 Plugin

After completing the plugin for AES256, developing the AES128 plugin was trivial. I followed the same process of starting with the code from the AES128 TGS-REP plugin and making the same modifications. All of the changes were basically the same as for the AES256 plugin; the ASN.1 structures in the decrypted data were exactly the same, and the same precomputed constants could be used for the key derivation process.

I selected a hash-mode number of 33100 for the AES128 plugin (similar to 33200 for the AES256 plugin). As stated earlier, this is also very likely to change if the Hashcat pull request for the plugins is accepted. But these are the hash-mode numbers you need to use if you want to compile the plugin yourself and use it in the meantime.

Bonus Round: An Interesting Edge Case

I came across a very interesting edge case while working on these plugins. To test the plugins, I’d been using a few hashes I captured myself, but later on I also added some of the test hashes from John the Ripper’s AS-REP plugin, since there were also a few example AES128 and AES256 hashes in the module. I used the following hash for the AES256 plugin, which was labeled as luser-18-12345678.pcap in John’s source code:

$krb5asrep$18$EXAMPLE.COMluser$42e34732112be6cec1532177a6c93af5ec3b2fc7da106c004d6d89ddcb
4131092aecbead3e9f30d07b593f4c7adc6478ab50b80fee07db3531471f5f1986c8882c45fef784258f9d431
95108b83a74f6dcae1beed179c356c0da4e2d69f122efc579fd207d2b2b241a6c275997f2ec6fec95573a7518
cb8b8528d932cc14186e4c5d46cef1eed4f2924ea316d80a62b0bcd98592a11eb69c04ef43b63aeae35e9f8
bd8f842d0c9c33d768cd33c55914c2a1fb2f7c640b7270cf2274993c0ce4f413aac8e9d7a231c70dd0c6f8b
9c16b47a90fae8d68982a66aa58e2eb8dde93d3504e87b5d4e33827c2aa501ed63544c0578032f395205
c63b030cccc699aafb9132692c79a154d645fe83927b0eda$420973360c2e907b9053f1db

The password for it was “12345678”. It almost threw me off when the plugin first started working, because this particular hash didn’t crack successfully even though my own hashes did. And, of course, it also cracked successfully in John. Luckily, my print statements helped here and showed something interesting in the first few decrypted bytes:

decrypted_block[0]: 7a81fc30

Maybe this was just random data, but it was rather coincidental that two of the bytes were exactly correct – if the overall length of the data is between 128 and 256 bytes, the second byte here should be 0x81 and the fourth byte should be 0x30. Not only that, but the first byte was 0x7a, which is very close the the 0x79 we were looking for. I temporarily changed the check for 0x79 to 0x7a, to let it run through the rest of the code and see if the checksum matched. This worked, and Hashcat recovered the password successfully, so it must have been valid data.

Another very useful source code comment gives us a good starting point to figure out what these values mean, this time from the Hashcat AS-REP RC4 plugin’s kernel code (hash-mode 18200):

The first byte is always 0x79 (01 1 11001, where 01 = "class=APPLICATION", 1 = "form=constructed", 11001 is 
application type 25)

“Application type 25” is something I recognised from the RFCs. RFC4120 has a list of application class tag numbers in Section 5.10, which are used to identify different data types. Type 25 is “EncASRepPart”, which makes sense, since this is exactly the data we’re dealing with here. Section 5.4.2 provides a detailed definition for the “KRB_KDC_REP” message format, which can be either an AS-REP or a TGS-REP. As expected, “APPLICATION 25” and the “EncASRepPart” is also mentioned here.

Now if 0x79 means type 25, then 0x7a must mean type 26. Just to be absolutely sure I confirmed this with the JavaScript ASN.1 decoder tool:

Figure 8: JavaScript ASN.1 decoder output for the test hash from the John source code, showing the application type

Application type 26 is “EncTGSRepPart”, or the “encrypted part” of a TGS-REP message. So perhaps this data just came from a TGS-REP message instead of an AS-REP message, but that shouldn’t be possible. One very simple problem is that the key usage number would be different – the key usage number for a TGS-REP encrypted part is “8” or “9”, whereas we were using “3” for an AS-REP encrypted part. Because of this, the decryption should have failed. So here we had some “EncTGSRepPart” data that was apparently encrypted using a key usage number for an “EncASRepPart”.

After much confusion, I found the answer by chance in Section 5.4.2 of RFC4120 while I was investigating the key usage numbers. The part where it explained the “enc-part” value included the following note:
Compatibility note: Some implementations unconditionally send an encrypted EncTGSRepPart (application tag number 26) in this field regardless of whether the reply is a AS-REP or a TGS-REP. In the interest of compatibility, implementors MAY relax the check on the tag number of the decrypted ENC-PART.

That solves the mystery – this data must have been generated by such an implementation that just sent application type 26, even though this (probably) wasn’t a TGS-REP message. But what’s also interesting is that we can follow the advice provided by the RFC and relax our check on the tag number, which might help us crack passwords that otherwise wouldn’t have worked, like this one. So I decided to alter the checks in the kernel code to look for either 0x79 or 0x7a. Technically, this change could also be made to the existing Hashcat AS-REP RC4 plugin’s kernel code.

Acknowledgements and Future Work

Apart from the development of the new Hashcat plugin, there’s some more work to be done on the available tooling. Future work includes the following:

  • Getting the plugin approved and integrated into the official Hashcat repository – the pull request was submitted alongside this blog post
  • Updating Rubeus and Impacket to handle AES-encrypted AS-REPs correctly – I’ve also submitted pull requests (Rubeus and Impacket) for these changes alongside this blog post
  • Modifying the existing Hashcat AS-REP RC4 plugin (hash-mode 18200) to also support “EncASRepPart” data tagged with application type 26, as discussed above

I thoroughly enjoyed doing this research and recognise that I was building on lots of excellent work done by others. Thanks to all the people behind Hashcat, John, Rubeus, Impacket, and that handy JavaScript ASN.1 decoder. Especially to the Hashcat team who went through the effort of putting together such a comprehensive plugin development guide. I’d also like to call out several others specifically; @harmj0y for a great post on AS-REP Roasting back in the day and for developing easy-to-use tooling to perform the attack, @Fist0urs for developing the Hashcat TGS-REP AES plugins that mine were based on, as well as @SkelSec and mohemiv for the Hashcat AS-REP RC4 plugin that I also borrowed code from.

References