Understanding PrintNightmare Vulnerability
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.
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:
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
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.
spoolsv.exe with IDA, reveals the function
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,
localspl.dll, moving to that DLL reveals the following function:
Looking closely at lines 21–26 raises a logic flaw. I renamed the variables for better context:
- Line 21:
fCheckPrivis set to zero
- Line 22: The function checks if the 15th bit of
dwFileCopyFlagsis set to 1. Notice that the variable
dwFileCopyFlagsis controlled by the client when calling
RpcAddPrinterDriverExwhich means that the client can choose the result of the if statement.
- Line 23: If the 15th bit of
fCheckPrivis assigned with some value, which means it doesn’t equal zero anymore (if a7 is not zero).
- Line 24: The function checks if
fCheckPrivis set, if so, it calls
ValidateObjectAccesswhich is responsible for checking if the client has
- Line 25: The function exits if the privilege check fails.
- Line 26: The function calls
InternalAddPrinterDriverExwhich 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  .
We can see that the exploit making sure that the relevant bit is one:
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:
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.
ValidateDriverInfo— Gets the driver data sent by the client and validates the signature of the driver and checks the file type.
CreateInternalDriverFileArray— Creates the driver files inside
GetPrintDriverVersion— Extracts the driver version (3)
CheckFilePlatform— Checks the platform of the driver and data file
CreateVersionDirectory— Creates the version directory
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,
- 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.
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
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
We can see that the PoC does exactly that:
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:
Once you get your hand over patched and unpatched versions, we can use any diffing tool we would like. I chose BinDif. Looking at
It’s easy to see that Microsoft has added some additional logic to the function. The patch version of that function is:
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:
- There are two new registry keys:
Which are responsible for
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.
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 :)