CVE2-2023-28244, Kerberos MITM, and Root Causes
Network machine-in-the-middle (MITM) attacks are fun. This post will briefly discuss how I wrote a MITM exploit for a public Windows Kerberos vulnerability, and found another vulnerability in the process.
Background
In October of 2022, I was attempting to reproduce James Forshaw’s research on downgrading Windows Kerberos encryption to the “RC4-MD4” cipher. This is an odd legacy cipher in cryptdll.dll
(one of several) that is not part of the documented IETF or Microsoft Kerberos specifications, but could nonetheless be negotiated between Windows client and KDC. Forshaw was able to implement two different practical attacks against the use of this cipher, resulting in CVE-2022-33647 and CVE-2022-33679. This resesarch is detailed in the Project Zero blog post “RC4 Is Still Considered Harmful”, which you may want to familiarize yourself with before continuing to read this post. I was particularly interested in the first attack, which did not require preauthentication to be disabled for any users, and involved MITM between the KDC and a Windows client. (At the time, I was enjoying writing ad-hoc network proxies for various protocols, and Kerberos seemed like a good candidate for a new project.)
While working on an exploit for CVE-2022-33647, I noticed some interesting behavior from the KDC. The attack as described required two separate downgrades. First, the client’s AS-REQ is downgraded to use the RC4-MD4 cipher for the preauth encryption. Second, the client’s list of supported ETYPEs in the AS-REQ body is replaced with a single instance of RC4-MD4, downgrading the KDC to use RC4-MD4 for the TGT session encryption. Both were required in order for the exploit to work, and I didn’t realize initially that these were two independent parameters that were not necessarily always in sync. So I assumed that both would be addressed by Microsoft’s security update, and I was a little bit surprised when I tested against a patched server.
Post-Patch Behavior
With the September 2022 patches applied to the domain controller, I observed that the original downgrade attack did not work.
The KDC service would reject AS-REQ messages that only contain the ETYPE_RC4_MD4
value in the request body.
However, if a client sent an AS-REQ with a preauthentication timestamp encrypted with ETYPE_RC4_MD4
(the first downgrade) but included some standard ETYPE values in the body (skipping the second downgrade), the patched KDC would still respond with an AS-REP where the body was still encrypted using ETYPE_RC4_MD4
.
It’s easy to get mixed up with all these fields and ETYPEs, but note that there are actually 3 different encryption keys involved in this single reply!
- First is the key used by the client to encrypt the timestamp for preauthentication. The KDC then uses the same key to encrypt the “enc-part” (body) of the reply. That key is derived from the user principal’s password.
- Second is the TGT encryption key, which is always one of the
krbtgt
service principal’s keys. This key is used by the KDC to encrypt the TGT itself, which is intended to be decrypted and read only by the KDC when validating service ticket requests. - Third, we have a random TGT session key that was generated by the KDC and provided in the encrypted response. This key is used to encrypt subsequent communications between the client and the KDC when requests are made using the TGT, such as a TGS-REQ. Note that the ETYPE for this key is not visible in the unencrypted packet capture - it is inside the encrypted “enc-part” (body) of the AS-REP, because it is intended only for the service principal that initiated the corresponding AS-REQ.
In general, the first and third ETYPE values are in sync when an AS-REQ without preauth is received by the KDC, but not necessarily when a request with preauth is received.
In our case, only the first encryption is using ETYPE_RC4_MD4
.
The TGT encryption should use the strongest ETYPE supported by the KDC (ETYPE_AES256_CTS_HMAC_SHA1_96
, aka “AES256”).
The TGT session key will use an ETYPE negotiated between the client and the KDC, based on the ETYPE field in the AS-REQ body.
Since the September 2022 patches, the KDC does not accept ETYPE_RC4_MD4
for this purpose.
Well, yeah, the original exploit did use the fact that only the first 5 bytes of the session key needed to be recovered. Incredibly, there was a 4-byte overlap in the RC4 keystream between the known plaintext of the (AS-REQ) encrypted timestamp and the (AS-REP) start of the encrypted session key, which was just enough to recover the first 4 bytes and allow easy brute-force of the final one. Without the session key downgrade, those 4 bytes wouldn’t be nearly enough to guess the full 16 or 32 random bytes of a normal RC4 or AES session key.
But the bigger problem seems to be that the preauth timestamp and the AS-REP encrypted part reuse the same RC4 keystream for encryption (because the keystream is generated solely from the user’s password.) This is the catastrophic flaw that allows us to decrypt parts of the AS-REP based on known plaintext in the encrypted timestamp. As long as the KDC accepts an RC4-MD4 encrypted preauth, that flaw still exists and can be potentially abused. We just need to come up with a new avenue of attack.
To get an idea, we can look at Forshaw’s second attack (CVE-2022-33679), and specifically at how the KDC Authentication Server can be used as an encryption oracle.
Preauth Encryption Oracle
The writeup for CVE-2022-33679 describes a way to take a valid AS-REP encrypted with RC4-MD4, and use it to forge an AS-REQ with a valid RC4-MD4 preauth timestamp.
This attack relied on first retrieving a valid AS-REP for a domain user account that was configured to not require Kerberos preauthentication.
Since our patched KDC will no longer accept an ETYPE_RC4_MD4
value in the request body, we can not request a downgraded AS-REP in this way anymore.
However, the AS-REQ forgery still works, as long as we have a valid AS-REP, which we can get via our MITM downgrade.
Per CVE-2023-33679, the AS-REP gives us 45 bytes of RC4 keystream, which (incredibly) is the exact length we need in order to encrypt a minimal PA-ENC-TS-ENC
structure.
We can adjust our tag length values to account for one additional null byte at the end of the plaintext, and then brute-force the final byte of keystream by sending repeated AS-REQ messages to the KDC with different final byte values for the ciphertext, until the KDC responds with an AS-REP.
As noted, the Windows decoding is flexible and accepts multiple variable-length encodings of length values, so we can then expand the plaintext SEQUENCE length value on byte at a time, encrypt with the expanded keystream, and repeat our brute-force for at least another 4 bytes.
Not exactly. After 4 bytes of expansion, I started getting errors on the fifth byte.
My offhand guess is, Windows models those lengths as 32-bit integers internally, and the decoder expects a big-endian encoded value than it can pad to up to 4 bytes. But all is not lost, there are other things we can try with the ASN.1 structure to increase its overall length.
ASN.1 Encoding Expansion
Let’s dig a little bit more into some of the details of Kerberos data structures and ASN.1 encoding.
Given the format of the PA_ENC_TS_ENC
structure, the minimum length of a plaintext encoding is 21 bytes (or 45 total bytes if you count the 24 null byte prefix), which conveniently overlaps exactly with the predictable plaintext length in the EncASRepPart
from the AS-REP message.
There are 3 length values present (the overall SEQUENCE
value length, the length of first SEQUENCE
element, and the length of the inner GeneralizedTime
value), each of which can be expanded by 4 bytes, for a total of 12 bytes.
In addition, we can add a null byte to the end of the GeneralizedTime
ASCII value, which takes us up to 13 bytes.
If we add the optional Microseconds
value to the structure (which includes two length values itself) we can expand the plaintext by another 12 bytes, for a total of 25 bytes.
This is more than enough to recover a 128-bit (16 bytes) AES key.
(The actual Microseconds
value itself can be up to 4 bytes long, but it can take any value, so it doesn’t help us to guess any additional keystream bytes beyond the tag and length.)
Although… the max value of the pausec
field should be 999_999
, or 0x000f423f
, so we might be able to use this to guess an additional 1 and 1/2 bytes of keystream, for a total of 26 and 1/2 bytes (untested).
This could reduce the recovery of a 256-bit AES session key to a 48-bit brute force attack.
That still seems pretty hard, so you might have better luck cracking the account password instead, if it is not a machine account.
I tried modifying the structure a few other ways to extend the length further, but I was unsuccessful. Maybe someone else will come up with a clever idea that works though.
Very true, though in this case, we are limited by the maximum length we can expand our timestamp structure.
To make this attack practical, we will need to avoid negotiating an AES256 session key.
We can use the same session key “downgrade” as CVE-2022-33647, but choose one of the standard ETYPE values with a 16-bit key, such as ETYPE_AES128_CTS_HMAC_SHA1_96
(aka AES128).
Just keep in mind that the session key ETYPE will affect the plaintext of the EncASRepPart
in the resulting AS-REP, which we need to account for when recovering our initial 45 bytes of keystream.
Putting It All Together
By combining the pieces described above, we can conduct a practical attack by-
Pretty much.
If we wait a little while, domain hosts will reauthenticate periodically, giving us an opportunity to compromise their domain machine accounts. Depending on user activity, we may also see TGT requests for user accounts come across. I briefly looked around for potential ways to coerce authentications to the KDC, but didn’t find anything that seemed easy and reliable, though I may have missed something obvious.
Anyway, here is the final proof-of-concept code in action…
Once we have enough keystream bytes, we can recover the TGT session key, which allows us to login (request service tickets) as the compromised user account.
The POC code can be found at https://github.com/sk3w/cve-2023-28244
Disclosure Timeline
This vulnerability was reported to Microsoft on 2022-11-21 with a detailed write-up and proof-of-concept code, and was acknowledged fairly quickly. Microsoft addressed the issue in the 2023-04-11 security updates, assigning the issue CVE-2023-28244.
Defensive Considerations
It does not appear straightforward to detect this downgrade out of the box on Windows.
The original CVE-2022-33647 and CVE-2022-33679 downgrades will result in Event ID 4768 events being generated with a Ticket Encryption Type value of -128
(0x80
), which is a clear red flag.
But, when only the pre-authentication etype is downgraded to RC4-MD4, the Ticket Encryption Type value will reflect the TGT session key etype (17
or 0x11
for our proof-of-concept exploit), and there is no other field to indicate the pre-authentication ETYPE.
Nor was I able to find an ETW provider with the pre-authentication etype value.
The only way I’ve been able to detect this downgrade is by looking at full packet data, parsing the AS-REQ structure, and looking at ETYPE values in the PA_ENC_TIMESTAMP
structures for a value of -128
(0x80
).
This might make other preauth downgrade attacks harder to detect as well.
On the preventative side, patch your domain controllers to ensure that this vulnerability is removed.
Also note that the AllowOldNt4Crypto
registry key can revert the behavior of the patches, so make sure that key doesn’t get set.
Consider disabling RC4 completely where possible, and only allow the standard AES ciphers for Kerberos.
If RC4 is disabled on Kerberos clients, it shouldn’t be possible to perform this downgrade attack, and should reduce risk from other RC4-related attacks.
For Server 2012 and beyond, adding sensitive domain accounts to the Protected Users
group is a great way to reduce risk.
Users in this group cannot use RC4 encryption types in Kerberos preauthentication, which should prevent this attack and reduce risk in a number of other ways.
If a user in this group is somehow compromised, the TGT is non-renewable with a maximum lifetime of 4 hours.
Epilogue
Fixing vulnerabilities is hard, especially when your software is widely deployed and your customers rely on a strong commitment to backwards compatibility. There is a balance between fixing the security issue and changing as little else as possible. Unfortunately, this can sometimes lead to edge-cases where a similar vulnerability may still exist. For an aspiring vulnerability researcher, it can be worthwhile to look at recently patched vulnerabilities to search for related issues or edge cases. For a defender, it shouldn’t necessarily be assumed that a patch will completely resolve an issue. Attack surface reduction through configuration hardening (such as completely disabling RC4-based ciphers) and active monitoring post-patch are critical tools to help cover these potential gaps.