This is going to be a bit of a weird post, because this priv esc exploit isn’t actually a successful exploit yet if you’re attacking a server OS – unless you can get into the Print Operators group. It works perfectly well on a workstation OS though so still worth bringing to people’s attention.
- A few Ricoh printer driver DLLs have “Everyone – Full Control” permissions by default
- When a new printer is added using this driver, the print spooler service (which runs as Local System) will load these DLLs
- Due to the bad permissons on the DLLs, any user can replace one of them with a malicious DLL. Then they can add a new local printer using the existing driver and at this point the malicious DLL will get loaded by the print spooler service. So now you’ve got your code being executed as Local System
- Regular users have permission to add new printers as long as they use a driver that is already installed (so this exploit only works if one of the vulnerable drivers is already installed, as regular users cannot install new drivers)
So if you’re attacking a workstation, you’re good. There’s even a metasploit module that will do all the work for you as usual.
For a server OS though, there’s a problem…
I tried to use this on a Windows Server 2019 machine that had the vulnerable driver installed and it failed. After some testing I discovered that this is because users do not have permission to add printers on a server, even if the driver is already installed. Recall from the bullet points above that we need to add a printer to trigger the print spooler service to load our malicious DLL.
In fact on a server OS, a regular user can’t even run the Powershell command Get-Printer to see a list of printers installed on the system:
The first thing I noticed in both the POC and the metasploit module code is that neither of them call the Windows AddPrinter API directly. The metasploit code calls the built in prnmngr.vbs script (which internally uses WMI) and the POC calls the PrintUIEntry function with rundll32.exe.
So just to make sure this permissions issue was not being caused by something in any of those scripts, I wrote a program that will call the AddPrinter API directly. I also made it call the EnumPrinters API to see if we can at least list existing printers, as even that fails with access denied when tried from Powershell as a non admin user.
Nothing crazy but for what its worth, the full code can be found on my github here: https://github.com/VbScrub/VbAddPrinter
Both of these operations worked perfectly fine as a regular user on my Windows 10 machine. So I moved them over to the target machine which is running Windows Server 2019 but there adding a printer fails with Access Denied:
The enum part of my program does succeed though, so at least we bypassed the Get-Printer access denied issue by calling the EnumPrinters API directly:
I noticed if we replace the original driver’s watermark.dll with a malicious DLL and then do anything that would cause it be accessed, the genuine DLL gets instantly copied back to the directory and overwrites our malicious DLL. This must be why in the original POC they are monitoring the file system for changes to detect when the original DLL gets copied back and then they quickly replace it again with the malicious DLL before it actually gets loaded by the print spooler service. I’ve implemented something similar using the .NET FileSystemWatcher class:
This worked as expected and now our malicious DLL always ends up being loaded instead of the original DLL (although making the program copy the DLL a few hundred times to make sure it sticks was maybe a bit overkill).
So now we have our malicious DLL in place, the problem is that we can only get it to be loaded by a process running as our regular user and not a system service. For example when we try to print to the printer from notepad or access the printer properties etc, our malicious DLL does get loaded but it is loaded by the notepad.exe process which is obviously running as our regular user and not Local System. So it seems like the only time the print spooler service loads these DLLs is when adding a new printer.
One other thing I noticed was that when we fail to add a new printer as a normal user, we see this in the Process Monitor stack trace:
When we do the same thing but as an admin, we don’t see this “AddPrinterCompletedInProc” function and instead we see a load of RPC calls. So I tried using the Samba RPC Client to call the AddPrinter RPC function directly (in the hopes that maybe we bypass the permissions check that is causing it to fail before it tries the RPC stuff), but this also fails with access denied:
I assumed the print spooler service would load all of the printer DLLs when it starts up, so perhaps we could crash the service and then when it auto restarts it would load our DLL. Unfortunately when I restarted the service I found it did not load any of these DLLs. To me it seems quite strange that it even loads the watermark DLL at all when the user adds a printer, because the print spooler service itself doesn’t seem to actually need it (otherwise it would load it itself when a user prints, or when the service starts).
One final weird observation is that in my tests I could only get the attack to work even with the user in the Print Operators group if the driver was set to run in isolated mode. This makes the print spooler service start a separate PrintIsolationHost.exe process when the driver is used and it is that process that loads the watermark.dll when we add a printer. No such issues on a workstation though (which is good as there’s no easy way to specify driver isolation there AFAIK)
In summary: Whilst this works perfectly on a Windows 10 machine, I couldn’t get it to work on a server OS as a regular user unless they were a member of the Print Operators group. Hopefully someone else can take my findings and finish the job 🙂