CVE-2020-1337 – PrintDemon is dead, long live PrintDemon!

Back to Posts

CVE-2020-1337 – PrintDemon is dead, long live PrintDemon!

Reading Time: 6 minutes

Banner Image by Sergio Kalisiak

TL; DR: I will explain, in details, how to trigger PrintDemon exploit and dissect how I’ve discovered a new 0-day; Microsoft Windows EoP CVE-2020-1337, a bypass of PrintDemon’s recent patch via a Junction Directory (TOCTOU).


  • PrintDemon primer, how the exploit works?
    • PrinterPort
    • WritePrinter
    • Shadow Job File
  • Binary Diffing CVE-2020-1048 Patch
  • CVE-2020-1337 – A bypass of CVE-2020-1048’s patch
  • Conclusion
  • Affected Systems
  • Disclosure Timeline

After Yarden Shafir’s & Alex Ionescu’s posts (PrintDemon, FaxHell) and their call to action, I’ve started diving into the PrintDemon exploit. PrintDemon is the catching name for Microsoft CVE-2020-1048: Windows Print Spooler Elevation of Privilege Vulnerability which is affecting (according to Microsoft), pretty much all Windows’ versions up to Windows 10.

It took me some time, especially because I was missing some basic concepts on Windows Print Spooler and its Internals but after three days (since its patch), I was able to reproduce a PoC.


Even if I appreciated their blog post, I think they’ve made it verbose on purpose; taking the reader on many different and false “paths” in order to try to “obfuscate” couple of information from “script kiddies”.

PrintDemon primer, how the exploit works?

PrintDemon is an elevation of privilege (EoP) vulnerability that exists in the Windows Print Spooler service as it improperly allows arbitrary file writing on the file system.

It relays on the fact that:

  • Unprivileged users can add printers.
  • The printer port can be a path to a file on disk.

In this way, when the newly added printer prints anything to its port, it instead creates a file on the filesystem and prints the content into it.


Unfortunately, if you try to manually add a local port (via the “Add Printer” functionality in the Control Panel), pointing to a path where your user does not have enough permissions, (eg. C:\Windows\System32\ualapi.dll ) you’ll end up getting an “Access Denied” error.

You’ll get the error because, as Shafir & Ionescu stated in their blog post, the GUI has a client-side check while, directly calling native Windows API, the check is not present. That’s also the very same reason why using PowerShell/WMI you can create files in paths outside of your user’s permissions, since they do not have that client-side check.


Even if you successfully write a simple program to add a printer and a local port pointing to a privileged location using Windows API, you won’t be able to trigger the bug, since you still have to find a way to overcome the default “print” behaviour that otherwise, will “mutilate” your payload.

Win32 applications use the default printing method that respects the margins of the Letter (or A4) format, adding few new lines for the top margin and spacing out your content from the left one.

To overcome this odd behaviour you must, again, call native Windows API and set the DOC_INFO_1 optionpDatatypetoRAW ”.

Now that everything is settled up, you will simply print the payload to your file and…

“Your Printer is in an error state”

Congratulations, you broke it!

Shadow Job File

Directly printing to it will break your printer and will put your file in an error state, your payload won’t get written on the file system and you will end up scratching your head asking yourself what you’ve done wrong…

It seems that the Print Spooler service will correctly retain your privileges and impersonate your user while writing to the privileged location, resulting in the above-mentioned error.

Bypassing this will require the use of shadow job file. Shadow job files are “backup” files of printer’s jobs in queue; they are used to store and resume jobs when the printer queue is enabled, as well as, restore jobs that were put in queue when the printer was disconnected or whenever an error occurred. Shadow job files do not retain information about the user which required their printing, for this reason, Print Spooler service will take care of them (using its own tokens and privileges).

In order to make the Print Spooler service use shadow files, you can trigger one of the following two conditions:

  1. Restart Windows’ Print Spooler service (which requires administrative privileges).
  2. Reboot.

Upon one of the two actions above occurs, Print Spooler service will kick in, taking the print job to the end and printing it to the right path. It will be able to write it anywhere on the filesystem since it has NT AUTHORITY\SYSTEM rights.

And that’s how you gain arbitrary file write!

Binary Diffing CVE-2020-1048 Patch

Binary diffing CVE-2020-1048’s patch (IDA+Diaphora) clearly shows that the only changes in the binary are: IsPortNamedPipe and IsValidNamedPipeOrCustomPort .

Microsoft’s patch added couple checks in the code before creating a printer port:

  1. Custom ports are allowed only if their name does not contain ‘/’ or ‘\’ characters.
  2. Named pipe ports and file ports are allowed only if the user has read/write permissions on the given path.

Here explained part of the patch:

; __int64 __fastcall IsValidNamedPipeOrCustomPort(wchar_t *Str1)
IsValidNamedPipeOrCustomPort(unsigned short *) proc near
mov     [rsp+arg_0], rbx
push    rdi
sub     rsp, 40h							; create space for calls
mov     rdi, rcx
mov     rcx, cs:WPP_GLOBAL_Control
lea     rax, WPP_GLOBAL_Control
cmp     rcx, rax
jz      short loc_18002E15D
test    dword ptr [rcx+44h], 800h
jz      short loc_18002E15D
mov     rcx, [rcx+38h]
lea     r8, WPP_e632a5ce42a53acd59d64f69283b8e8d_Traceguids
mov     edx, 25h
mov     r9, rdi
call    WPP_SF_S
mov     rcx, rdi					; Str1 points to "\.\\pipe\"
call    [email protected]@[email protected]			; IsPortNamedPipe(ushort *)
xor     ebx, ebx
mov     rcx, rdi					; Str
test    eax, eax
jz      short loc_18002E1B6
mov     [rsp+48h+hTemplateFile], rbx			; hTemplateFile
xor     r9d, r9d					; lpSecurityAttributes
mov     [rsp+48h+dwFlagsAndAttributes], ebx		; dwFlagsAndAttributes
xor     r8d, r8d					; dwShareMode
mov     edx, 40000000h					; dwDesiredAccess
mov     [rsp+48h+dwCreationDisposition], 3		; dwCreationDisposition
call    cs:__imp_CreateFileW				; call CreateFileW
cmp     rax, 0FFFFFFFFFFFFFFFFh				; check if handle exist
jz      short loc_18002E1A6				; CreateFileW failed, no handle
mov     rcx, rax					; hObject
call    cs:__imp_CloseHandle
mov     eax, 1 					; return 1 (true/ok)
loc_18002e1a6:						; if no handle, we do not have permission
call    cs:__imp_GetLastError				; will result in access denied
cmp     eax, 2
setz    bl
mov     eax, ebx
mov     edx, 5Ch 					; check for '\' char
call    cs:__imp_wcschr 				; search '\' in port string
test    rax, rax 					; is '\' in port string?
jnz     short EAX_TO_ZERO				; if return 1 means it contains '\', stop checks and report err
lea     edx, [rax+2Fh]				; check for '/' char
mov     rcx, rdi					; Str
call    cs:__imp_wcschr				; search '/' in port string
test    rax, rax					; is '/' in port string?
jz      short loc_18002E19F				; if return 1 means it contains '/', stop checks and report err
xor     eax, eax					; result = 0 (false/err)
mov     rbx, [rsp+48h+arg_0]
add     rsp, 40h					; restore rsp
pop     rdi
IsValidNamedPipeOrCustomPort(unsigned short *) endp

Unfortunately, the patch has two main issues:

  1. Patch leaves the system vulnerable to pre-existing ports.
  2. Even worse, the check of user read/write permissions on the given path is performed only on port creation event.

CVE-2020-1337 – A bypass of CVE-2020-1048’s patch

Since the check only happens when creating a new port, if the user has read/write permission on that path it will pass the check, but if later, the path change, the Print Spooler service will not check it again and it will directly print to it, leading to a Time-of-check to time-of-use (TOCTOU) vulnerability.

The only way I was able to think of it, in order to match each condition, was to:

  1. Create a directory in a place where the user has write privileges (eg. %username%\Desktop\temp_dir ). Windows’ Spooler Service will allow port creation in this provided folder as the user has correct permissions over it.
  2. Create a new printer port pointing to that directory and set as filename the name of the file needed for the elevation of privileges (eg. C:\Users\user\Desktop\temp_dir\ualapi.dll ).
  3. Add a printer using the above port.
  4. Delete the created directory (eg. C:\Users\user\Desktop\temp_dir ).
  5. Re-create the directory but this time as a junction (NTFS Symbolic Link) (eg. mklink /j C:\Users\user\Desktop\dir C:\Windows\System32 ).
  6. Pause all the printing jobs for the used printer device in order to trigger the creation of Shadow Job Files.
  7. Print the binary content of the DLL to it.
  8. Reboot the system/restart Print Spooler service.
  9. Un-pause the printing jobs.
  10. ualapi.dll is now created in C:\Windows\System32\ as the Print Spooler service printed the content of it from a shadow file (who was not retaining user’s privileges) and because the port path was pointing to a junction directory.


CVE-2020-1337 is a bypass of (PrintDemon) CVE-2020-1048’s patch via a junction directory. PrintDemon’s patch was made to remediate an Elevation of Privileges (EoP)\Local Privilege Escalation (LPE) vulnerability affecting the Windows’ Print Spooler Service.

Thanks to Microsoft Security Response Center (MSRC) for the acknowledgement and CVE.

Hic sunt dracones” – VoidSec on Windows NT 4 components

Affected Systems

Disclosure Timeline

  • 18th May 2020: Issue discovery and testing.
  • 19th May 2020: Issue submitted to MSRC.
  • 19th May 2020: Acknowledgement Notification & MSRC case opened.
  • 19th – 5th May-June 2020: Triaged.
    • Severity: Important
    • Security Impact: Elevation of Privilege
  • 5th June 2020 – 10th August: Patch development and testing process.
  • 11th August 2020 Microsoft’s Patch Tuesday: patch release and published patch note.
  • 11th August 2020 CVE-2020-1337 collides with Peleg Hadar & Tomer Bar (SafeBreach), Alex Ionescu, Ziga Sumenjak & Blaz Satler (0patch) and Javi Garcia.

Share this post

Back to Posts