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.
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:
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
:
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:
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 variabledwFileCopyFlags
is controlled by the client when callingRpcAddPrinterDriverEx
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 callsValidateObjectAccess
which is responsible for checking if the client hasSeLoadDriverPrivilege
. - 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:
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%SPOOLER%\drivers\x64\
GetPrintDriverVersion
— Extracts the driver version (3)CheckFilePlatform
— Checks the platform of the driver and data fileCreateVersionDirectory
— Creates the version directory%SPOOLER%\drivers\x64\3\
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:
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
:
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:
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