Normal view

There are new articles available, click to refresh the page.
Before yesterdayIFCR - Medium

Pass-the-Challenge: Defeating Windows Defender Credential Guard

26 December 2022 at 20:42

In this blog post, we present new techniques for recovering the NTLM hash from an encrypted credential protected by Windows Defender Credential Guard. While previous techniques for bypassing Credential Guard focus on attackers targeting new victims who log into a compromised server, these new techniques can also be applied to victims logged on before the server was compromised.

Credential Guard is intended to safeguard both NTLM hashes and Kerberos tickets, but for the purposes of this post, we will focus solely on NTLM hashes.

We will provide a general overview of the technical details, with the option to explore further for those who are interested in the underlying mechanics.

Note that when we mention an “NTLM hash,” we are actually referring to the NT hash, as the LM hash is obsolete.

Credential Guard 101

There are many blog posts about Credential Guard, some of which are more accurate than others. In this section, we will provide an overview of Credential Guard and explain why it is important for attackers and defenders to understand it.

To quote Microsoft about Credential Guard, Pass-the-Hash, and Pass-The-Ticket:

Windows Defender Credential Guard prevents these attacks [Pass-the-Hash and Pass-The-Ticket] by protecting NTLM password hashes, Kerberos Ticket Granting Tickets, and credentials stored by applications as domain credentials.

The LSASS (Local Security Authority Subsystem Service) process is responsible for managing and enforcing security policies on a Windows system. It handles tasks such as authenticating users, granting access to resources, and enforcing security policies.

The LSASS process stores credentials in memory, including hashed passwords. Windows Defender Credential Guard aims to safeguard these credentials by isolating them from the LSASS process memory. We will cover the implementation of this in more detail shortly.

Why do we care?

Attackers often attempt to dump credentials from the LSASS process memory on a compromised machine in order to move laterally within the network, using tools like Mimikatz that can extract various credentials, including plaintext passwords, NTLM hashes, and Kerberos tickets.

In a Pass-the-Hash (PtH) attack, an attacker can use a compromised NTLM hash to authenticate to a system or service without knowing the actual password. This is possible because NTLM hashes are derived from the user’s password and are used for authentication in many protocols.

Credential Guard appears to protect against these types of attacks by isolating NTLM hashes (and Kerberos tickets) in the LSASS process memory, thus protect against the initial compromise of a user’s NTLM hash.

Now, if we compromise a system that has Credential Guard enabled and attempt to extract credentials from the LSASS process memory using Mimikatz, what do we observe?

As demonstrated above, we are unable to extract the NTLM hash from LSASS memory and are instead presented with “LSA Isolated Data: NtlmHash”. For comparison, this is what the output would look like on a system that is not protected by Credential Guard.

This compromised NTLM hash could then be used to authenticate to a system or service.

At IFCR, we have noticed an increasing number of systems protected by Credential Guard during our Red Team engagements. Additionally, starting with the Windows 11 Enterprise version 22H2 and Windows 11 Education version 22H2, systems that are compatible with Credential Guard have it turned on by default.

How?

Windows Defender Credential Guard uses virtualization-based security (VBS) to isolate secrets. VBS utilizes hardware virtualization features to create a secure region of memory that is separate from the normal operating system.

To understand the challenges attackers face when dealing with Credential Guard, it can be helpful to think of the normal operating system running inside one virtual machine (VM) and secure processes running inside another VM with a separate kernel. These VMs are managed by the “Hypervisor”.

Virtualization Based Security (VBS) and Hypervisor Enforced Code Integrity (HVCI) for Olympia Users! — Microsoft Community Hub

Even if attackers were to gain kernel code execution within the normal operating system, they would still need to escape the VM by attacking the Hypervisor or the secure VM. This scenario is similar to what happens with virtualization-based security.

So, how does Credential Guard work? When Credential Guard is enabled, a process called LSAIso (LSA Isolated) runs inside the secure VM. LSASS and LSAIso can communicate through advanced local procedure calls (ALPCs)

How Windows Defender Credential Guard works | Microsoft Learn

When the LSASS process wants to protect a secret, it can call upon LSAIso to encrypt it. The encrypted secret is then returned to LSASS. Ideally, only LSAIso should be able to decrypt the secret. Once an NTLM hash is protected, the LSASS process only holds an isolated secret (an encrypted blob).

As shown in the picture above, the LSAIso process has “NTLM support”. When the LSASS process wants to perform an NTLM operation on the encrypted secret, it can call on various methods in the LSAIso process to perform the operation. It’s worth pointing out that LSAIso does not have network access. Therefore, even though LSAIso can perform NTLM operations, the LSASS process is still responsible for carrying out any actions that come before and after the operation. For example, while LSAIso can compute an NTLM Challenge/Response pair, LSASS is responsible for receiving and sending the pair.

Previous State of Defeating Credential Guard

Until now, the only publicly known attacks against Credential Guard involve attacking the authentication pipeline of LSASS before secrets are protected. For example, this could involve hooking a function in LSASS so that when a new victim logs on to a compromised server, their credentials can be compromised before they are sent to LSAIso.

New State of Defeating Credential Guard

Although it requires a few additional steps, it is still possible to recover the NTLM hash of an NTLM isolated secret. In other words, if we manage to compromise a system and come across encrypted NTLM credentials in the LSASS process’ memory, we can still extract the NTLM hash.

By leveraging the exposed functionality of the LSAIso process along with the encrypted credentials, we can perform the required operations to ultimately obtain the NTLM hash.

Recovering the NTLM hash

In this section, we will demonstrate two techniques for obtaining the NTLM hash.

First, we must gain code execution within the LSASS process in order to communicate with the LSAIso process over the established ALPC communication channel. This step is described in more detail in the “Technical Details” section for those interested in the underlying mechanics.

Second, we must obtain a memory dump of the LSASS process to extract the encrypted credentials. I have modified the Pypykatz code to accomplish this, and the details are also provided in the “Technical Details” section.

For this example, I have created a domain administrator account with a long, random password that corresponds to the NTLM hash 65A13AB2FAEB5B700DE1A938AE5621CA.

In this scenario, we have obtained a memory dump of the LSASS process and extracted the encrypted credentials using the modified version of Pypykatz, as illustrated below.

In the next two sections, we will demonstrate how to extract the NTLM hash 65A13AB2FAEB5B700DE1A938AE5621CA from the “encrypted blob” (isolated secret).

I have also created a tool called “PassTheChallenge” that invokes LSAIso methods by loading a library into the LSASS process. This tool will be made available with this blog post, and the technical details and required information will be provided in the “Technical Details” section.

PtCv1

This technique allows us to recover the NTLM hash in a relatively short amount of time.

The LSAIso process exposes a method named NtlmIumCalculateNtResponse, which allows us to calculate an NTLMv1 response for an arbitrary server challenge using the encrypted credentials.

By utilizing the “Context Handle”, “Proxy Info”, and “Encrypted blob” from the memory dump, we can pass these parameters to PassTheChallenge.exe using the command nthash. By default, the tool uses the static challenge 1122334455667788. If all goes well, it will produce an NTHASH. Despite the format, this is not the NTLM hash, but rather an NTLMv1 response.

Now for the interesting part. With the NTLMv1 response and the static challenge 1122334455667788, we can submit it to crack.sh and have it cracked for free. Before you think that this requires hours cracking, just keep reading. We’re not cracking the NTLMv1 response into a password, but rather into an NTLM hash.

If you need a refresher on how an NTLMv1 response is generated, you can refer to the following code:

The NTLMv1 response consists of three 8-byte chunks. Each chunk is created by encrypting the challenge with DES and a 7-byte chunk of the key. You may have noticed that three 7-byte chunks of the key add up to 21 bytes, which is larger than the 16-byte NTLM hash. To address this, the last chunk of the key is made up of the first 2 bytes of the NTLM hash followed by null bytes.

To summarize, we need to crack three DES encryptions using three different 7-byte keys, one of which only has 2 unknown bytes, to recover the NTLM hash. This process is much simpler than cracking a single 16-byte key. Fortunately, crack.sh has built one of the largest publicly available Rainbow Tables for the entire DES keyspace. They claim to be able to achieve an average crack time of 25 seconds and a success rate of 99.5%. If the system is unable to crack the key immediately, it will pass the job on to a brute-force rig, which should be able to find the key within a few days.

Let’s see it in action. First, we can submit the NTHASH format to crack.sh for free.

In less than a minute, I received an email from crack.sh stating that the NTLM hash was successfully recovered in 30 seconds: 65A13AB2FAEB5B700DE1A938AE5621CA.

While this is not an advertisement for crack.sh, it does illustrate how straightforward it is to crack an NTLMv1 hash back into an NTLM hash.

This particular hash was not previously submitted to crack.sh, and the use of a long, random password indicates that cracking is not a matter of password complexity, but rather the inherent weakness of NTLMv1 hashes.

PtCv2

Now, let’s suppose that the NTLMv1 option is not available for some reason — perhaps Microsoft decided to remove it. Another interesting option is to compute an NTLMv2 respone using the LSAIso method NtlmIumLm20GetNtlm3ChallengeResponse.

To demonstrate this, I have modified the Impacket function computeResponseNTLMv2 to interactively print the challenge and prompt for a response. This single modification will work with all scripts and tools that utilize Impacket for authentication, such as Certipy.

When the password is set to CHALLENGE, the computeResponseNTLMv2 function in Impacket will instead print out the challenge and prompt for a response. Let’s see it in action. For the first demonstration, we will use Impacket’s psexec.py to gain code execution on the domain controller DC.

We will run the script as usual, but for my modification to take effect, we need to specify the password CHALLENGE. Impacket will then print out a custom formatted challenge and wait for a response, as shown below.

We can then utilize PassTheChallenge.exe as before, but this time we use the command challenge and append the challenge to the previously used parameters.

The tool will then output a response, which we will provide to psexec.py.

As shown above, psexec.py performs multiple authentications, so we will need to repeat these steps a few times. However, we eventually gain access to a shell and achieve code execution.

While this did not allow us to recover the NTLM hash, it does raise the possibility of other ways to do so using our NTLMv2 primitive.

If you’ve read some of my other posts, you may have noticed my fondness for Active Directory Certificate Services (AD CS). If you’re not familiar with AD CS, I recommend checking out one of my previous posts on the topic.

Now, let’s utilize our NTLMv2 primitive and Certipy to request a certificate for the victim user “Administrator” through Active Directory Certificate Services.

As previously mentioned, the modified Impacket function will even cause Certipy to print the challenge and prompt for a response when the password is set to CHALLENGE. As shown above, the certificate request was successful and the issued certificate along with the private key was saved to administrator.pfx.

Finally, we can use the certificate and Certipy to authenticate as the administrator user and retrieve the NTLM hash, as demonstrated below.

It’s worth mentioning that in order to request certificates and authenticate with them, it is necessary to have AD CS installed in the environment. Once AD CS is enabled, any user can be targeted by default.

An Extra Note

Based on my own testing, it appears that encrypted credentials can be used across reboots, contrary to some existing information. It seems that LSAIso does not use a per-boot secret to encrypt the credentials. Therefore, it is possible to return to the compromised machine at a later time to perform these operations. The only thing to keep in mind is that the “Context Handle” and “Proxy Info” are memory addresses extracted from the memory dump, and these need to be recalculated through a new memory dump or similar methods. The “Context Handle” and “Proxy Info” addresses are not tied to a specific set of credentials, and all encrypted NTLM credentials will have the same “Context Handle” and “Proxy Info” address in the memory dump. Refer to the “Technical Details” section for more information.

Conclusion

In this blog post, we focused specifically on NTLM. However, it’s worth considering that Kerberos may also offer interesting functionality that could help us achieve our goals.

Earlier in the post, we referenced a quote from Microsoft’s page on Credential Guard. Here’s how that same page concludes:

While Windows Defender Credential Guard is a powerful mitigation, persistent threat attacks will likely shift to new attack techniques(…)

And so we did.

Technical Details

If you’ve read this far, you may be interested in the technical details, whether you are an attacker or a defender.

LSASS and LSAIso

As previously mentioned, LSASS and LSAIso communicate with each other through ALPC and RPC.

The Security Support Provider (SSP) “MSV” (msv1_0.dll) is responsible for NTLM and Kerberos authentication. When the msv1_0.dll module is loaded into LSASS and the SpInitialize function is called to initialize the SSP, if Credential Guard is enabled, the module will create a new NtlmCredIsoIum object and save the address of this object to the global variable LocalhostNtLmCredIsoObj::IsoObj. This object acts as an interface for implementations of the NtlmCredIsoApi interface. If Credential Guard is not enabled, the LocalhostNtLmCredIsoObj::IsoObj variable will instead store the NtLmCredIsoInProc object, which implements methods that do not communicate with LSAIso, resulting in the credentials not being encrypted.

When a new NtlmCredIsoIum object is created, it establishes an ALPC binding to LSA_ISO_RPC_SERVER, where LSAIso is already listening for procedure calls. After the binding is established, MSV will call LSAIso’s NtlmIumGetContext method to obtain a unique context handle.

The context handle is passed to the NtlmCredIsoIum object during initialization and stored at offset +0x10.

Inside LSAIso, the NtlmIumGetContext method checks if an “auth cookie” has already been given out. If it has, the method aborts. If not, it stores the “auth cookie” in the provided context handle.

Based on my own testing, the LSAIso process assigns a unique “auth cookie” value to its own memory and associates it with the context handle provided by the LSASS process. This means that the LSASS process cannot directly access the “auth cookie” value, but when it communicates with LSAIso using the context handle, LSAIso can recognize that the “auth cookie” value is associated with that specific context handle.

In order to utilize the binding established between the LSASS and LSAIso processes, I have implemented a tool that loads a module into LSASS. Initially, I attempted to duplicate or extract the binding, but it appears that the inner workings of RPC bindings are more complex than just a single pointer or value within a process.

As for the use of the context handle, by examining the LSAIso executable, we can see the methods supported by the NTLM interface.

If we examine the NtlmIumProtectCredential method within LSAIso, we can see that it checks if the provided context handle matches the global “auth cookie” value. If there is no match, the method will pause for 5 milliseconds before returning an error code (0xc0000022).

The “auth cookie” may seem to be the first argument of the function, but it is actually the RPC marshalling and context handling that transforms the context handle into the “auth cookie”. If we attempt to call the function using a 64-bit value rather than a context handle, the RPC server within LSAIso will return a “bad RPC stub” or “invalid handle” error because the marshalling of our client does not match the unmarshalling of the server.

It may be possible for someone to come up with a smarter implementation for duplicating the binding and context handle across processes.

Now that we understand how LSASS and LSAIso communicate, let’s look at some examples. The NtlmIumProtectCredential method mentioned earlier is used to protect an unprotected credential set (such as NTLM).

To summarize, the IumpProtectCredential function will encrypt the credentials within the MSV1_0_SECRETS_WRAPPER structure and return it to LSASS. This means that LSAIso does not maintain a record of encrypted credentials; it is LSASS’ responsibility to do so. LSAIso just knows how to decrypt the credentials and perform operations on them.

Let’s examine the NtlmIumCalculateNtResponse method within LSAIso.

As before, the method first verifies that the context handle is valid. It then checks if the IsEncrypted field and NtPasswordPresent field of the MSV1_0_SECRETS_WRAPPER structure are set to true. This structure is not documented, but I have reverse engineered it and it can be found in the released tool.

If both fields are true, the method then calls IumpUnprotectCredential to decrypt the credentials. It’s worth noting that the declaration of this method in the RPC interface does not return the decrypted MSV1_0_SECRETS_WRAPPER to LSASS. After decrypting the credentials, the method calls SystemFunction009 inside CRYPTSP.DLL. The Wine implementation of that function can be found here.

Now let’s take a look at how this method is called within LSASS.

As can be seen, the call to NdrClientCall3 uses the “Proxy Info” object (_MIDL_STUBLESS_PROXY_INFO) as its first parameter. The procedure number 3 for NtlmIumCalculateNtResponse is passed as the second parameter, followed by the context handle (this + 0x10) as the fourth parameter, the server challenge as the fifth parameter, the MSV1_0_SECRETS_WRAPPER (encrypted credentials) as the sixth parameter, and a pointer to the response as the seventh parameter.

The RPC interface shared between LSASS and LSAIso specifies which data is exchanged between the two processes. As a result, one cannot assume that an operation on a parameter within LSAIso will have any effect on the corresponding parameter in LSASS.

These are just examples of how LSASS and LSAIso can communicate.

Pypykatz

I have made a few modifications to Pypykatz in my own version to print out additional information. Specifically, I have added a feature to read and print out the encrypted blob in the case that the credential is encrypted, rather than reading the encrypted blob as NT, LM, and SHA values.

Additionally, the variable pNtlmCredIsoInProc actually holds a pointer to the NtlmCredIsoIum object mentioned in the previous section if the credential is protected by Credential Guard. As a result, the “Context Handle” can be found at offset +0x10 of this address.

We will also dynamically extract the address of the “Proxy Info” object used in the NdrClientCall3 call.

The output will appear as follows:

The modified version can be found on my Github.

PassTheChallenge

Finally, I have created a tool that consists of an executable and a SSP (Security Support Provider) provider (DLL).

The plan is to use AddSecurityPackage to load a new SSP provider into the LSASS process. Our custom SSP will then start a local RPC server that we can communicate with from our executable.

This allows the PassTheChallenge tool to connect to the new local RPC server started by our security package, allowing us to communicate back and forth and call various functions.

The PassTheChallenge executable includes several different commands. One of them is the injection of the module, which can be done as follows:

Once the module is loaded, we can issue a simple “ping” command to see if it is responding.

Here is how the tool can be used:

For instance, LSAIso enables us to compare two encrypted blobs. By combining this with a method for securing our own credentials, we can compare an encrypted blob with another encrypted blob or with an NT hash.

The <addresses> parameter is obtained from the output of Pypykatz and represents the <context handle>:<proxy info>. This is followed by an encrypted blob, which can then be compared to either another encrypted blob or an NT hash.

From an operational security perspective, it’s important to be aware that the tool may not be very secure and there may be errors present. It’s crucial to use caution when working with this tool as crashing the LSASS process can compromise the stability of the operating system. Therefore, it’s advisable to be careful and avoid introducing any incorrect addresses or similar. After the module is loaded, it will remain in the LSASS memory until the next system reboot.

The tool can be found here.

Impacket

I have also added six lines of code to the Impacket source code in the computeResponseNTLMv2 function within the ntlm.py file.

The patch is relatively straightforward. If the password is CHALLENGE, it will print out the domain, user, server name, and server challenge in a specific format. This challenge can then be used with the PassTheChallenge tool. The output from PassTheChallenge can then be passed back to Impacket, which will use the challenge response and session key that are computed.

The patched version can be found on my Github page.

Further Research

In this post, we have not yet discussed Kerberos, which will be a focus of future research. If you are interested in researching Credential Guard, you may encounter challenges such as “How to debug LSAIso?” (spoiler alert: I am not aware of a solution). However, I have likely encountered many other challenges and may be able to help. Feel free to reach out to me on Twitter with any questions you may have, regardless of their size, and I will try my best to help.


Pass-the-Challenge: Defeating Windows Defender Credential Guard was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

Certipy 4.0: ESC9 & ESC10, BloodHound GUI, New Authentication and Request Methods — and more!

4 August 2022 at 18:23

Certipy 4.0: ESC9 & ESC10, BloodHound GUI, New Authentication and Request Methods — and more!

A new version of Certipy has been released along with a forked BloodHound GUI that has PKI support! In this blog post, we will look at some of the major new features of Certipy, which includes LDAPS (Schannel) and SSPI authentication, new request options and methods, and of course support for the forked BloodHound GUI that I changed to have new nodes, edges, and prebuilt queries for AD CS. At the end of the blog post, we will also look at the two new privilege escalation techniques for AD CS: ESC9 and ESC10.

BloodHound x Certipy

The BloodHound team has delivered many impressive updates, and according to their release post on version 4.1 and version 4.2, Active Directory Certificate Services (AD CS) abuse primitives are on their road map and coming soon. However, it’s been 6 months since the release of version 4.1, so I decided to implement it myself into the BloodHound GUI. This also means that if you want to use the original version of the BloodHound GUI with Certipy, you’ll have to pass the -old-bloodhound option to Certipy’s find command, as the new BloodHound data output from Certipy is only compatible with the forked GUI. The forked version is based on the latest version of BloodHound (4.2.0, August 3, 2022) and requires neo4j ≥ 4.4.0. There’s also a forked version of BloodHound 4.1.1, which doesn’t require neo4j ≥ 4.4.0.

Now, let’s see a graph.

This graph was drawn by simply selecting the CA node and then clicking on “See Enabled Templates”, as shown below.

It’s of course also possible to easily view the object controllers of the CA like you would do with any other object.

The same is possible for certificate templates. Simply select the template and click “See Certificate Authorities”.

Want to see enrollment rights or object controllers? Also one click away.

This also comes with prebuilt queries so you don’t have to mess with your custom queries.

Even though the forked BloodHound GUI was mainly focused on PKI integration, I decided to add a few features that I personally like. For instance, you can now hover your mouse over a query and click the little “Copy” button to copy the query to your clipboard.

Once you’ve copied your query, you can paste and edit it in the new multi-lined “Raw Query” text area.

These are just some small features I personally enjoy, and I might add new ones. The source code can be found at https://github.com/ly4k/BloodHound/ and prebuilt binary releases can be found here. I’ll regularly pull commits from the upstream version so you don’t miss out on those features. If you have any additions, feel free to open an issue or create a pull request.

Old Is New Again

Now, back to Certipy. I have reintroduced and improved some old features of Certipy that I previously removed related to Certipy’s find command. For text and JSON based output, Certipy will now check for ESC1, ESC2, ESC3, ESC4, and the new ESC9 on certificate templates, and ESC6, ESC7, and ESC8 on certificate authorities based on the current user’s nested group memberships. Furthermore, if ms-DS-MachineAccountQuota is not 0 (default: 10) then Certipy will act as if the current user is also a member of the Domain Computers group, since the user will most likely be able to add a new domain computer. In addition to this, the find command now accepts the -vulnerable parameter to only show vulnerable certificate templates, and -hide-admins to hide administrators from the permissions for a cleaner output. These options only apply to text and JSON based output (-text and -json) and does not affect the BloodHound data.

For those who want a bit more stealth, the find command has also received the new -dc-only option to only connect the domain controller (DC). This means that Certipy will not connect to the CA to check for permissions, Web Enrollment, and request flags. This will affect the BloodHound data, and as shown below, Certipy cannot determine permissions, Web Enrollment, and request flags for the CA when this option is set — but it does not affect certificate templates since all this information is stored on the DC.

New Authentication Methods

Scannel (LDAPS)

Our good friends at FalconForce recently published a blog post on how to detect “UnPACing” — the technique used by Certipy and Rubeus during PKINIT Kerberos authentication to retrieve the NT hash. In the Certified Pre-Owned whitepaper, the authors, Will Schroeder and Lee Christensen, mention that Active Directory supports certificate authentication over two protocols by default: Kerberos and Secure Channel (Schannel). One protocol that supports client authentication via Schannel is LDAPS (LDAP over SSL/TLS) — assuming AD CS has been setup. As such, this is exactly what I’ve implemented into Certipy.

Once you’ve obtained your shiny new certificate, run the auth command like you’d usually do, but this time, specify the -ldap-shell option to drop into an interactive LDAP shell with a limited set of commands that should be enough to aid you in the right direction, for instance configuring Resource-based Constrained Delegation, adding a user to a group, reading LAPS, and more.

This command will connect to the domain controller, and instead of authenticating via Kerberos, Certipy will connect to LDAP and present the certificate during the StartTLS upgrade. It is worth noting that the type of certificate that can be used depends on the CertificateMappingMethods registry key.

This new feature is also relevant for ESC10 (see later in the post).

Windows Integrated Authentication (SSPI)

Now, imagine you just got code execution on a domain-joined machine. You could run your C2 agent, open a SOCKS proxy connection, and then run Certipy through that. The problem in this scenario is that you don’t know the credentials of your current user context. This has happened to me a few times. Instead, let me introduce Certipy’s new SSPI integration.

The first step is to get Certipy on your target machine. You could install Python and then Certipy, or you could just use something like PyInstaller (pyinstaller ./Certipy.spec) to pack it into an executable. Once you’ve done that, you can run all your usual commands, but instead of specifying username, password, and domain, you can just pass the -sspi option. This will make Certipy use your current user’s domain context for authentication by using Windows APIs to retrieve Kerberos tickets.

The -sspi flag can be used on all commands that require user credentials, e.g. find.

Now, -sspi is not the only new flag related to Windows authentication. The auth command now also accepts -print and -kirbi to print the ticket or save the ticket as Kirbi format — both outputs ready to be used for Rubeus. On top of this, if you would just like to inject your newly acquired domain administrator ticket into your current logon session, you can do that with the -ptt flag.

The same thing can be achieved by using -print with the auth command, and then passing the Base64 ticket to Certipy’s new ptt command in the -ticket option. The Base64 ticket can also be used for Rubeus.

The new ptt command can be used to inject tickets from a file or command line, but it can also be used to request a new TGT using credentials and inject the ticket into your logon session. This is useful if you’re not in a domain context and you don’t want to write the username, password, and domain for each command.

Change of Parameters

Certipy has also received a minor change on how a username, domain, password and target are specified. The username and domain is now specified in -username user@domain, and the password is specified in -password, whereas the target host can be specified in -target if required. Otherwise, the target will be derived from the domain. This change is because of the parsing logic related to usernames, password, domains, and targets in a single string that could prevent some usernames and passwords from being parsed correctly, for instance if the sAMAccountName or password contains an @ symbol.

New Request Methods

Web Enrollment

Some users of Certipy reported that during an engagement, they could only request certificates through the web interface and not via RPC. As such, I have implemented web based enrollment into Certipy for that exact reason. Currently, it supports both HTTP and HTTPS, but only with password or NTLM authentication. To request a certificate through the web interface, simply pass the -web option to your usual req command.

Double SAN

A feature request was sent to me to allow specifying a DNS host name instead of a UPN for the old -alt parameter. As such, the -alt parameter has been removed in favor of the two new parameters -upn and -dns. And it turns out that you can even specify both parameters in a single request.

Certipy will now print out all the account identifications found in the certificate. Now, what would happen if we tried to authenticate with this certificate?

Well, Certipy will now ask you which identification you wish to use. So can we have one certificate with identification for multiple users?

Yes, we can. As shown above, two different NT hashes were returned depending on the identification used. It is of course also possible to only specify a single identification.

Key Archival and Key Size

A user reported that a template had a different minimum key size than the one that was generated by Certipy. This will yield the error CERTSRV_E_KEY_LENGTH. Certipy now accepts the -key-size parameter to specify a different key size, as shown below.

While this was rather trivial to implement, another user reported that a certificate template was configured to require key archival. This is specified as CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL in the msPKI-Private-Key-Flag of the certificate template. Key archival means that the enrollee must send its private key during the request such that it can be stored in the CA database and later recovered by a Recovery Agent. For this, Microsoft requires that the private key will be embedded in a CMC (Certificate Management over CMS) request rather than a usual PKCS10 request. After reading the RFC and the sparse Microsoft-specific documentation, Certipy can finally overcome this issue.

This is a whole different type of request and protocol, that includes retrieving the CA Exchange Certificate, crafting undocumented ASN1 structures, encrypting the private key, and a few more headaches. Nonetheless, I wouldn’t want this single flag to stand in my (or your) way to becoming domain administrator during an engagement.

Other Features

You might also encounter some other unmentioned features — which might not seem that useful — that is merely a result of my own research. For instance, it’s possible to renew a certificate using an old certificate with the -renew parameter. Since I had already implemented all the structures and functionality, I thought I’d just add it to Certipy.

New Escalations

To understand the new escalations, we must first understand Microsoft’s patch for CVE-2022–26923.

My previously reported AD privilege escalation vulnerability “Certifried” (CVE-2022–26923) actually contained four different cases. The case described in my previous blog post was that it was possible to simply duplicate the DNS host name of a machine account. This would work from a low-privileged user in a default AD CS environment. However, I also reported three other closely related vulnerabilities that required a GenericWrite permission over a low-privileged account. These were not described in the previous blog post, but here’s a brief summary of the other cases:

  • Overwrite userPrincipalName of user to be <sAMAccountName> of target to hijack user account since the missing domain part does not violate an existing UPN
  • Overwrite userPrincipalName of user to be <sAMAccountName>@<domain> of target to hijack machine account since machine accounts don’t have a UPN
  • Delete userPrincipalName of user and overwrite sAMAccountName to be <sAMAccountName> without a trailing $ to hijack a machine account

These three cases are all related to how a certificate is mapped to an account during authentication. First of all, a missing domain part of the UPN doesn’t matter. Secondly, if the KDC can’t find an account where the userPrincipalName matches the UPN in the certificate, it will try to find an account where the sAMAccountName matches the UPN in the certificate. And lastly, if the userPrincipalName property doesn’t exist for the enrollee during the certificate request, it will build the UPN in the certificate based on the sAMAccountName. On top of this, if the KDC cannot find an account where the sAMAccountName matches the UPN in the certificate, it will simply add a $ at the end and try again, as we know from CVE-2021–42287/CVE-2021–42278.

So how did Microsoft fix this? First of all, they made sure that the “Validated write to DNS host name” permission on a machine account now only accepts a value that matches the sAMAccountName property. This means that it is still possible to duplicate the DNS host name of a domain controller (or another machine account) if you have GenericWrite over a machine account, as shown below.

This was tested against a fully patched domain controller where john only had GenericWrite over johnpc$.

On top of this, Microsoft implemented the new szOID_NTDS_CA_SECURITY_EXT security extension for issued certificates, which will embed the objectSid property of the requester. Furthermore, Microsoft created the new registry key values (HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel) CertificateMappingMethods and (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc) StrongCertificateBindingEnforcement.

These security updates also broke our beloved ESC6. However, after the patch for CVE-2022–26923, Windows admins started reporting issues that certificated based authentication no longer worked in their environments due to the new security hardenings. On “/r/sysadmin” on Reddit, Windows admins shared workarounds for the issue by changing the values of the registry keys to be the de facto old values (just old values from now on). On top of this, Microsoft’s official workaround was to either manually map all the certificates to each user or to set the CertificateMappingMethods to the old value. Changing these registry keys to the old values will reintroduce ESC6 and introduce the new ESC10. So why are we interested in how a certificate is mapped to a user?

Certificate Mappings

As mentioned earlier, Active Directory supports certificate authentication over two protocols by default: Kerberos and Secure Channel (Schannel). As such, the two new registry keys StrongCertificateBindingEnforcement and CertificateMappingMethods correspond to Kerberos and Schannel, respectively.

Certificates can either be mapped via implicit or explicit mappings. For explicit mappings, the altSecurityIdentities property on an account object is configured to contain identifiers for a certificate, for instance the issuer and serial number. This way, when a certificate is used for authentication via explicit mapping, it must be signed by a trusted CA and then match the values specified in the altSecurityIdentities. On the other hand, when a certificate is used for authentication via implicit mapping, then the information from the certificate’s Subject Alternative Name (SAN) extension is used to map the certificate to an account, either the UPN or DNS field.

However, Schannel and Kerberos don’t use the same techniques for mapping a certificate implicitly. Let’s take a look at how a certificate is mapped implicitly for each protocol.

Kerberos Certificate Mapping

The new registry key value (HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc) StrongCertificateBindingEnforcement is by default set to 1 now. Before the patch, this key did not exist, but the old value was 0, i.e. strong certificate binding was not enforced. This value can either be set to 0, 1, or 2.

If the value is 0, then no strong certificate mapping checks are done. This means that the new szOID_NTDS_CA_SECURITY_EXT certificate extension isn’t used for anything even though it’s embedded in the certificate — so certificate mapping in Kerberos is exactly as before the patch. Setting this value to 0 is not recommended by Microsoft, but according to BleepingComputer, this value fixed the authentication issues for a Windows admin.

If this value is 1 (default value after patch), the KDC checks if there is a strong certificate mapping (explicit). If yes, authentication is allowed. Otherwise, the KDC will check if the certificate has the new SID extension and validate it. If this extension is not present, authentication is allowed if the user account predates the certificate.

If this value is 2, the KDC checks if there’s a strong certificate mapping. If yes, authentication is allowed. Otherwise, the KDC will check if the certificate has the new SID extension and validate it. If this extension is not present, authentication is denied.

Microsoft is planning on setting this value to 2 by default on May 9, 2023 and removing the registry key value, such that a certificate must have a strong explicit mapping or the szOID_NTDS_CA_SECURITY_EXT extension to be used for authentication via Kerberos.

So, let’s say that the value is set to 0; how is a certificate then implicitly mapped? For this blog post, we are not interested in explicit mapping (altSecurityIdentities). When a certificate is used for authentication via Kerberos, the KDC will first verify that it is issued by a trusted CA and that the certificate can be used for client authentication. For implicit mappings, the KDC will then try to map the certificate to an account either via the UPN or DNS SAN value.

If the certificate contains a UPN with the value [email protected], the KDC will first try to see if there exists a user with a userPrincipalName property value that matches. If not, it checks if the domain part corp.local matches the Active Directory domain. If there is no domain part in the UPN SAN, i.e. the UPN is just john, then no validation is performed. Next, it will try to map the user part john to an account where the sAMAccountName property matches. If this also fails, it will try to add a $ to the end of the user part, i.e. john$, and try the previous step again (sAMAccountName). This means that a certificate with a UPN value can actually be mapped to a machine account.

If the certificate contains a DNS SAN and not a UPN SAN, then the KDC will split the DNS name into a user part and a domain part, i.e. johnpc.corp.local becomes johnpc and corp.local. The domain part is then validated to match the Active Directory domain, and the user part will be appended by a $ and then mapped to an account where the sAMAccountName property matches, i.e. johnpc will be looked up as johnpc$.

This certificate mapping is explained in MS-PKCA 3.1.5.2.1 and the DNS mapping is explained a bit more in depth in my previous blog post on CVE-2022–26923.

If the new registry key value StrongCertificateBindingEnforcement is set to 1 or 2, then the szOID_NTDS_CA_SECURITY_EXT certificate extension will be used to map the certificate to an account where the objectSid property matches. If the value is 1 and the extension is not present, then the mapping is performed as if the value was set to 0.

Schannel Certificate Mapping

Schannel will map the certificate a little bit differently than the KDC would. Let’s take a look at the possible values for the CertificateMappingMethods registry key value. This value is a DWORD that supports multiple values as a bit set. The new default value is 0x18 (0x8 and 0x10), whereas the old value was 0x1f (all of the below values).

  • 0x0001 — Subject/Issuer certificate mapping (explicit)
  • 0x0002 — Issuer certificate mapping (explicit)
  • 0x0004 —SAN certificate mapping (implicit)
  • 0x0008 — S4U2Self certificate mapping (Kerberos)
  • 0x0010 — S4U2Self explicit certificate mapping (Kerberos)

So, Schannel actually doesn’t support the new szOID_NTDS_CA_SECURITY_EXT extension directly. Instead, it will use S4U2Self to map the certificate via Kerberos, which then supports the szOID_NTDS_CA_SECURITY_EXT extension. However, this is performed as the last step if the other supported mappings fail. This means that if certificate contains a UPN or DNS name, and the CertificateMappingMethods contains the 0x4 value, then the szOID_NTDS_CA_SECURITY_EXT certificate extension and StrongCertificateBindingEnforcement registry value will have absolutely no influence on the certificate mapping via Schannel. This is a bit more interesting to us, since Microsoft officially suggested setting this registry key value to the old value 0x1f (all of the above methods) as an alternative to manually mapping all certificates if the security updates caused authentication issues: “If you experience authentication failures with Schannel-based server applications, we suggest that you perform a test. Add or modify the CertificateMappingMethods registry key value on the domain controller and set it to 0x1F and see if that addresses the issue.”

Now that we understand the patch for CVE-2022–26923, let’s look at the new ESCs and some examples.

ESC9 — No Security Extension

Description

ESC9 refers to the new msPKI-Enrollment-Flag value CT_FLAG_NO_SECURITY_EXTENSION (0x80000). If this flag is set on a certificate template, the new szOID_NTDS_CA_SECURITY_EXT security extension will not be embedded. ESC9 is only useful when StrongCertificateBindingEnforcement is set to 1 (default), since a weaker certificate mapping configuration for Kerberos or Schannel can be abused as ESC10 — without ESC9 — as the requirements will be the same.

Conditions:

  • StrongCertificateBindingEnforcement not set to 2 (default: 1) or CertificateMappingMethods contains UPN flag
  • Certificate contains the CT_FLAG_NO_SECURITY_EXTENSION flag in the msPKI-Enrollment-Flag value
  • Certificate specifies any client authentication EKU

Abuse

Please see the “Examples” section for a practical example. To abuse this misconfiguration, the attacker needs GenericWrite over any account A that is allowed to enroll in the certificate template to compromise account B (target).

ESC10 — Weak Certificate Mappings

Description

ESC10 refers to two registry key values on the domain controller.

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel CertificateMappingMethods. Default value 0x18 (0x8 | 0x10), previously 0x1F.

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc StrongCertificateBindingEnforcement. Default value 1, previously 0.

Case 1

StrongCertificateBindingEnforcement set to 0

Case 2

CertificateMappingMethods contains UPN bit (0x4)

Abuse

Please see the “Examples” section for practical examples for each case. To abuse these misconfigurations, the attacker needs GenericWrite over any account A that is allowed to enroll in a certificate with client authentication to compromise account B (target).

Unfortunately, these registry keys cannot be read by a low-privileged user remotely. However, if you ever find yourself in a scenario, where you have GenericWrite over any account, it might be worth trying each abuse case nonetheless.

Examples

ESC9

Conditions:

  • StrongCertificateBindingEnforcement set to 1 (default) or 0
  • Certificate contains the CT_FLAG_NO_SECURITY_EXTENSION flag in the msPKI-Enrollment-Flag value
  • Certificate specifies any client authentication EKU

Requisites:

  • GenericWrite over any account A to compromise any account B

In this case, [email protected] has GenericWrite over [email protected], and we wish to compromise [email protected]. [email protected] is allowed to enroll in the certificate template ESC9 that specifies the CT_FLAG_NO_SECURITY_EXTENSION flag in the msPKI-Enrollment-Flag value.

First, we obtain the hash of Jane with for instance Shadow Credentials (using our GenericWrite).

Next, we change the userPrincipalName of Jane to be Administrator. Notice that we’re leaving out the @corp.local part.

This is not a constraint violation, since the Administrator user’s userPrincipalName is [email protected] and not Administrator.

Now, we request the vulnerable certificate template ESC9. We must request the certificate as Jane.

Notice that the userPrincipalName in the certificate is Administrator and that the issued certificate contains no “object SID”.

Then, we change back the userPrincipalName of Jane to be something else, like her original userPrincipalName [email protected].

Now, if we try to authenticate with the certificate, we will receive the NT hash of the [email protected] user. You will need to add -domain <domain> to your command line since there is no domain specified in the certificate.

And voilà.

ESC10(Case 1)

Conditions:

  • StrongCertificateBindingEnforcement set to 0

Requisites:

  • GenericWrite over any account A to compromise any account B

In this case, [email protected] has GenericWrite over [email protected], and we wish to compromise [email protected]. The abuse steps are almost identical to ESC9, except that any certificate template can be used.

First, we obtain the hash of Jane with for instance Shadow Credentials (using our GenericWrite).

Next, we change the userPrincipalName of Jane to be Administrator. Notice that we’re leaving out the @corp.local part.

This is not a constraint violation, since the Administrator user’s userPrincipalName is [email protected] and not Administrator.

Now, we request any certificate that permits client authentication, for instance the default User template. We must request the certificate as Jane.

Notice that the userPrincipalName in the certificate is Administrator.

Then, we change back the userPrincipalName of Jane to be something else, like her original userPrincipalName [email protected].

Now, if we try to authenticate with the certificate, we will receive the NT hash of the [email protected] user. You will need to add -domain <domain> to your command line since there is no domain specified in the certificate.

ESC10(Case 2)

Conditions:

  • CertificateMappingMethods contains UPN bit flag (0x4)

Requisites:

  • GenericWrite over any account A to compromise any account B without a userPrincipalName property (machine accounts and built-in domain administrator Administrator)

In this case, [email protected] has GenericWrite over [email protected], and we wish to compromise the domain controller [email protected].

First, we obtain the hash of Jane with for instance Shadow Credentials (using our GenericWrite).

Next, we change the userPrincipalName of Jane to be [email protected].

This is not a constraint violation, since the DC$ computer account does not have userPrincipalName.

Now, we request any certificate that permits client authentication, for instance the default User template. We must request the certificate as Jane.

Then, we change back the userPrincipalName of Jane to be something else, like her original userPrincipalName ([email protected]).

Now, since this registry key applies to Schannel, we must use the certificate for authentication via Schannel. This is where Certipy’s new -ldap-shell option comes in.

If we try to authenticate with the certificate and -ldap-shell, we will notice that we’re authenticated as u:CORP\DC$. This is a string that is sent by the server.

One of the available commands for the LDAP shell is set_rbcd which will set Resource-Based Constrained Delegation (RBCD) on the target. So we could perform a RBCD attack to compromise the domain controller.

Alternatively, we can also compromise any user account where there is no userPrincipalName set or where the userPrincipalName doesn’t match the sAMAccountName of that account. From my own testing, the default domain administrator [email protected] doesn’t have a userPrincipalName set by default, and this account should by default have more privileges in LDAP than domain controllers.

Conclusion

In this blog post, we looked at some new features of Certipy and the forked BloodHound GUI that I changed to have full PKI support. Furthermore, we looked at the new ESCs — which are not as juicy as ESC1 or ESC8 — but I have seen many environments where everyone or a specific group had GenericWrite over a single user or computer. On top of this, we might see more and more admins change these registry key values or enabling the vulnerable flag on a template — simply because it makes thing work — just like ESC6.

If you have any questions, feel free to message me on Twitter (@ly4k_). In the next blog post, we’ll look at two Windows privilege escalation vulnerabilities in the Print Spooler (CVE-2022–29104 & CVE-2022–30138). This is an interesting technique that might work for other services, and the NSA was subsequently acknowledged for reporting these vulnerabilities as well. A presto!


Certipy 4.0: ESC9 & ESC10, BloodHound GUI, New Authentication and Request Methods — and more! was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

Certifried: Active Directory Domain Privilege Escalation (CVE-2022–26923)

10 May 2022 at 19:47

In this blog post, we’ll dive into a recently patched Active Directory Domain Privilege Escalation vulnerability that I reported through ZDI to Microsoft.

In essence, the vulnerability allowed a low-privileged user to escalate privileges to domain administrator in a default Active Directory environment with the Active Directory Certificate Services (AD CS) server role installed. At Institute For Cyber Risk, we see AD CS environments on almost every engagement. It’s rare that we see large and medium-sized Active Directory environments without AD CS installed. The vulnerability was patched as part of the May 2022 Security Updates from Microsoft.

Background

In Summer 2021, Will Schroeder and Lee Christensen published their excellent whitepaper Certified Pre-Owned: Abusing Active Directory Certificate Services which took a deep dive into the security of Active Directory Certificate Services (AD CS). The whitepaper thoroughly explained various tricks for persistence, theft, and privilege escalation — but also defensive guidance and general documentation on AD CS.

When I initially read the whitepaper from Will Schroeder and Lee Christensen, I only began researching into abusing misconfigurations. It was not until December 2021 when I got inspired by Charlie Clark’s (@exploitph) blog post on CVE-2021–42287 and CVE-2021–42278 that I started to look into actual vulnerabilities related to AD CS.

Introduction to Active Directory Certificate Services

If you already feel comfortable with the basics of Active Directory Certificate Services, you can skip this section. On the other hand, if you’re still feeling a bit perplexed about public key infrastructure (PKI) and certificates after reading this section, don’t worry. For this vulnerability, you can think of a certificate as merely a prove of identification, similar to a Kerberos ticket.

If you haven’t already, I highly recommend reading the shortened version of “Certified Pre-Owned” before continuing. I’ll try to cover some details throughout this post as well, but Will Schroeder and Lee Christensen has already done a great job at explaining the essentials, so here’s a snippet from their blog post that perfectly summarizes AD CS.

AD CS is a server role that functions as Microsoft’s public key infrastructure PKI implementation. As expected, it integrates tightly with Active Directory and enables the issuing of certificates, which are X.509-formatted digitally signed electronic documents that can be used for encryption, message signing, and/or authentication.
The information included in a certificate binds an identity (the subject) to a public/private key pair. An application can then use the key pair in operations as proof of the identity of the user. Certificate Authorities (CAs) are responsible for issuing certificates.
At a high level, clients generate a public-private key pair, and the public key is placed in a certificate signing request (CSR) message along with other details such as the subject of the certificate and the certificate template name. Clients then send the CSR to the Enterprise CA server. The CA server then checks if the client is allowed to request certificates. If so, it determines if it will issue a certificate by looking up the certificate template AD object […] specified in the CSR. The CA will check if the certificate template AD object’s permissions allow the authenticating account to obtain a certificate. If so, the CA generates a certificate using the “blueprint” settings defined by the certificate template (e.g., EKUs, cryptography settings, issuance requirements, etc.) and using the other information supplied in the CSR if allowed by the certificate’s template settings. The CA signs the certificate using its private key and then returns it to the client.
That’s a lot of text. So here’s a graphic:
https://posts.specterops.io/certified-pre-owned-d95910965cd2

In essence, users can request a certificate based on a predefined certificate template. These templates specifies the settings for the final certificate, e.g. whether it can be used for client authentication, what properties must be defined, who is allowed to enroll, and so on. While AD CS can be used for many different purposes, we will only focus on the client authentication aspect of AD CS.

So, let’s just make a quick example on how certificates can be used for authentication in Active Directory. We’ll be using Certipy to request and authenticate with the certificate. I have created the domain CORP.LOCAL with AD CS installed. I have also created a default, low-privileged user named JOHN. In the example below, we request a certificate from the CA CORP-DC-CA based on the template User. We then use the issued certificate john.pfx for authentication against the KDC. When authenticating with a certificate, Certipy will try to request a Kerberos TGT and retrieve the NT hash of the account.

Requesting and authenticating with a certificate

Vulnerability

Discovery

By default, domain users can enroll in the User certificate template, and domain computers can enroll in the Machine certificate template. Both certificate templates allow for client authentication. This means that the issued certificate can be used for authentication against the KDC via the PKINIT Kerberos extension.

So why does AD CS have different templates for users and computers, one might ask? In short, user accounts have a User Principal Name (UPN), whereas computer accounts do not. When we request a certificate based on the User template, the UPN of the user account will be embedded in to the certificate for identification. When we use the certificate for authentication, the KDC tries to map the UPN from the certificate to a user. If we look at the msPKI-Certificate-Name-Flag property of the User template, we can also see that SubjectAltRequireUpn (CT_FLAG_SUBJECT_ALT_REQUIRE_UPN) is specified.

“User” certificate template

As per MS-ADTS (3.1.1.5.1.3 Uniqueness Constraints), the UPN must be unique, which means we cannot have two users with the same UPN. For instance, if we try to change the UPN of Jane to [email protected], we will get a constraint violation, since the UPN [email protected] is already used by John.

Constraint violation when trying to change UPN of “Jane” to “[email protected]

As mentioned previously, computer accounts do not have a UPN. So what do computer accounts then use for authentication with a certificate? If we look at the Machine certificate template, we see that SubjectAltRequireDns (CT_FLAG_SUBJECT_ALT_REQUIRE_DNS) is specified instead.

“Machine” certificate template

So let’s try to create a new machine account, request a certificate, and then authenticate with the certificate.

Testing the “Machine” certificate template

As we can see above, the certificate is issued with the DNS host name JOHNPC.corp.local, and if we look at the computer account JOHNPC$, we can notice that this value is defined in the dNSHostName property.

If we look at the permissions of the JOHNPC object, we can see that John (the creator of the machine account) has the “Validated write to DNS host name” permission.

The “Validated write to DNS host name” permission is explained here, and described as “Validated write permission to enable setting of a DNS host name attribute that is compliant with the computer name and domain name.” So what does “compliant with the computer name and domain name” mean?

If we (as John) try to update the DNS host name property of JOHNPC to TEST.corp.local, we encounter no issues or constraint violations, and the SAM Account Name of JOHNPC is still JOHNPC$.

So let’s try to request a certificate now.

We notice that the certificate is now issued with the DNS host name TEST.corp.local. So now we are fairly certain that the DNS host name in the issued certificate is derived from the dNSHostName property, and John (as the creator of the machine account) has the “Validated write to DNS host name” permission.

Vulnerability

If we read the MS-ADTS (3.1.1.5.1.3 Uniqueness Constraints) documentation, nowhere does it mention that the dNSHostName property of a computer account must be unique.

If we look at the domain controller’s (DC$) dNSHostName property, we find that the value is DC.CORP.LOCAL.

So without further ado, let’s try to change the dNSHostName property of JOHNPC to DC.CORP.LOCAL.

This time, we get an error message saying “An operations error occurred”. This is different than when we tried to change the UPN to another user’s UPN, where we got a constraint violation. So what really happened?

Well, if we looked carefully when we changed the dNSHostName property value of JOHNPC from JOHNPC.corp.local to TEST.corp.local, we might have noticed that the servicePrincipalName property value of JOHNPC was updated to reflect the new dNSHostName value.

And according to MS-ADTS (3.1.1.5.1.3 Uniqueness Constraints), the servicePrincipalName property is checked for uniqueness. So when we tried to update the dNSHostName property of JOHNPC to DC.corp.local, the domain controller tried to update the servicePrincipalName property, which would be updated to include RestrictedKrbHost/DC.corp.local and HOST/DC.corp.local, which would then conflict with the domain controller’s servicePrincipalName property.

So by updating the dNSHostName property of JOHNPC, we indirectly caused a constraint violation when the domain controller also tried to update the servicePrincipalName of JOHNPC.

If we take a look at the permissions of JOHNPC, we can also see that John (as the creator of the machine account) has the “Validated write to service principal name” permission.

The “Validated write to service principal name” permission is explained here, and described as “Validated write permission to enable setting of the SPN attribute which is compliant to the DNS host name of the computer.” So if we want to update the servicePrincipalName of JOHNPC, the updated values must also be compliant with the dNSHostName property.

Again, what does “compliant” mean here? We notice that only two values are updated and checked when we update the dNSHostName, namely RestrictedKrbHost/TEST.corp.local and HOST/TEST.corp.local, which contains the dNSHostName property value. The other two values RestrictedKrbHost/JOHNPC and HOST/JOHNPC contains the sAMAccountName property value (without the trailing $).

So only the servicePrincipalName property values that contain the dNSHostName value must be compliant with dNSHostName property. But can we then just delete the servicePrincipalName values that contain the dNSHostName?

Yes we can. So if we now try to update the dNSHostName property value of JOHNPC to DC.corp.local, the domain controller will not have to update the servicePrincipalName, since none of the values contain the dNSHostName property value.

Let’s try to update the dNSHostName property value of JOHNPC to DC.corp.local.

Success! We can see that the dNSHostName property was updated to DC.corp.local, and the servicePrincipalName was not affected by the change, which means we didn’t cause any constraint violations.

So now JOHNPC has the same dNSHostName as the domain controller DC$.

Now, let’s try to request a certificate for JOHNPC using the Machine template, which should embed the dNSHostName property as identification.

Another success! We got a certificate with the DNS host name DC.corp.local. Let’s try to authenticate using the certificate.

Authentication was also successful, and Certipy retrieved the NT hash for dc$. As a Proof-of-Concept, we can use the NT hash to perform a DCSync attack to dump the hashes of all the users.

You might have wondered, why we didn’t have to change the DNS host name of JOHNPC to something else before authenticating with the certificate. How did the KDC know what account to map the certificate to?

PKINIT & Certificate Mapping

If you don’t care about the technical details on how certificates are mapped to accounts during authentication, you can skip this section.

Public Key Cryptography for Initial Authentication (PKINIT) is an extension for the Kerberos protocol. The PKINIT extension enables the use of public key cryptography in the initial authentication exchange of the Kerberos protocol. In other words, PKINIT is the Kerberos extension that allows the use of certificates for authentication. In order to use a certificate for Kerberos authentication, the certificate must be configured with the “Client Authentication” Extended Key Usage (EKU), and some sort of identification of the account. The Windows implementation of the PKINIT protocol extension for Kerberos is described in MS-PKCA. The documentation specifies, among other things, how the KDC maps a certificate to an account during authentication. The certificate mapping is explained in MS-PKCA 3.1.5.2.1.

First, the account is looked up based on the principal name specified in the AS-REQ, e.g. [email protected]. Then, depending on the userAccountControl property of the account, the KDC validates the certificate mapping based on either the Subject Alternative Name (SAN) DNSName or UPNName in the certificate. If the WORKSTATION_TRUST_ACCOUNT (domain computer) or SERVER_TRUST_ACCOUNT (domain controller) bit is set, the KDC validates the mapping from the DNSName. Otherwise, the KDC validates the mapping from the UPNName. For this blog post, we’re only interested in the DNSName mapping. The mapping of the DNSName field is described in MS-PKCA 3.1.5.2.1.1.

The documentation states that the KDC must confirm that the sAMAccountName of the account looked up matches the computer name in the DNSName field of the certificate terminated with $ and that the DNS domain name in the DNSName field of the certificate matches the DNS domain name of the realm. As an example, suppose we have the computer account JOHNPC$ in the domain corp.local. For a valid mapping, the DNSName of the certificate must therefore be JOHNPC.corp.local, i.e. <computername>.<domain>, where <computername> is the sAMAccountName without the trailing $.

So during PKINIT Kerberos authentication, we supply a principal name (e.g. [email protected]) and a certificate with a DNSName set to johnpc.corp.local. The KDC then looks up the account from the principal name. Since johnpc$ is a computer account, the KDC then splits the DNSName field into a computer name and realm part. The KDC then validates that the computer name part matches the sAMAccountName terminated with $ and that the realm part matches the domain. If both parts match, the validation is a success, and the mapping is thus valid. It is worth noting that the dNSHostName property of the account is not used for the certificate mapping. The dNSHostName property is only used when the certificate is requested.

Patch

UPDATED MAY 11

The vulnerability was patched as part of the May 2022 Security Updates from Microsoft by introducing a new Object ID (OID) in new certificates to further fingerprint the user. This is done by embedding the user’s objectSid (SID) within the new szOID_NTDS_CA_SECURITY_EXT (1.3.6.1.4.1.311.25.2) OID. Certificate Templates with the new CT_FLAG_NO_SECURITY_EXTENSION (0x80000) flag set in the msPKI-Enrollment-Flag attribute will not embed the new szOID_NTDS_CA_SECURITY_EXT OID, and therefore, these templates are still vulnerable to this attack. It is unlikely that this flag is set, but you should be aware of the implications of turning this flag on. Furthermore, the “Validated write to DNS host name” permission now only allows setting a dNSHostName attribute that matches the SAM Account Name of the account. However, with a generic write permission over the computer account, it’s still possible to create a duplicate dNSHostName value.

An attempt to exploit the vulnerability against a patched domain controller will return KDC_ERR_CERTIFICATE_MISMATCH during Kerberos authentication, if the certificate has the szOID_NTDS_CA_SECURITY_EXT OID. I also tried to perform the authentication using Schannel against LDAPS to check whether the vulnerability was only patched in the Kerberos implementation. Fortunately, it seems that this method can’t bypass the security update. There might be some other interesting cases, since the dNSHostName property can still be duplicated and embedded in the certificate. To check if a CA is vulnerable, we can simply request a certificate and check whether the user’s SID is embedded within the certificate. It is worth noting that both the KDC and CA server must be patched in order to fully mitigate the vulnerability.

This patch also brings an end to the ESC6 attack described in Will Schroeder and Lee Christensen’s whitepaper; but the ESC1 attack will still work, since the new OID isn’t embedded in certificates based on certificate templates with the ENROLLEE_SUPPLIES_SUBJECT flag specified.

Certipy

Along with release of this blog post, Certipy has received some new updates that includes functionality to easily create a new machine account with the DNS host name dc.corp.local and then request a certificate.

Mitigations

A patch has officially been released by Microsoft. If you’re unable to install the patch, there are a few other measures you can take to mitigate the vulnerability. First of all, you can harden your AD CS environment by restricting certificate enrollment. While not directly a mitigation, you can also change the MS-DS-Machine-Account-Quota attribute to 0, which is the value that determines the number of computer accounts that a user is allowed to create in a domain. By default, this value is set to 10. This does not mitigate the vulnerability, since an attacker might compromise a machine account by compromising a workstation, for instance with KrbRelay.

Disclosure Timeline

  • Dec 14, 2021: Vulnerability reported to Zero Day Initiative
  • Dec 17, 2021: Case assigned
  • Dec 31, 2021: Case investigated
  • Jan 11, 2022: Case contracted
  • Jan 20, 2022: Case reviewed
  • Jan 21, 2022: Vendor disclosure, tracked as ZDI-CAN-16168
  • May 10, 2022: Patch released by Microsoft

Certifried: Active Directory Domain Privilege Escalation (CVE-2022–26923) was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

Certipy 2.0: BloodHound, New Escalations, Shadow Credentials, Golden Certificates, and more!

19 February 2022 at 13:09

As the title states, the latest release of Certipy contains many new features, techniques and improvements. This blog post dives into the technical details of many of them.

Public Key Infrastructure can be difficult to set up. Users and administrators are often not fully aware of the implications of ticking a single checkbox — especially when that single checkbox is what (finally) made things work.

It’s been almost five months since the first release of Certipy. Back then, Certipy was just a small tool for abusing and enumerating Active Directory Certificate Services (AD CS) misconfigurations. Since then, our offensive team at Institut For Cyber Risk have come across many different AD CS environments during our engagements, which required new additions and features.

These have been implemented and Certipy 2.0 is now ready for public release.

BloodHound integration

One of the major new features of Certipy is BloodHound integration. The old version had a simple feature to find vulnerable certificate templates based on the current user’s group memberships. But as we’ll see in a bit, integrating with BloodHound is just better.

By default, the new version of Certipy will output the enumeration results in both BloodHound data as well as text- and JSON-format. I’ve often found myself running the tool multiple times because I wanted to view the output in a different format.

It is however possible to output only BloodHound data, JSON, or text by specifying one or more of the switches -bloodhound, -json, or -text, respectively.

The BloodHound data is saved as a ZIP-file that can be imported into the latest version of BloodHound (4.1.0 at the time of writing). Please note that Certipy uses BloodHound’s new format, introduced in version 4.

New edges and nodes means new queries. I have created the most important queries that Certipy supports. The queries can be found in the repository and imported into your own BloodHound setup.

Certipy’s BloodHound queries

The new version of Certipy can abuse all of the scenarios listed in the “Domain Escalation” category. Suppose we have taken over the domain user account JOHN. Let’s start with one of the first queries, “Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)”.

Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)

This is a fairly simple path. But as we go through the escalation queries, we might see why BloodHound is just better, as attack paths can be built, using the imported Certipy data.

Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)

New Escalation Techniques

The whitepaper “Certified Pre-Owned” lists 8 domain escalation techniques for misconfigurations in AD CS (ESC1-ESC8). Previously, only ESC1 and ESC6 were supported by Certipy, and ESC8 was supported by Impacket’s ntlmrelayx. While ESC1 and ESC8 are the vulnerabilities we’ve seen the most, we’ve also come across other misconfigurations, which is why I have implemented all of them, except for ESC5 which is too abstract.

As such, Certipy now supports abusing all of the escalation techniques listed in the queries.

ESC1 — Misconfigured Certificate Templates

Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)

The most common misconfiguration we’ve seen during our engagements is ESC1. In essence, ESC1 is when a certificate template permits Client Authentication and allows the enrollee to supply an arbitrary Subject Alternative Name (SAN).

The previous release of Certipy had support for this technique as well, but the new version comes with improvements, and therefore, I will demonstrate all of the escalation techniques that the new version of Certipy supports.

For ESC1, we can just request a certificate based on the vulnerable certificate template and specify an arbitrary SAN with the -alt parameter.

A new feature of Certipy is that certificates and private keys are now stored in PKCS#12 format. This allows us to pack the certificate and private key together in a standardized format.

Another neat feature is that the auth command will try to retrieve the domain and username from the certificate for authentication.

In most cases, this will not work, usually because the domain name cannot be resolved. To work around this, all the necessary parameters can be specified on the command line if needed.

ESC2 — Misconfigured Certificate Templates

Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC2)

ESC2 is when a certificate template can be used for any purpose. The whitepaper “Certified Pre-Owned” does not mention any specific domain escalation technique that works out of the box for ESC2. But since the certificate can be used for any purpose, it can be used for the same technique as with ESC3, which we’ll see below.

ESC3 — Misconfigured Enrollment Agent Templates

Shortest Paths to Enrollment Agent Templates from Owned Principals (ESC3)

ESC3 is when a certificate template specifies the Certificate Request Agent EKU (Enrollment Agent). This EKU can be used to request certificates on behalf of other users. As we can see in the path above, the ESC2 certificate template is vulnerable to ESC3 as well, since the ESC2 template can be used for any purpose.

First, let’s request a certificate based on ESC3.

With our new Certificate Request Agent certificate we can request certificates on behalf of other users by specifying the -on-behalf-of parameter along with our Certificate Request Agent certificate. The -on-behalf-of parameter value must be in the form of domain\user, and not the FQDN of the domain, i.e. corp rather than corp.local.

For good measure, here’s the same attack with the ESC2 certificate template.

ESC4 — Vulnerable Certificate Template Access Control

Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)

ESC4 is when a user has write privileges over a certificate template. This can for instance be abused to overwrite the configuration of the certificate template to make the template vulnerable to ESC1.

As we can see in the path above, only JOHNPC has these privileges, but our user JOHN has the new AddKeyCredentialLink edge to JOHNPC. Since this technique is related to certificates, I have implemented this attack as well, which is known as Shadow Credentials. Here’s a little sneak peak of Certipy’s shadow auto command to retrieve the NT hash of the victim.

We’ll go into more details about the technique later in this post, but for now, we just want the NT hash of JOHNPC.

The new version of Certipy can overwrite the configuration of a certificate template with a single command. By default, Certipy will overwrite the configuration to make it vulnerable to ESC1. We can also specify the -save-old parameter to save the old configuration, which will be useful for restoring the configuration after our attack. Be sure to do this, if using Certipy outside a test environment.

As we can see below, the new configuration will allow Authenticated Users full control over the certificate template. Moreover, the new template can be used for any purpose, and the enrollee supplies the SAN, meaning it’s vulnerable to ESC1.

When we’ve overwritten the configuration, we can simply request a certificate based on the ESC4 template as we would do with ESC1.

If we want to restore the configuration afterwards, we can just specify the path to the saved configuration with the -configuration parameter. You can also use this parameter to set custom configurations.

ESC5 — Vulnerable PKI Object Access Control

ESC5 is when objects outside of certificate templates and the certificate authority itself can have a security impact on the entire AD CS system, for instance the CA server’s AD computer object or the CA server’s RPC/DCOM server. This escalation technique has not been implemented in Certipy, because it’s too abstract. However, if the CA server is compromised, you can perform the ESC7 escalation.

ESC6 — EDITF_ATTRIBUTESUBJECTALTNAME2

Find Certificate Authorities with User Specified SAN (ESC6)

ESC6 is when the CA specifies the EDITF_ATTRIBUTESUBJECTALTNAME2 flag. In essence, this flag allows the enrollee to specify an arbitrary SAN on all certificates despite a certificate template’s configuration. In Certipy, this can be seen in the property “User Specified SAN” of the CA. If this property is not shown, it means that Certipy couldn’t get the security and configuration of the CA.

The attack is the same as ESC1, except that we can choose any certificate template that permits client authentication.

ESC7 — Vulnerable Certificate Authority Access Control

Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)

ESC7 is when a user has the Manage CA or Manage Certificates access right on a CA. While there are no public techniques that can abuse only the Manage Certificates access right for domain privilege escalation, we can still use it to issue or deny pending certificate requests.

We’ve seen this misconfiguration on one of our engagements. The whitepaper mentions that this access right can be used to enable the EDITF_ATTRIBUTESUBJECTALTNAME2 flag to perform the ESC6 attack, but this will not have any effect until the CA service (CertSvc) is restarted. When a user has the Manage CA access right, the user is allowed to restart the service. However, it does not mean that the user can restart the service remotely and we were also not allowed to restart this service on our engagement.

In the following, I will explain a new technique I found that doesn’t require any service restarts or configuration changes.

In order for this technique to work, the user must also have the Manage Certificates access right, and the certificate template SubCA must be enabled. Fortunately, with our Manage CA access right, we can fulfill these prerequisites if needed.

If we don’t have the Manage Certificates access right, we can just add ourselves as a new “officer”. An officer is just the term for a user with the Manage Certificates access right, as per MS-CSRA 3.1.1.7.

After running a new Certipy enumeration with the find command, and importing the output into BloodHound, we should now see that JOHN has the Manage Certificates and Manage CA access right.

Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)

The next requirement is that the default certificate template SubCA is enabled. We can list the enabled certificate templates on a CA with the -list-templates parameter, and in this case, the SubCA template is not enabled on our CA.

With our Manage CA access right, we can just enable the SubCA certificate template with the -enable-template parameter.

The SubCA certificate template is now enabled, and we’re ready to proceed with the new attack.

The SubCA certificate template is enabled by default, and it’s a built-in certificate template, which means that it cannot be deleted in the Certificate Templates Console (MMC).

The SubCA certificate template is interesting because it is vulnerable to ESC1 (Enrollee Supplies Subject and Client Authentication).

However, only DOMAIN ADMINS and ENTERPRISE ADMINS are allowed to enroll in the SubCA certificate template.

Show Enrollment Rights for Certificate Template

But if a user has the Manage CA access right and the Manage Certificates access right, the user can effectively issue failed certificate requests.

Let’s try to request a certificate based on the SubCA certificate template, where we specify the SAN [email protected].

We get a CERTSRV_E_TEMPLATE_DENIED error, meaning that we are not allowed to enroll in the template. Certipy will ask if we still want to save the private key for our request, and in this case, we answer “y” (for yes). Certipy also prints out the request ID, which we’ll use for issuing the certificate.

With our Manage CA and Manage Certificates access right, we can issue the failed certificate request with the ca command by specifying the request ID in -issue-request parameter.

When we’ve issued the certificate, we can now retrieve it with the req command by specifying the -retrieve parameter with our request ID.

It is now possible to use the certificate to obtain the password hash of the administrator account.

ESC8 — NTLM Relay to AD CS HTTP Endpoints

Find Certificate Authorities with HTTP Web Enrollment (ESC8)

ESC8 is when an Enrollment Service has installed and enabled HTTP Web Enrollment. This is attack is already implemented in Impacket’s ntlmrelayx, but I thought there was room for improvements and new features.

In Certipy, this vulnerability can be seen in the property “Web Enrollment” of the CA.

To start the relay server, we just run the relay command and specify the CA’s IP.

By default, Certipy will request a certificate based on the Machine or User template depending on whether the relayed account name ends with $. It is possible to specify another template with the -template parameter.

We can then use a technique such as PetitPotam to coerce authentication from a computer. In this example, I simply made a dir \\IP\foo\bar from an administrator command prompt.

Certipy will relay the NTLM authentication to the Web Enrollment interface of the CA and request a certificate for the user.

Now, let’s consider a scenario where all certificate requests must be approved by an officer, and we have the Manage Certificates access right.

In this scenario, Certipy will ask if we want to save the private key for our request. In this case, we answer yes.

With our Manage Certificates access right, we can issue the request based on the request ID.

We then start the relaying server again, but this time, we specify -retrieve with our request ID.

Certipy will retrieve the certificate based on the request ID rather than requesting a new one. This is an edge case, but I thought it was interesting to implement nonetheless.

Shadow Credentials

One of the new edges in BloodHound 4.1.0 is AddKeyCredentialLink.

Path from JOHN to JOHNPC

The attack has been dubbed Shadow Credentials, and it can be used for account takeover. As mentioned earlier, this technique is related to certificates, and therefore, I have implemented the attack in Certipy. We can also abuse this technique when we have a “Generic Write” privilege over another user and we don’t want to reset their password.

Auto

The command you’ll likely use the most is auto, which we saw earlier. In essence, Certipy will create and add a new Key Credential, authenticate with the certificate, and then restore the old Key Credential attribute of the account. This is useful if you just want the NT hash of the victim account, like in the previous ESC4 scenario.

Add

If you want to add a Key Credential manually for persistence, you can use the add action. This will add a new Key Credential and save the certificate and private key, which can be used later with Certipy’s auth command.

List

It is also possible to list the current Key Credentials of an account. This is useful for deleting or getting detailed information about a specific Key Credential.

Info

Information about a Key Credential can be retrieved with the info action, where the Key Credential can be specified with the -device-id.

Remove

To remove a Key Credential, you can use the remove action and specify the Key Credential in the -device-id parameter.

Clear

Alternatively, you can just clear all of the Key Credentials for the account if desired.

Golden Certificates

Another new major feature of Certipy is the ability to create “Golden Certificates”. This is a technique for domain persistence after compromising the CA server or domain. It’s an alternative to “Golden Tickets”, but instead of forging tickets, you can forge certificates that can be used for Kerberos authentication.

Backup

The first step is to get the certificate and private key of the CA. Suppose we have compromised the domain administrator administrator and we want to retrieve the certificate and private key of the CA. This can be done in the Certification Authority Console on the CA server, but I have implemented this in Certipy as well.

In essence, Certipy will create a new service on the CA server which will backup the certificate and private key to a predefined location. The certificate and private key will then be retrieved via SMB, and finally the service and files are removed from the server.

Forging

With the CA certificate and private key, we can use Certipy’s new forge command to create certificates for arbitrary users. In this case, we create a certificate for JOHN. The subject doesn’t matter in this case, just the SAN.

We can then use the certificate to authenticate as JOHN.

It also works for the domain controller DC$.

It does not, however, work for disabled accounts, such as the krbtgt account.

Other Improvements

The latest release of Certipy also contains a few minor improvements and adjustments.

Certificate Authority Management

We saw earlier how we could enable a certificate template. If we want to cleanup after ourselves, we can disable the template, as shown below.

We can also remove JOHN as an officer, but we’ll still have our Manage CA access rights.

As we can see below, if we try to perform the ESC7 attack now, we get an “access denied” when trying to issue the certificate.

JOHN can also add another manager, for instance JANE.

JANE can then add a third manager, EVE.

JANE can even remove herself as a manager.

But then she won’t be able to remove EVE as a manager.

Dynamic Endpoints

This is a minor, but important new feature. During one of our engagements, SMB was firewalled off on the CA server. The previous release of Certipy requested certificates via the named pipe cert, and when SMB was firewalled off, then the old version was not able to request certificates.

However, the CA service actually listens on a dynamic TCP endpoint as well. The new version of Certipy will try to connect to the dynamic TCP endpoint, if SMB fails.

Alternatively, you can specify the -dynamic-endpoint parameter to prefer the dynamic TCP endpoint over SMB. This is useful if you don’t want to wait for the timeout for each request when SMB is firewalled off.

For slower connections, such as through a proxy, it’s also possible to specify a longer timeout than the default 5 seconds for almost all types of commands with the -timeout parameter.

The new version of Certipy has many more improvements and parameters than I’ve shown in this blog post. I recommend that you explore the new features and parameters of Certipy if you find yourself in an unusual situation. If you have any issues or feature requests, I encourage you to submit them on Github. I hope you will enjoy the new version of Certipy.

The new version can be found here: https://github.com/ly4k/Certipy


Certipy 2.0: BloodHound, New Escalations, Shadow Credentials, Golden Certificates, and more! was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

SpoolFool: Windows Print Spooler Privilege Escalation (CVE-2022-21999)

8 February 2022 at 20:21

UPDATE (Feb 9, 2022): Microsoft initially patched this vulnerability without giving me any information or acknowledgement, and as such, at the time of patch release, I thought that the vulnerability was identified as CVE-2022–22718, since it was the only Print Spooler vulnerability in the release without any acknowledgement. I contacted Microsoft for clarification, and the day after the patch release, Institut For Cyber Risk and I was acknowledged for CVE-2022–21999.

In this blog post, we’ll look at a Windows Print Spooler local privilege escalation vulnerability that I found and reported in November 2021. The vulnerability got patched as part of Microsoft’s Patch Tuesday in February 2022. We’ll take a quick tour of the components and inner workings of the Print Spooler, and then we’ll dive into the vulnerability with root cause, code snippets, images and much more. At the end of the blog post, you can also find a link to a functional exploit.

Background

Back in May 2020, Microsoft patched a Windows Print Spooler privilege escalation vulnerability. The vulnerability was assigned CVE-2020–1048, and Microsoft acknowledged Peleg Hadar and Tomer Bar of SafeBreach Labs for reporting the security issue. On the same day of the patch release, Yarden Shafir and Alex Ionescu published a technical write-up of the vulnerability. In essence, a user could write to an arbitrary file by creating a printer port that pointed to a file on disk. After the vulnerability (CVE-2020–1048) had been patched, the Print Spooler would now check if the user had permissions to create or write to the file before adding the port. A week after the release of the patch and blog post, Paolo Stagno (aka VoidSec) privately disclosed a bypass for CVE-2020–1048 to Microsoft. The bypass was patched three months later in August 2020, and Microsoft acknowledged eight independent entities for reporting the vulnerability, which was identified as CVE-2020–1337. The bypass for the vulnerability used a directory junction (symbolic link) to circumvent the security check. Suppose a user created the directory C:\MyFolder\ and configured a printer port to point to the file C:\MyFolder\Port. This operation would be granted, since the user is indeed allowed to create C:\MyFolder\Port. Now, what would happen if the user then turned C:\MyFolder\ into a directory junction that pointed to C:\Windows\System32\ after the port had been created? Well, the Spooler would simply write to the file C:\Windows\System32\Port.

These two vulnerabilities, CVE-2020–1048 and CVE-2020–1337, were patched in May and August 2020, respectively. In September 2020, Microsoft patched a different vulnerability in the Print Spooler. In short, this vulnerability allowed users to create arbitrary and writable directories by configuring the SpoolDirectory attribute on a printer. What was the patch? Almost the same story: After the patch, the Print Spooler would now check if the user had permissions to create the directory before setting the SpoolDirectory property on a printer. Perhaps you can already see where this post is going. Let’s start at the beginning.

Introduction to Spooler Components

The Windows Print Spooler is a built-in component on all Windows workstations and servers, and it is the primary component of the printing interface. The Print Spooler is an executable file that manages the printing process. Management of printing involves retrieving the location of the correct printer driver, loading that driver, spooling high-level function calls into a print job, scheduling the print job for printing, and so on. The Spooler is loaded at system startup and continues to run until the operating system is shut down. The primary components of the Print Spooler are illustrated in the following diagram.

https://docs.microsoft.com/en-us/windows-hardware/drivers/print/introduction-to-spooler-components

Application

The print application creates a print job by calling Graphics Device Interface (GDI) functions or directly into winspool.drv.

GDI

The Graphics Device Interface (GDI) includes both user-mode and kernel-mode components for graphics support.

winspool.drv

winspool.drv is the client interface into the Spooler. It exports the functions that make up the Spooler’s Win32 API, and provides RPC stubs for accessing the server.

spoolsv.exe

spoolsv.exe is the Spooler’s API server. It is implemented as a service that is started when the operating system is started. This module exports an RPC interface to the server side of the Spooler’s Win32 API. Clients of spoolsv.exe include winspool.drv (locally) and win32spl.dll (remotely). The module implements some API functions, but most function calls are passed to a print provider by means of the router (spoolss.dll).

Router

The router, spoolss.dll, determines which print provider to call, based on a printer name or handle supplied with each function call, and passes the function call to the correct provider.

Print Provider

The print provider that supports the specified print device.

Local Print Provider

The local print provider provides job control and printer management capabilities for all printers that are accessed through the local print provider’s port monitors.

The following diagram provides a view of control flow among the local printer provider’s components, when an application creates a print job.

https://docs.microsoft.com/en-us/windows-hardware/drivers/print/introduction-to-print-providers

While this control flow is rather large, we will mostly focus on the local print provider localspl.dll.

Vulnerability

The vulnerability consists of two bypasses for CVE-2020–1030. I highly recommend reading Victor Mata’s blog post on CVE-2020–1030, but I’ll try to cover the important parts as well.

When a user prints a document, a print job is spooled to a predefined location referred to as the “spool directory”. The spool directory is configurable on each printer and it must allow the FILE_ADD_FILE permission to all users.

Permissions of the default spool directory

Individual spool directories are supported by defining the SpoolDirectory value in a printer’s registry key HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Printers\<printer>.

The Print Spooler provides APIs for managing configuration data such as EnumPrinterData, GetPrinterData, SetPrinterData, and DeletePrinterData. Underneath, these functions perform registry operations relative to the printer’s key.

We can modify a printer’s configuration with SetPrinterDataEx. This function requires a printer to be opened with the PRINTER_ACCESS_ADMINISTER access right. If the current user doesn’t have permission to open an existing printer with the PRINTER_ACCESS_ADMINISTER access right, there are two options:

  • The user can create a new local printer
  • The user can add a remote printer

By default, users in the INTERACTIVE group have the “Manage Server” permission and can therefore create new local printers, as shown below.

Security of the local print server on Windows desktops

However, it seems that this permission is only granted on Windows desktops, such as Windows 10 and Windows 11. During my testing on Windows servers, this permission was not present. Nonetheless, it was still possible for users without the “Manage Server” permission to add remote printers.

If a user adds a remote printer, the printer will inherit the security properties of the shared printer from the printer server. As such, if the remote printer server allows Everyone to manage the printer, then it’s possible to obtain a handle to the printer with the PRINTER_ACCESS_ADMINISTER access right, and SetPrinterDataEx would update the local registry as usual. A user could therefore create a shared printer on a different server or workstation, and grant Everyone the right to manage the printer. On the victim server, the user could then add the remote printer, which would now be manageable by Everyone. While I haven’t fully tested how this vulnerability behaves on remote printers, it could be a viable option for situations, where the user cannot create or mange a local printer. But please note that while some operations are handled by the local print provider, others are handled by the remote print provider.

When we have opened or created a printer with the PRINTER_ACCESS_ADMINISTER access right, we can configure the SpoolDirectory on it.

When calling SetPrinterDataEx, an RPC request will be sent to the local Print Spooler spoolsv.exe, which will route the request to the local print provider’s implementation in localspl.dll!SplSetPrinterDataEx. The control flow consists of the following events:

  • 1. spoolsv.exe!SetPrinterDataEx routes to SplSetPrinterDataEx in the local print provider localspl.dll
  • 2. localspl.dll!SplSetPrinterDataEx validates permissions before restoring SYSTEM context and modifying the registry via localspl.dll!SplRegSetValue

In the case of setting the SpoolDirectory value, localspl.dll!SplSetPrinterDataEx will verify that the provided directory is valid before updating the registry key. This check was not present before CVE-2020–1030.

localspl.dll!SplSetPrinterDataEx

Given a path to a directory, localspl.dll!IsValidSpoolDirectory will call localspl.dll!AdjustFileName to convert the path into a canonical path. For instance, the canonical path for C:\spooldir\ would be \\?\C:\spooldir\, and if C:\spooldir\ is a symbolic link to C:\Windows\System32\, the canonical path would be \\?\C:\Windows\System32\. Then, localspl.dll!IsValidSpoolDirectory will check if the current user is allowed to open or create the directory with GENERIC_WRITE access right. If the directory was successfully created or opened, the function will make a final check that the number of links to the directory is not greater than 1, as returned by GetFileInformationByHandle.

localspl.dll!IsValidSpoolDirectory

So in order to set the SpoolDirectory, the user must be able to create or open the directory with writable permissions. If the validation succeeds, the print provider will update the printer’s SpoolDirectory registry key. However, the spool directory will not be created by the Print Spooler until it has been reinitialized. This means that we will need to figure out how to restart the Spooler service (we will come back to this part), but it also means that the user only needs to be able to create the directory during the validation when setting the SpoolDirectory registry key — and not when the directory is actually created.

In order to bypass the validation, we can use reparse points (directory junctions in this case). Suppose we create a directory named C:\spooldir\, and we set the SpoolDirectory to C:\spooldir\printers\. The Spooler will check that the user can create the directory printers inside of C:\spooldir\. The validation passes, and the SpoolDirectory gets set to C:\spooldir\printers\. After we have configured the SpoolDirectory, we can convert C:\spooldir\ into a reparse point that points to C:\Windows\System32\. When the Spooler initializes, the directory C:\Windows\System32\printers\ will be created with writable permissions for everyone. If the directory already exists, the Spooler will not set writable permissions on the folder.

As such, we need to find an interesting place to create a directory. One such place is C:\Windows\System32\spool\drivers\x64\, also known as the printer driver directory (on other architectures, it’s not x64). The printer driver directory is particularly interesting, because if we call SetPrinterDataEx with the CopyFiles registry key, the Spooler will automatically load the DLL assigned in the Module value — if the Module file path is allowed.

This event is triggered when pszKeyName begins with the CopyFiles\ string. It initiates a sequence of functions leading to LoadLibrary.

localspl.dll!SplSetPrinterDataEx

The control flow consists of the following events:

  • 1. spoolsv.exe!SetPrinterDataEx routes to SplSetPrinterDataEx in the local print provider localspl.dll
  • 2. localspl.dll!SplSetPrinterDataEx validates permissions before restoring SYSTEM context and modifying the registry via localspl.dll!SplRegSetValue
  • 3. localspl.dll!SplCopyFileEvent is called if pszKeyName argument begins with CopyFiles\ string
  • 4. localspl.dll!SplCopyFileEvent reads the Module value from printer’s CopyFiles registry key and passes the string to localspl.dll!SplLoadLibraryTheCopyFileModule
  • 5. localspl.dll!SplLoadLibraryTheCopyFileModule performs validation on the Module file path
  • 6. If validation passes, localspl.dll!SplLoadLibraryTheCopyFileModule attempts to load the module with LoadLibrary

The validation steps consist of localspl.dll!MakeCanonicalPath and localspl.dll!IsModuleFilePathAllowed. The function localspl.dll!MakeCanonicalPath will take a path and convert it into a canonical path, as described earlier.

localspl.dll!MakeCanonicalPath

localspl.dll!IsModuleFilePathAllowed will validate that the canonical path either resides directly inside of C:\Windows\System32\ or within the printer driver directory. For instance, C:\Windows\System32\payload.dll would be allowed, whereas C:\Windows\System32\Tasks\payload.dll would not. Any path inside of the printer driver directory is allowed, e.g. C:\Windows\System32\spool\drivers\x64\my\path\to\payload.dll is allowed. If we are able to create a DLL in C:\Windows\System32\ or anywhere in the printer driver directory, we can load the DLL into the Spooler service.

localspl.dll!SplLoadLibraryTheCopyFileModule

Now, we know that we can use the SpoolDirectory to create an arbitrary directory with writable permissions for all users, and that we can load any DLL into the Spooler service that resides in either C:\Windows\System32\ or the printer driver directory. There is only one issue though. As mentioned earlier, the spool directory is created during the Spooler initialization. The spool directory is created when localspl.dll!SplCreateSpooler calls localspl.dll!BuildPrinterInfo. Before localspl.dll!BuildPrinterInfo allows Everybody the FILE_ADD_FILE permission, a final check is made to make sure that the directory path does not reside within the printer driver directory.

localspl.dll!BuildPrinterInfo

This means that a security check during the Spooler initialization verifies that the SpoolDirectory value does not point inside of the printer driver directory. If it does, the Spooler will not create the spool directory and simply fallback to the default spool directory. This security check was also implemented in the patch for CVE-2020-1030.

To summarize, in order to load the DLL with localspl.dll!SplLoadLibraryTheCopyFileModule, the DLL must reside inside of the printer driver directory or directly inside of C:\Windows\System32\. To create the writable directory during Spooler initialization, the directory must not reside inside of the printer driver directory. Both localspl.dll!SplLoadLibraryTheCopyFileModule and localspl.dll!BuildPrinterInfo check if the path points inside the printer driver directory. In the first case, we must make sure that the DLL path begins with C:\Windows\System32\spool\drivers\x64\, and in the second case, we must make sure that the directory path does not begin with C:\Windows\System32\spool\drivers\x64\.

During the both checks, the SpoolDirectory is converted to a canonical path, so even if we set the SpoolDirectory to C:\spooldir\printers\ and then convert C:\spooldir\ into a reparse point that points to C:\Windows\System32\spool\drivers\x64\, the canonical path will still become \\?\C:\Windows\System32\spool\drivers\x64\printers\. The check is done by stripping of the first four bytes of the canonical path, i.e. \\?\C:\Windows\System32\spool\drivers\x64\ printers\ becomes C:\Windows\System32\spool\drivers\x64\ printers\, and then checking if the path begins with the printer driver directory C:\Windows\System32\spool\drivers\x64\. And so, here comes the second bug to pass both checks.

If we set the spooler directory to a UNC path, such as \\localhost\C$\spooldir\printers\ (and C:\spooldir\ is a reparse point to C:\Windows\System32\spool\drivers\x64\), the canonical path will become \\?\UNC\localhost\C$\Windows\System32\spool\drivers\x64\printers\, and during comparison, the first four bytes are stripped, so UNC\localhost\C$\Windows\System32\spool\drivers\x64\printers\ is compared to C:\Windows\System32\spool\drivers\x64\ and will no longer match. When the Spooler initializes, the directory \\?\UNC\localhost\C$\Windows\System32\spool\drivers\x64\printers\ will be created with writable permissions. We can now write our DLL into C:\Windows\System32\spool\drivers\x64\printers\payload.dll. We can then trigger the localspl.dll!SplLoadLibraryTheCopyFileModule, but this time, we can just specify the path normally as C:\Windows\System32\spool\drivers\x64\printers\payload.dll.

We now have the primitives to create a writable directory inside of the printer driver directory and to load a DLL within the driver directory into the Spooler service. The only thing left is to restart the Spooler service such that the directory will be created. We could wait for the server to be restarted, but there is a technique to terminate the service and rely on the recovery to restart it. By default, the Spooler service will restart on the first two “crashes”, but not on subsequent failures.

To terminate the service, we can use localspl.dll!SplLoadLibraryTheCopyFileModule to load C:\Windows\System32\AppVTerminator.dll. When loaded into Spooler, the library calls TerminateProcess which subsequently kills the spoolsv.exe process. This event triggers the recovery mechanism in the Service Control Manager which in turn starts a new Spooler process. This technique was explained for CVE-2020-1030 by Victor Mata from Accenture.

Here’s a full exploit in action. The DLL used in this example will create a new local administrator named “admin”. The DLL can also be found in the exploit repository.

SpoolFool in action

The steps for the exploit are the following:

  • Create a temporary base directory that will be used for our spool directory, which we’ll later turn into a reparse point
  • Create a new local printer named “Microsoft XPS Document Writer v4”
  • Set the spool directory of our new printer to be our temporary base directory
  • Create a reparse point on our temporary base directory to point to the printer driver directory
  • Force the Spooler to restart to create the directory by loading AppVTerminator.dll into the Spooler
  • Write DLL into the new directory inside of the printer driver directory
  • Load the DLL into the Spooler

Remember that it is sufficient to create the driver directory only once in order to load as many DLLs as desired. There’s no need to trigger the exploit multiple times, doing so will most likely end up killing the Spooler service indefinitely until a reboot brings it back up. When the driver directory has been created, it is possible to keep writing and loading DLLs from the directory without restarting the Spooler service. The exploit that can be found at the end of this post will check if the driver directory already exists, and if so, the exploit will skip the creation of the directory and jump straight to writing and loading the DLL. The second run of the exploit can be seen below.

Second run of SpoolFool

The functional exploit and DLL can be found here: https://github.com/ly4k/SpoolFool.

Conclusion

That’s it. Microsoft has officially released a patch. When I initially found out that there was a check during the actual creation of the directory as well, I started looking into other interesting places to create a directory. I found this post by Jonas L from Secret Club, where the Windows Error Reporting Service (WER) is abused to exploit an arbitrary directory creation primitive. However, the technique didn’t seem to work reliably on my Windows 10 machine. The SplLoadLibraryTheCopyFileModule is very reliable however, but assumes that the user can manage a printer, which is already the case for this vulnerability.

Patch Analysis

[Feb 08, 2022]

A quick check with Process Monitor reveals that the SpoolDirectory is no longer created when the Spooler initializes. If the directory does not exist, the Print Spooler falls back to the default spool directory.

To be continued…

Disclosure Timeline

  • Nov 12, 2021: Reported to Microsoft Security Response Center (MSRC)
  • Nov 15, 2021: Case opened and assigned
  • Nov 19, 2021: Review and reproduction
  • Nov 22, 2021: Microsoft starts developing a patch for the vulnerability
  • Jan 21, 2022: The patch is ready for release
  • Feb 08, 2022: Patch is silently released. No acknowledgement or information received from Microsoft
  • Feb 09, 2022: Microsoft gives acknowledgement to me and Institut For Cyber Risk for CVE-2022-21999

SpoolFool: Windows Print Spooler Privilege Escalation (CVE-2022-21999) was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

First!

8 February 2022 at 17:38

Today we are launching the IFCR research blog. The ambition is to provide original research and new technical ideas and approaches to the offensive security community.

New material will be added occasionally and not by a defined schedule. That being said, we’ve already got quite a few interesting posts lined-up.

Stay tuned for more…!

The IFCR Offensive Ops Team


First! was originally published in IFCR on Medium, where people are continuing the conversation by highlighting and responding to this story.

❌
❌