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).
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 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:
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
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!
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:
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’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:
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:
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:
%username%\Desktop\temp_dir
). Windows’ Spooler Service will allow port creation in this provided folder as the user has correct permissions over it.C:\Users\user\Desktop\temp_dir\ualapi.dll
).C:\Users\user\Desktop\temp_dir
).mklink /j C:\Users\user\Desktop\dir C:\Windows\System32
).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