Understanding PrintNightmare Vulnerability

Hido Cohen
7 min readJul 20, 2021
Image by Mashka via Adobe Stock

PrintNightmare (CVE-2021–34527) is a new vulnerability which was raised after Microsoft has patched a prior vulnerability — CVE-2021–1675. According to MSDN, PrintNightmare is:

A remote code execution vulnerability exists when the Windows Print Spooler service improperly performs privileged file operations. An attacker who successfully exploited this vulnerability could run arbitrary code with SYSTEM privileges. An attacker could then install programs; view, change, or delete data; or create new accounts with full user rights.

Please keep in mind the difference between the two:

CVE-2021–1675 is a local privilege escalation vulnerability where CVE-2021–34527 is a remote code execution vulnerability.

Today, I want to walk you through my research about the vulnerability and show you what allowed this vulnerability to exist in the first place. After we’ll discuss how and why the exploit works, we’ll move on to see how Microsoft has patched this issue.

High-Level Overview

PrintNightmare exploits the way that Print Spooler adds a new printer driver to a remote system. Looking at MSDN for some details about the architecture of printer providers we can find the following diagram:

Figure 1: Print Providers Diagram

According to that diagram, we can see that the client-server communication is done over RPC and the process that is responsible for handling such requests is spoolsv.exe.

Under the hood, spoolsv.exe uses a DLL called Localspl.dll which is responsible for communicating with the kernel components and actually doing the work.

When a client wants to add a new printer driver on a remote system it can use the function RpcAddPrinterDriverEx. The privilege that the client needs is SeLoadDriverPrivilege which is enabled by default to the Administrators group.

The vulnerable function allows the attacker to skip that privilege check and load any printer driver it desires without the required permissions which leads to remote code execution with escalated privileges.

Identifying the Vulnerability

We can divide the vulnerability into two phases, a privilege escalation phase and a remote code execution phase.

Both phases reside inside the spoolsv.exe and localspl.dll binaries. Let’s deep dive into the relevant functions and see where the flaw is.

Privilege Escalation

Opening spoolsv.exe with IDA, reveals the function RpcAddPrinterDriverEx:

Figure 2: Unpatched version of RpcAddPrinterDriverEx

We don’t see anything special with this function (yet), so I drilled down to see where the function call for adding a new printer driver. As we saw in figure 1, spoolsv.exe calls localspl.dll, moving to that DLL reveals the following function:

Figure 3: SplAddPrinterDriverEx function

Looking closely at lines 21–26 raises a logic flaw. I renamed the variables for better context:

  • Line 21: fCheckPriv is set to zero
  • Line 22: The function checks if the 15th bit of dwFileCopyFlags is set to 1. Notice that the variable dwFileCopyFlags is controlled by the client when calling RpcAddPrinterDriverEx which means that the client can choose the result of the if statement.
  • Line 23: If the 15th bit of dwFileCopyFlags is 1, fCheckPriv is assigned with some value, which means it doesn’t equal zero anymore (if a7 is not zero).
  • Line 24: The function checks if fCheckPriv is set, if so, it calls ValidateObjectAccess which is responsible for checking if the client has SeLoadDriverPrivilege.
  • Line 25: The function exits if the privilege check fails.
  • Line 26: The function calls InternalAddPrinterDriverEx which is responsible for actually adding the new printer driver.

Did you identify the vulnerability? All the attacker needs to do is to set the 15th bit of dwFileCopyFlags — which is in his control — to be 1.

At this point there are several exploit PoCs available in Github [3] [4].

We can see that the exploit making sure that the relevant bit is one:

Figure 4: dwFileCopyFlags 15th bit is set to 1

dwFileCopyFlags = APD_COPY_ALL_FILES (0x4) + 0x10 + 0x8000 = 0x8014 = 1000 0000 0001 0100

We can also open WinDbg and set a breakpoint just before calling InternalAddPrinterDriverEx to see the call stack until this point:

Figure 5: AddPrinterDriverEx call stack

Remote Code Execution

Once the attacker bypasses the privileges check he’s now able to call InternalAddPrinterDriverEx and achieve remote code execution. Let’s see how.

Please note that this is a long function so I won’t add any screen shots, but, We’ll talk about its general execution flow.

  1. ValidateDriverInfo — Gets the driver data sent by the client and validates the signature of the driver and checks the file type.
  2. CreateInternalDriverFileArray — Creates the driver files inside %SPOOLER%\drivers\x64\
  3. GetPrintDriverVersion — Extracts the driver version (3)
  4. CheckFilePlatform — Checks the platform of the driver and data file
  5. CreateVersionDirectory — Creates the version directory %SPOOLER%\drivers\x64\3\
  6. CopyFilesToFinalDirectory — This function has several tasks:
  • Copies the driver files into “Old” and “New” to a temporary sub-directory according to the version number, for example, %SPOOLER%\drivers\x64\3\old\1.
  • If you add a new version of the same driver, it’s previous file will be added to the %SPOOLER%\drivers\x64\old\<version_number>\ directory and will stay there.

7. WaitRequiredForDriverUnload — At this point all the files located in the system, this function is responsible for loading the driver.

The problem resides with the way files are copied and saved on the system. As mentioned before, there are three files that we care about, driver file, data file and config file. All of which supports UNC file paths.

We can divide the attack method into two parts: copying the malicious DLL from the attacker machine file share into the target machine local filesystem and loading the malicious DLL into spoolsv.exe.

The first part is done by calling RpcAddPrinterDriverEx with the malicious DLL as the data file. This will cause the file to be copied into %SPOOLER%\drivers\x64\old\<version_number>\malicious.dll once we call RpcAddPrinterDriverEx again with updated values.

The second call to RpcAddPrinterDriverEx will set the config file to be %SPOOLER%\drivers\x64\old\<version_number>\malicious.dll. This is a local path and now it will be loaded into the spoolsv.exe process.

We can see that the PoC does exactly that:

Figure 6: PoC source-code

Microsoft’s Response

After understanding exactly how the exploits work, Let’s see how Microsoft fixed the issue. For that, we need to find a patched file and compare between the two. The quick-and-dirty way to acquire both versions is by spinning up two Windows machines each one with the desired version. The second method is by following this amazing article which explains how we can extract and differentiate Windows patches. If you choose the second method you will identify what specific files have been updated (spoiler alert: spoolsv.exe and winspool.drv).

Once you get your hand over patched and unpatched versions, we can use any diffing tool we would like. I chose BinDif. Looking at RpcAddPrinterDriverEx:

Figure 7: RpcAddPrinterDriverEx BinDiff

It’s easy to see that Microsoft has added some additional logic to the function. The patch version of that function is:

Figure 8: Patched version of RpcAddPrinterDriver

There are several additional checks before the function continues the execution (compared to figure 2).

The function names are pretty informative, this allows us to quickly understand the logic of the update.

The most interesting information we can extract from the update is:

  1. There are two new registry keys:
  • HKLM\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrint\NoWarningNoElevationOnInstall
  • HKLM\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrint\RestrictDriverInstallationToAdministrators

Which are responsible for elavation_required and restrict_to_admins flags.

2. Before continuing the execution the function checks if elavation_required flag is enabled and if so, it checks if the user is an admin. If the user isn’t admin, the function will set the 15th bit of dwFileCopyFlags to be one — which forces a privilege check inside localspl!SplAddPrinterDriverEx. This is how Microsoft fixes the privilege escalation vulnerability.

3. Next, the function checks if the restrict_to_admins is disabled or if the user is an admin — those checks prevent unauthorized users from loading a new printer driver.

Conclusion

PrintNightmare is an interesting story about vulnerability snowballing, it started as a local privilege escalation and ended up to be a severe remote code execution vulnerability. In this article we dived deep into the internals of the Printer Spooler logic, specifically we looked at how it handles addition of new printer drivers. After understanding the vulnerabilities we saw how Microsoft’s patch fixes those flaws.

For me it was very fun and informative research, although this is a N-day vulnerability, I wanted to understand exactly how it works to be able to show you my findings in a clear way. Hope you found this reading worth your time! Stay safe :)

References

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

[2] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34527

[3] https://github.com/cube0x0/CVE-2021-1675

[4] https://github.com/calebstewart/CVE-2021-1675

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

[6] https://wumb0.in/extracting-and-diffing-ms-patches-in-2020.html

--

--