Back to Posts

Share this post

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

Posted by: voidsec

Reading Time: 8 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 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 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\SYSTEMrights.

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
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:

  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.dllis 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.

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.
Back to Posts