Windows Print Spooler Patch Bypass Re-Enables Persistent Backdoor

August 11, 2020 | Simon Zuckerbraun

In May 2020, Microsoft patched CVE-2020-1048, a critical privilege escalation bug in Windows. Through this vulnerability, an attacker with the ability to execute low-privileged code on a Windows machine can easily establish a persistent backdoor, allowing the attacker to return at any later time and escalate privileges to SYSTEM. The backdoor is “persistent” in the sense that, once established, the backdoor will persist even after a patch for the vulnerability has been applied. CVE-2020-1048 is credited to Peleg Hadar (@peleghd) and Tomer Bar of SafeBreach Labs. It is also described in a highly-detailed Windows Internals blog post by Yarden Shafir & Alex Ionescu, who apparently made an independent discovery of this bug at about the same time as the SafeBreach Labs team.

On May 25, 2020, a mere 13 days after the release of the patch for CVE-2020-1048, the ZDI program received a submission from a researcher who goes by the name math1as. In the submission, math1as showed how Microsoft’s patch is insufficient to prevent exploitation of the vulnerability. This new flaw was addressed in the August patches as CVE-2020-1337.

In this post, I will first recap the original Print Spooler vulnerability. My discussion will draw upon the findings of Shafir and Ionescu. Next, I will explain how math1as evaded the May patch, and examine the new patch Microsoft has released in response.

CVE-2020-1048: The Original Vulnerability

The original vulnerability, CVE-2020-1048, hinges upon some peculiarities in the architecture of the Windows Print Spooler service. Triggering the vulnerability gives an attacker the ability to write arbitrary data to any file on the system. The file write takes place in the context of SYSTEM. An attacker can use this to drop a malicious DLL to System32, where it will be picked up and executed by a process running as SYSTEM.

Shafir and Ionescu write that the Print Spooler service has received relatively little scrutiny and that its code has not changed much since Windows NT 4. That alone should tell us that we can expect some interesting surprises therein. As they explain in their blog post, the first architectural features we should take note of are as follows:

       -- An unprivileged user is permitted to install a new printer driver, as long as the printer driver is on the list of “pre-existing, inbox drivers” present in the registry. One of these is called Generic / Text Only, which, coincidentally, is of great utility to an attacker who is seeking to write controlled data.
       -- An unprivileged user is further permitted to create a new printer port on the system.
       -- A printer port is described by a string. If the string is a path to a file, then the port is a “file port” and any printer output directed to that port is simply written to the specified file. (They point out that this is not to be confused with the concept of “printing to a file”, which refers to the use of the special ports FILE: or PORTPROMPT:.)
       -- An unprivileged user is further permitted to add a printer to the system, which is essentially a binding between a driver and a port. The printer is identified by a name, which again is simply a string. Once created, a document can be printed to the printer identified by that name. If the associated driver is Generic / Text Only and the port is a file port, then any text content printed to that printer will be written verbatim to the specified file, just like the output that would be sent to a dot-matrix printer from the 1980s (think echo Test >LPT1:).

Conveniently, each of these tasks can be accomplished easily with a simple PowerShell command (note, the assumption is that the user is named user1):

Figure 1

The only hitch is that the output file will contain some additional whitespace, added to produce margins. Shafir and Ionescu mention how this problem can be circumvented easily by using the Win32 API to write the document to the printer instead of using PowerShell’s Out-Printer.

The next point to consider is the context in which the file write occurs. The Print Spooler service runs as SYSTEM, but this does not mean that we immediately have a way to write an arbitrary file as SYSTEM. That would be far too easy. The Print Spooler has two safety mechanisms built in:

1.     Safety check at port creation time: When creating a file port, a test is made to ensure that the user creating the port has write access to the requested file.

2.     Safety check at print time: When performing the actual printing, the Print Spooler service does not run as SYSTEM, but instead impersonates the user that requested the print job.

The essence of CVE-2020-1048 is that each of these two safety mechanisms contains a fatal flaw.

First, let us discuss the safety check at port creation time. It turns out that this check is not located in the Print Spooler service at all, but rather in a UI component that is the spooler service’s expected client-side counterpart! This is a classic example of CWE-602: Client-Side Enforcement of Server-Side Security. In our case, though, no client-side hacking is even needed, because it so happens that the new client-side code that comes with PowerShell’s Add-PrinterPort does not contain the security check offered by the original UI client. So, by going the PowerShell route, no check at all occurs at port creation time, not even on the client side.

Now to discuss the safety check at print time. To understand what goes wrong with this check, we must first note that spooled print jobs persist over reboots. This raises an interesting question. If a reboot has intervened, then the token representing the requester of the print job no longer exists. When the job resumes, then, what do you think the Print Spooler service does when the time comes to impersonate the original requester?

If your guess was “it punts”, you win the prize! If a reboot has intervened, so that the original token associated with the print job is no longer available, then the Print Spooler executes the job using a token associated with the process’s identity of SYSTEM. Shafir and Ionescu express surprise at learning that this behavior, which dates back to Windows NT 4, is apparently by design and will not be remediated.

The final PoC for CVE-2020-1048, then, is as follows:

Figure 2

Initially, the Print Spooler will fail to write to the file C:\Windows\System32\test.dll, because it is impersonating the requester. After a reboot, though, it will try again as SYSTEM, and this time it will successfully write the attacker’s content to the file. A crash and restart of the Print Spooler service can be used in place of a reboot since this will also serve to destroy the requester’s token held by the Print Spooler process.

This was tested on Windows 10 1909 18363.778 x64 (April 2020 patch level).

Interlude: Debugging the Start of a Windows Service

While investigating this vulnerability, I wanted to use a debugger to examine the behavior of the Print Spooler service when it resumes a job after a crash. I wanted the ability to break into the debugger very soon after the launch of the Print Spooler process, in order to have the opportunity to set relevant breakpoints. This presented a technical challenge.

In the case of debugging normal applications, accomplishing such a task is straightforward. One can launch the application from the debugger, and rely on the “initial breakpoint” feature to stop execution before the start of application code, thus providing an opportunity to set the desired breakpoints. This approach cannot be used with a Windows service, though. Processes implementing Windows services are launched from the special process services.exe. Attempting to launch a Windows service process from the debugger instead generally will not work.

A second common technique for breaking into a process immediately at launch time is to use the “Debugger” option within Global Flags:

Figure 3

This instructs Windows that the specified executable should always be launched in WinDBG. While this technique is handy for attaching a debugger to certain processes that tend to launch at unpredictable times (such as dllhost.exe), unfortunately, it won’t help for Windows services. This difficulty is that Windows services run within a special non-interactive Windows session, session 0. Automatically launching a service within WinDBG using gflags will technically succeed, but in this case, WinDBG will itself be part of session 0. WinDBG will not display on any visible desktop and will therefore be unusable.

Incidentally, there is an option in service configuration called “Allow service to interact with desktop”. One might think that setting this option causes the service to run on the interactive desktop instead of within session 0, but this is not the case.

What other avenues remain? I realized that all I really needed was a delay upon the launch of the service process, to give me time to manually attach WinDBG to the already-created process. Well, such matters can be arranged. I loaded the Print Spooler executable spoolsv.exe into IDA and navigated to the entry function mainCRTStartup. At the start of this function, I used Patch program | Assemble… to write the instruction jmp $ (hex: EB FE):

Figure 4

This is a jump instruction that jumps to itself, forming an infinite loop. Using Patch program | Apply patches to input file…, I obtained a patched version of spoolsv.exe. After stopping the service, I replaced spoolsv.exe in System32 with my patched version. Upon restarting the service, I now had a spoolsv.exe that was hung on startup, providing enough time to manually attach a debugger.

A bit more work was still needed. There must be a way to break out of the infinite loop and allow the service to proceed normally. After attaching the debugger, I switched to the main thread, manually reverted my 2-byte patch, set whatever breakpoints I wanted, and continued execution. The WinDBG command I used looked like this:

Figure 5

Note that when services.exe starts a service process, it expects the new process to respond within about 30 seconds, or else it will abort. To save precious seconds, I found it useful to prepare as much as possible before attempting to start the service. I made sure to have WinDBG already launched (as an administrator) and ready to go. I also prepared the above WinDBG command in Notepad, and even copied it to the clipboard, all ready to paste into WinDBG once it was attached.

I hope you find this technique helpful on those occasions when you want to debug code that executes soon after the start of a Windows process.

The May 2020 Patch for CVE-2020-1048

As mentioned above, Microsoft does not seem intent on fixing the weakness in the second security check. Accordingly, when they patched CVE-2020-1048 in May, they addressed only the first security check.

The weakness of the first security check was that it was performed only on the client side, and not on the server side, which is to say the Print Spooler service. A client-side check is insufficient because it takes place within an untrusted process that is under the control of a potential attacker, who is thereby in a position to disable the security check. This vulnerability type is CWE-602: Client-Side Enforcement of Server-Side Security, as explained above. The approach to remediating a vulnerability of this type is to implement the security check on the server side in addition to (or instead of) on the client side.

Using BinDiff to compare localspl.dll before and after the May patch (10.0.0.18362.693 vs. 10.0.0.18362.836), we can see that exactly one code change has been made:

Figure 6

The added code consists of calls to functions IsValidNamedPipeOrCustomPort and PortIsValid. Specifically, PortIsValid is the function that performs the check to determine whether the requester has write access to the specified file. The essential code of PortIsValid is as follows:

Figure 7

Keep in mind that the code in Figure 7 is executed while impersonating the requester. It first checks for an existing, writable file of the specified name. If not found, it goes on to try creating a new writable file with the specified name but uses the FILE_FLAG_DELETE_ON_CLOSE flag to promptly destroy any newly-created file. The function only returns TRUE if it was possible to gain write access to a file with the specified name while impersonating the requester.

Note that since no attempt was made to remediate the second security check, CVE-2020-1048 produces an insidious sort of persistence. As long as the attacker is successful once, they can return at any time in the future and write additional content to the printer, thus re-creating the malicious DLL whenever desired. This ability persists even if CVE-2020-1048 has been patched in the interim because the only functioning check is at the time of port creation.

Patch Bypass: CVE-2020-1337

It is evident that our submitter, math1as, very quickly realized that the patch shown above for CVE-2020-1048 is insufficient to prevent exploitation of the underlying vulnerability.

The trouble is that the new security check, shown above in Figure 7, evaluates a file path at the time of port creation. This is not sufficient to guarantee that the same textual file path will be safe to access at the time the print job runs. In particular, it is possible to specify a path to a file in an ordinary, writable location so that the check at port creation time succeeds, but then delete the specified folder and replace it with a junction to a sensitive location before the print job executes. A vulnerability of this type falls into the category CWE-59: Improper Link Resolution Before File Access ('Link Following'), in which a vulnerable piece of software can be influenced to access a file path that appears to be safe but in fact links to a sensitive location. It can also be thought of as belonging to the category CWE-386: Symbolic Name not Mapping to Correct Object.

The new steps for triggering the bug, post-patch, are therefore as follows (assuming the user name is user1):

Figure 8

As before, the file write will fail at first. After a reboot, however, the Print Spooler will retry the job as SYSTEM and successfully write to the intended file "C:\Windows\System32\test3.dll".

This was tested on Windows 10 1909 18363.836 x64 (May 2020 patch level).

Patch Analysis of CVE-2020-1337

Microsoft responded with a new patch in July 2020. The new patch adds a call to GetFinalPathNameByHandleW, which is used to determine the normalized form of the specified file path. If the normalized form does not match the specified path, the operation is denied. The patch also adds a call to GetFileInformationByHandle to make sure that the specified file is not linked to some other file elsewhere in the file system. These two new checks are performed both at port creation time and at print job execution time, before any data is written to the file port.

It is important to note that these two functions operate upon an already-open file handle, as opposed to operating on a file path string. The file handle passed to these functions is the same file handle that the spooler will later use in file write operations if the checks succeed. Furthermore, the file handle is opened using share mode FILE_SHARE_READ, prohibiting any other process from modifying the file while the handle is open. In this way, it is guaranteed that the functions will return correct information for the file that will be ultimately written to, and an attacker cannot perform any new redirections in between the check and the write.

Conclusion

Physical printing is an inherently mechanical process that runs many orders of magnitudes more slowly than electronic computation, making the spooling of print jobs a necessity. Print spooling as an architectural feature goes back as far as the Colossus computer, built in 1944[i]. Within Windows as well, the print spooler is rooted in history, sharing implementation details with long-forgotten iterations of the OS. It is likely that we have not yet seen the last of the surprises emerging from research into this component of Windows.

You can find me on Twitter at @HexKitchen, and follow the team for the latest in exploit techniques and security patches.


[i] B. Jack Copeland and Others, Colossus: The Secrets of Bletchley Park’s Codebreaking Computers, Oxford University Press. p. 99.