In May 2020, FusionX reported an elevation of privilege vulnerability to the Microsoft Security Response Center (MSRC). The vulnerability affected the application logic implemented in the Windows Print Spooler service. It can be exploited by unprivileged users to attain arbitrary code execution as SYSTEM. Exploitation involves chaining several primitives to load an arbitrary DLL into the print spooler process. Microsoft addressed the issue in CVE-2020-1030, released in September. This post walks through the technical details and illustrates the exploit development process.

Getting a handle on printer privileges

The vulnerability emerges when a user adds a printer to a Windows system. By default, users can add printers without administrator privileges. This is only applicable if the printer uses a preinstalled or inbox driver. We use the following code to add a printer with the Microsoft Print To PDF driver.

<<< Start >>>

Figure 1 – Adding local printer with Microsoft Print To PDF driver

<<< End >>>

Calling AddPrinter returns a printer handle with the PRINTER_ALL_ACCESS permission, granting access rights to standard and administrative print operations. This behavior is further outlined in Microsoft’s documentation:

The caller of the AddPrinter function must have SERVER_ACCESS_ADMINISTER access to the server on which the printer is to be created. The handle returned by the function will have PRINTER_ALL_ACCESS permission, and can be used to perform administrative operations on the printer.

While it seems illogical to have SERVER_ACCESS_ADMINISTER permission as an unprivileged user, that is the intended configuration according to the Print Server security properties (Figure 2). The INTERACTIVE security identifier has the Manage Server permission enabled, which corresponds to SERVER_ACCESS_ADMINISTER. Possessing a subset of these permissions is referred to as a partial delegated print administrator.

<<< Start >>>

Figure 2 – Print Server permissions for INTERACTIVE security identifier

<<< End >>>

The printer handle, as a result of AddPrinter, introduces spooler APIs, which are inaccessible from an unprivileged context. However, administrative access is only scoped to the printer object, limiting the functions at our disposal. The subsequent sections will demonstrate how the handle is leveraged to undermine the application’s logic.

Point and print: A missing path could spell trouble

Point and Print is one of several printer sharing technologies designed for driver distribution. In Point and Print, driver (prior to v4 model) and configuration files are automatically downloaded from the print server. Installation is extendable with a custom Point and Print DLL. The library is implemented by defining the CopyFiles registry key within the printer’s configuration.

Printer configurations are stored as individual subkeys under HKLM\Software\Microsoft\Windows NT\CurrentVersion\Print\Printers. The 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 SetPrinterData and its extended version, SetPrinterDataEx. These functions require a printer handle with PRINTER_ACCESS_ADMINISTER. Users can retrieve a handle with functions such as OpenPrinter or, in our case, AddPrinter as previously illustrated.

<<< Start >>>

Figure 3 – Assigning Point and Print DLL to printer configuration

<<< End >>>

SetPrinterDataEx with the CopyFiles registry key causes the spooler to automatically load the Point and Print DLL assigned in the Module value (Figure 3). This event is triggered when pszKeyName begins with the CopyFiles\ string (Figure 4). It initiates a sequence of functions leading to LoadLibrary and LoadLibraryEx – Windows API for mapping a DLL into the current process.

<<< Start >>>

Figure 4 – SplSetPrinterDataEx checking for "CopyFiles" registry key

<<< End >>>

The control flow consists of the following events:

  1. spoolsv!SetPrinterDataEx routes to SplSetPrinterDataEx in the local print provider, dll
  2. localspl!SplSetPrinterDataEx validates permissions before restoring SYSTEM context and modifying the registry via localspl!SplRegSetValue
  3. localspl!SplCopyFileEvent is called if pszKeyName argument begins with CopyFiles\ string
  4. localspl!SplCopyFileEvent reads the Module value from printer’s CopyFiles registry key and passes the string to localspl!SplLoadLibraryTheCopyFileModule
  5. localspl!SplLoadLibraryTheCopyFileModule sets the current directory of the exe process to System32
  6. localspl!SplLoadLibraryTheCopyFileModule performs validation with localspl!MakeCanonicalPath and localspl!IsModuleFilePathAllowed then attempts to load the module with LoadLibrary
  7. If validation or LoadLibrary fails, an alternative path is retrieved from localspl!GetIniDriverAndDirForThisMachineEx and calls LoadLibraryEx with LOAD_WITH_ALTERED_SEARCH_PATH

<<< Start >>>

Figure 5 – Control flow of SetPrinterDataEx in local print provider (localspl.dll)

<<< End >>>

<<< Start >>>

Figure 6 – Control flow graph of SplLoadLibraryTheCopyFileModule

<<< End >>>

The print spooler initially attempts loading the Point and Print DLL from the system directory. If this fails, it performs an additional attempt using a new path sourced from the spooler, driver, environment, and driver version directories. We can observe this behavior by intentionally calling SetPrinterDataEx with an invalid module.

<<< Start >>>

Figure 7 – Search paths for Point and Print DLLs

<<< End >>>

The spooler searches the following paths when loading a Point and Print DLL.

  1. %SYSTEMROOT%\System32
  2. %SYSTEMROOT%\System32\spool\drivers\<ENVIRONMENT>\<DRIVERVERSION>

Notice the PATH NOT FOUND result in Figure 6. This path refers to the version 4 driver directory. Based on our testing, the version 4 driver directory is absent on Windows systems. Its absence may correspond with the introduction of the v4 driver model in Windows 8 and Windows Server 2012.

The missing path indicated a potential code execution opportunity if we can create the directory with read and write permissions. An arbitrary DLL can then be placed into the file path and invoked via SetPrinterDataEx. Unfortunately, the environment directory (x64) inherits its DACL from its parent directory, preventing unprivileged users from simply creating the missing path.

<<< Start >>>

Figure 8 – Contents of x64 driver directory

<<< End >>>

<<< Start >>>

Figure 9 – DACL of x64 driver directory

<<< End >>>

Spooler directory: Take a careful look under the hood

When a user prints a document, a print job is spooled to a predefined location referred to as the spool directory. The default location is C:\Windows\System32\spool\PRINTERS. To maintain relevance, there are two important aspects to note:

  1. The spool directory must allow WriteData permission to all users
  2. The spool directory is configurable on each printer

<<< Start >>>

Figure 10 – Registry value and DACL of default spool directory

<<< End >>>

Individual spool directories are supported by defining the SpoolDirectory value in a printer’s registry key. If unspecified, the printer is mapped to the DefaultSpoolDirectory instead. The spool directory is created (or mapped if it exists) when localspl!SplCreateSpooler calls localspl!BuildPrinterInfo. This has only been observed when the spooler service initializes; therefore, changes to a printer’s spool directory aren’t reflected until service has restarted. We will revisit spooler initialization momentarily.

<<< Start >>>

Figure 11 – Registry value for printer spool directory

<<< End >>>

Once the printer object is created (Figure 1), we leverage the returned handle to call SetPrinterDataEx and configure the printer’s spool directory. Keep in mind, SetPrinterDataEx requires administrative permission, which is afforded with the handle’s PRINTER_ALL_ACCESS access rights.

<<< Start >>>

Figure 12 – Assigning spool directory to printer configuration

<<< End >>>

*Note: The pszKeyName argument is relative to the printer's registry key. Pass a backslash to pszKeyName to set a value in the root and not a subkey. This will allow access to the printer's main configuration values.
Print spooler service & initialization: Time to terminate

The print spooler service must reinitialize for the spool directory to take effect. Standard users are restricted from restarting system services, and thereby limited to two options:

  1. Wait for the system or service to restart
  2. Forcefully restart the service using an unconventional method

Our efforts thus far have focused on loading arbitrary Point and Print libraries. However, this is only necessary if we want to load arbitrary DLLs. Our code execution primitive is perfectly applicable (and free from additional stipulations) to existing files in the System32 directory.

Enter AppVTerminator.dll. This library is a signed Microsoft binary included in Windows (confirmed on Windows 10). 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.

<<< Start >>>

Figure 13 – Control flow graph of AppVTerminator.dll

<<< End >>>

<<< Start >>>

Figure 14 – Default recovery configuration of Print Spooler service

<<< End >>>

We can leverage SetPrinterDataEx to set AppVTerminator.dll as a Point and Print DLL. Specifying the Module value name will invoke the Point and Print behavior. The spoolsv.exe service will immediately restart as a result of loading the library.

Spooler initialization can be delayed from a few seconds up to several minutes once the service has restarted. This can pose a minor inconvenience in a time-sensitive operation. After evaluating several APIs, EnumPrinters proved most reliable for invoking localspl!BuildPrinterInfo. Executing EnumPrinters sets off the following chain of calls:

  1. spoolsv!PrvEnumPrinters fetches and sets the RouterPreInitEvent event (spoolsv!WaitForSpoolerInitialization)
  2. RouterPreInitEvent is created in spoolsv!SpoolerInitializeSpooler and waited on in spoolsv!PreInitializeRouter
  3. Once the event is set, spoolsv!InitializeRouter is invoked
  4. This routine initializes print providers and first starts with dll. It invokes LoadLibrary to load the DLL, then fetches and calls the InitializePrintProvidor function
  5. localspl!InitializePrintProvidor eventually calls localspl!SplCreateSpooler, which in turn calls localspl!BuildPrinterInfo
  6. localspl!BuildPrinterInfo effectively builds local printer information spools

Our proof of concept implements some logic to monitor the service and spool directory. Once those threads have started, we issue multiple calls to EnumPrinters to expedite initialization.

Getting a handle on code execution
When the service initializes, the spool directory (spool\drivers\x64\4) is created with a SECURITY_DESCRIPTOR granting all users with WriteData permission. This allows us to move our payload into the directory.

Our payload, imitating as a Point and Print DLL, must export the SpoolerCopyFileEvent function. This function is called once the module is loaded into the process.

<<< Start >>>

Figure 15 – Exported function in Point and Print DLL

<<< End >>>

To obtain code execution, we use OpenPrinter to request a handle to our existing printer object (Figure 1). SetPrinterDataEx is called once more to trigger Point and Print with our payload. Figure 16 shows the file path argument sent to LoadLibraryEx, which is responsible for loading the module into the spoolsv.exe process.

<<< Start >>>

Figure 16 – Call to LoadLibraryExW with payload file path

<<< End >>>

<<< Start >>>

Proof of concept demonstration

<<< End >>>

To combat this vulnerability, check our PoC on Github

The Windows Print Spooler is a complex ecosystem with a code base dating as far back as NT4. And, in combination with new capabilities added in later Windows versions, the print spooler is prime for logic-based vulnerabilities. This is evident in the uptick of spooler-related findings reported in 2020.[i]

Our proof of concept is available on our Github. We would like to thank Microsoft’s Security Response Center and Bryan Alexander for his guidance and contribution.

Disclosure Timeline
Date Event
2020-05-06 Report submitted to Microsoft Security Response Center
2020-05-07 Case 58551 opened
2020-05-14 Microsoft requested additional information
2020-05-15 Provided additional files for reproduction
2020-06-08 Requested status update
2020-06-10 Received update, vulnerability successfully reproduced
2020-06-18 Received update, fix scheduled for September release
2020-09-08 Vulnerability addressed in CVE-2020-1030

 

Accenture Security is a leading provider of end-to-end cybersecurity services, including advanced cyber defense, applied cybersecurity solutions and managed security operations. We bring security innovation, coupled with global scale and a worldwide delivery capability through our network of Advanced Technology and Intelligent Operations centers. Helped by our team of highly skilled professionals, we enable clients to innovate safely, build cyber resilience and grow with confidence. Follow us @AccentureSecure on Twitter or visit us at www.accenture.com/security.

Copyright © 2020 Accenture. All rights reserved. Accenture, and its logo are trademarks of Accenture.

This document is produced by consultants at Accenture as general guidance. It is not intended to provide specific advice on your circumstances. If you require advice or further details on any matters referred to, please contact your Accenture representative. Given the inherent nature of this document, the content contained in this article is based on information gathered and understood at the time of its creation. It is subject to change. Accenture provides the information on an “as-is” basis without representation or warranty and accepts no liability for any action or failure to act taken in response to the information contained or referenced in this article.

___

[i]  List of Print Spooler CVEs:

https://www.thezdi.com/blog/2020/8/11/windows-print-spooler-patch-bypass-re-enables-persistent-backdoor

https://nvd.nist.gov/vuln/detail/CVE-2019-0759

https://www.cyberscoop.com/windows-print-spooler-safebreach-black-hat/

 

Victor Mata

Security Principal

Subscribe to Accenture's Cyber Defense Blog Subscribe to Accenture's Cyber Defense Blog