So on 9th November 2021, Cliff Fisher tweeted about a bunch of CVE's to do with Active Directory that caught a lot of people's eyes. These included CVE-2021-42278, CVE-2021-42291, CVE-2021-42287 and CVE-2021-42282. The one that caught my eye the most was CVE-2021-42287 as it related to PAC confusion and impersonation of domain controllers, also having just worked on PAC forging with Rubeus 2.0.
This post discusses my quest to figure out how to exploit this issue and some things I discovered along the way.
I just want to highlight that there's no new research here, this issue was discovered by Andrew Bartlett of Catalyst IT. I just found one way to weaponise it, there may well be others in the issues he found.
A Little Digging
So immediately upon seeing Cliff's tweet, Ceri Coburn and I started tryng to figure out how this could be exploited. We (perhaps incorrectly) latched onto the text on Microsofts description of CVE-2021-42287 which seemed to be based around the idea of TGT's being issued without PACs. This led me to modify Rubeus to allow for requesting TGT's without a PAC.
After Ceri debugging the Windows KDC and us digging through the leaked XP source we were convinced that to trigger the codepath we needed to go down (to insert a PAC into a ST when it was requested with a TGT lacking a PAC) required a cross domain S4U2self but was unable to get it to work. The only way we could get a DC to add a PAC when an service ticket (ST) was requested using a TGT without a PAC was by configuring altSecurityIdentities.
This process involves modifying the altSecurityIdentities attribute of an account in a foreign domain to Kerberos:[samaccountname]@[domain] to impersonate that user.
So below you can see a low privileged user (internal.user) of the local doamin (internal.zeroday.lab) has GenericAll over a high privileged user (external.admin) of a different domain (external.zeroday.lab):
As this user we can add ourselves to the altSecurityIdentities attribute as shown below:
Now we can get a TGT from our local DC and request it without a PAC using the new
This results in an obviously small TGT. We then use that TGT to request a referral to the target domain (external.zeroday.lab) from our local DC:
That referral can then be used to request ST's for services on our target domain (external.zeroday.lab). Here I'm requesting a ST for LDAP/EDC1.external.zeroday.lab the DC's LDAP service:
The size of the ST is very large compared to the previous 2 tickets, this is because (as we'll see) a PAC has been added. As shown in the klist output below, this ST is for the original user internal.user which has no special privileges on either domain:
Using this ST, however, we can DCSync:
So what happened here is the DC has searched for the account in the local database, it hasn't found it so it's then searched to see if any accounts have this account listed in their AltSecurityIdentities attribute, which external.admin does because we added it earlier, and if so, the DC adds a PAC belonging to that account. This can be verified using Rubeus'
describe command and the AES256 key we just DCsync'd:
We now effectly have the privileges of the external.admin user on the external.zeroday.lab domain.
This didn't help us exploiting the issue we wanted but I did find it interesting.
Then along came this tweet from Clément Notin which actually mentioned the Samba information regarding these issues and led me to CVE-2020-25719 and this patch. What particularly caught my attention was this paragraph:
Delegated administrators with the right to create other user or machine accounts can abuse the race between the time of ticket issue and the time of presentation (back to the AD DC) to impersonate a different account, including a highly privileged account.
Suddenly I realised that to make the local lookup fail, we didn't need to attack a foreign domain but perhaps remove the account after retrieving the TGT.
I started playing with naming a machine account the same as the local DC (minus the $), requesting a TGT (still without a PAC), removing the machine account and using that TGT. I noticed something funny.
When using this PAC-less TGT with a U2U request but without supplying an additional ticket, it was failing to decrypt the resulting ST:
The U2U ST should be encrypted with the session of within the provided TGT but as I didn't provide an additional ticket I assumed it was triyng to lookup the account based on the sname which I was setting to IDC1 the samaccountname of my now missing machine account. I had the idea to try decrypting this ST using the long term key of the domain controller that I was naming my machine account after (IDC1$):
It worked! It sucessfuly decrypted the ST, it just couldn't find the PAC because there wasn't one there. I tried the same thing using S4U2self and got the same result, the DC was looking for my IDC1 account, not finding it and then search for the same but adding a $ on the end, finding the domain controller account and encrypting the ticket using it's key instead.
At that time I still couldn't figure out why it wasn't adding the PAC, so I decided to try requesting the initial TGT with a PAC instead of without a PAC and surprisingly it worked! So apparently there was no need to request a TGT without a PAC, supplying a TGT with a PAC for an account that has the samaccountname of the DC minus the $ to a request for an S4U2self ticket, when the intial account no longer exists, results in the ST being encrypted using the key of the DC.
The sname of that resulting ST can be modified as per Alberto Solino's post here. So it can be used to authenticate against any service on the target box, even users protected from delegation, as Elad Shamir mentions in the Solving a Sensitive Problem section of Wagging the Dog.
The last thing to work out was how can we get a machine account in this state from a low privileged user, as until now I was manually modifying the machine account as an admin. Thankfully Kevin Robertson's amazing post on the Macine Account Quota helped massively. It explains that the creator of the machine account has control over various attributes including the samAccountName and ServicePrincipalName. Another problem I was running into was trying to change the samaccountname, as trying to change it to be the same as the DC minus the $, I was getting the following error:
As Kevin mentions in his post:
If you modify the samAccountName, DnsHostname, or msDS-AdditionalDnsHostName attributes, the SPN list will automatically update with the new values.
So the SPN it was trying to set was already an SPN belonging to the target DC. Ceri suggested removing the SPNs before changing the samaccountname, which worked.
Lastly, until now I was removing the machine account after requesting the TGT (which requires admin privileges), I had to test whether disabling it or renaming it worked too. Disabling it resulted in a S_PRINCIPAL_UNKNOWN error being returned by the DC when requesting the S4U2self but renaming it worked.
Finally all of the pieces were in place.
Checking If Exploitable
To determine if a DC is patched is very easy. Using my additional
/nopac switch to Rubeus'
asktgt, request a TGT without a PAC, if the DC is vulnerable it'll look like the following:
Look at the size of the returned TGT. If the DC is not vulnerable the TGT will look as follows:
The size difference is immediately obvious. The next thing to check would be the MAQ:
By default it is 10 as above but can be changed, anything above 0 will do. Lastly we need to check the SeMachineAccountPrivilege which is granted to Authenticated Users by default:
If everything checks out, we can exploit this issue.
The Full Attack
The first step is to create a machine account we can use for the attack (The account I create is called TestSPN$). Kevin's Powermad works nicely for this:
After this, PowerView's
Set-DomainObject can be used to clear the SPNs from the machine account:
Changing the machine account's samaccountname can be done using Powermad's
Set-MachineAccountAttribute (Here I'm changing it to IDC1, because the DC's samaccountname is IDC1$):
asktgt can be leveraged to request a TGT for that newly created machine account (This is just a normal TGT for the machine we just created but using it's new samaccontname):
Set-MachineAccountAttribute can again be used to change the machine accounts samaccountname (either back to what it was or something else entirely, it doesn't matter):
With the machine account renamed, it is now possible to request an S4U2self ticket using the retrieved TGT and get an ST encrypted with the DC's key, at the same time we can rewrite the sname within the ticket to be the LDAP service:
Here, I've impersonated the Administrator user for the LDAP service on the DC. It's worth noting that this could be any user on any service on any system on the domain.
The ticket has been sucessfully injected into LSA as shown below:
Using that ticket it is now possible to DCSync:
The commands I run to do this are shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Mitigation / Detection
The best way to mitigate against this is to install the Microsoft patch (KB5008602), this patch fixes the issue with PAC confusion as well as fixes this issue with S4U2self created by the earlier KB5008380 patch.
Setting the Machine Account Quota to 0 is a quick and easy fix for stopping low privileged user from being able to create machine accounts, another related fix is to remove Authenticated Users from the SeMachineAccountPrivilege and adding Domain Admins or another group of allowed accounts.
There are several Events caused by various steps which would be useful for determining attempts to perform this attack. The credit for determining these Events should go entirely to Andrew Schwartz, I simply sent my logs to him after I performed the attack.
Machine Account Creation
Firstly, there is a 5156 event of an inbound connection to LDAP to create the machine account, For this event ID Andrew leveraged the research of “A Voyage to Uncovering Telemetry: Identifying RPC Telemetry for Detection Engineers” By: Jonathan Johnson:
Immediately followed by a 4673 event, which is the usage of the SeMachineAccountPrivilege:
As well as a 4741 event, describing the creation of the machine account:
And a 4724 event, regarding the password reset of the newly create machine account:
Clearing The SPNs
Next a 4742 event can be seen when the SPNs are removed from the machine account, this will show
Changing the SamAccountName
A 4742 event will also show when the SAM Account Name is changed, and the new name will be shown in the SAM Account Name field:
More interestingly, a 4781 event will show both the old account name and the new account name:
When retrieving the TGT, a 4768 event will show, interestingly the Account Name field will show the new name of the account, while the User ID field will show the old name:
Then the account name change happens again with the 2 events mentioned above.
Lastly, as Elad mentions in his Wagging the Dog post, event 4769 fires, this time, however, some discrepancy is shown between Account Name and Service Name, while the Service Name field has the proper machine account name, the Account Name is missing the trailing dollar ($):
With the November 9th updates, many changes were made to AD and I wouldn't be surprised if many other avenues existed using those issues but the one I use directly from a low privileged user to full domain takeover with a default configuration.
Ensuring all DC's are fully patched should be the main priority here as it will only take 1 unpatched DC for domain takeover to be possible.
On top of patching, proper AD hardening with decent monitoring will always minimise the impact of any compromise significantly.