Normal view

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

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.

❌
❌