CVE-2020-1337 – PrintDemon is dead, long live PrintDemon!
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).
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”.
Table of Contents
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.
PrinterPort
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.
WritePrinter
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
option “pDatatype
” to “RAW
”.
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:
- Restart Windows’ Print Spooler service (which requires administrative privileges).
- 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:
- Custom ports are allowed only if their name does not contain ‘/’ or ‘\’ characters.
- 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 loc_18002e13c: test dword ptr [rcx+44h], 800h jz short loc_18002E15D loc_18002e145: mov rcx, [rcx+38h] lea r8, WPP_e632a5ce42a53acd59d64f69283b8e8d_Traceguids mov edx, 25h mov r9, rdi call WPP_SF_S loc_18002e15d: mov rcx, rdi ; Str1 points to "\.\\pipe\" call ?IsPortNamedPipe@@YAHPEAG@Z ; IsPortNamedPipe(ushort *) xor ebx, ebx mov rcx, rdi ; Str test eax, eax jz short loc_18002E1B6 loc_18002e16e: 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 loc_18002e196: mov rcx, rax ; hObject call cs:__imp_CloseHandle loc_18002e19f: mov eax, 1 ; return 1 (true/ok) jmp short END_OF_PIPEORCUSTOM 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 jmp short END_OF_PIPEORCUSTOM loc_18002e1b6: 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 loc_18002e1c6: 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 EAX_TO_ZERO: xor eax, eax ; result = 0 (false/err) END_OF_PIPEORCUSTOM: mov rbx, [rsp+48h+arg_0] add rsp, 40h ; restore rsp pop rdi retn IsValidNamedPipeOrCustomPort(unsigned short *) endp
Unfortunately, the patch has two main issues:
- Patch leaves the system vulnerable to pre-existing ports.
- 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:
- 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. - 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
). - Add a printer using the above port.
- Delete the created directory (eg.
C:\Users\user\Desktop\temp_dir
). - Re-create the directory but this time as a junction (NTFS Symbolic Link) (eg.
mklink /j C:\Users\user\Desktop\dir C:\Windows\System32
). - Pause all the printing jobs for the used printer device in order to trigger the creation of Shadow Job Files.
- Print the binary content of the DLL to it.
- Reboot the system/restart Print Spooler service.
- Un-pause the printing jobs.
ualapi.dll
is now created inC:\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.
Conclusion
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.