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.
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.
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.
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.
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 toSplSetPrinterDataEx
in the local print providerlocalspl.dll
- 2.
localspl.dll!SplSetPrinterDataEx
validates permissions before restoringSYSTEM
context and modifying the registry vialocalspl.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.
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
.
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
.
The control flow consists of the following events:
- 1.
spoolsv.exe!SetPrinterDataEx
routes toSplSetPrinterDataEx
in the local print providerlocalspl.dll
- 2.
localspl.dll!SplSetPrinterDataEx
validates permissions before restoringSYSTEM
context and modifying the registry vialocalspl.dll!SplRegSetValue
- 3.
localspl.dll!SplCopyFileEvent
is called ifpszKeyName
argument begins withCopyFiles\
string - 4.
localspl.dll!SplCopyFileEvent
reads theModule
value from printer’sCopyFiles
registry key and passes the string tolocalspl.dll!SplLoadLibraryTheCopyFileModule
- 5.
localspl.dll!SplLoadLibraryTheCopyFileModule
performs validation on theModule
file path - 6. If validation passes,
localspl.dll!SplLoadLibraryTheCopyFileModule
attempts to load the module withLoadLibrary
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!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.
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.
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.
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.
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
You may also enjoy reading, CVEs You May Have Missed While Log4J Stole The Headlines
Stay informed of the latest Cybersecurity trends, threats and developments. Sign up for RiSec Weekly Cybersecurity Newsletter Today
Remember, CyberSecurity Starts With You!
- Globally, 30,000 websites are hacked daily.
- 64% of companies worldwide have experienced at least one form of a cyber attack.
- There were 20M breached records in March 2021.
- In 2020, ransomware cases grew by 150%.
- Email is responsible for around 94% of all malware.
- Every 39 seconds, there is a new attack somewhere on the web.
- An average of around 24,000 malicious mobile apps are blocked daily on the internet.