Merge pull request #1731 from guardicore/1695-parsing-mimikatz

1695 parsing mimikatz
This commit is contained in:
VakarisZ 2022-02-23 13:58:47 +02:00 committed by GitHub
commit 3fee7dec90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 196 additions and 320 deletions

View File

@ -0,0 +1,9 @@
from enum import Enum
class CredentialComponentType(Enum):
USERNAME = "username"
PASSWORD = "password"
NT_HASH = "nt_hash"
LM_HASH = "lm_hash"
SSH_KEYPAIR = "ssh_keypair"

View File

@ -1,9 +1,12 @@
from dataclasses import dataclass, field
from infection_monkey.i_puppet import CredentialType, ICredentialComponent
from common.common_consts.credential_component_type import CredentialComponentType
from infection_monkey.i_puppet import ICredentialComponent
@dataclass(frozen=True)
class LMHash(ICredentialComponent):
credential_type: CredentialType = field(default=CredentialType.LM_HASH, init=False)
credential_type: CredentialComponentType = field(
default=CredentialComponentType.LM_HASH.value, init=False
)
lm_hash: str

View File

@ -1,9 +1,12 @@
from dataclasses import dataclass, field
from infection_monkey.i_puppet import CredentialType, ICredentialComponent
from common.common_consts.credential_component_type import CredentialComponentType
from infection_monkey.i_puppet import ICredentialComponent
@dataclass(frozen=True)
class NTHash(ICredentialComponent):
credential_type: CredentialType = field(default=CredentialType.NT_HASH, init=False)
credential_type: CredentialComponentType = field(
default=CredentialComponentType.NT_HASH.value, init=False
)
nt_hash: str

View File

@ -1,9 +1,12 @@
from dataclasses import dataclass, field
from infection_monkey.i_puppet import CredentialType, ICredentialComponent
from common.common_consts.credential_component_type import CredentialComponentType
from infection_monkey.i_puppet import ICredentialComponent
@dataclass(frozen=True)
class Password(ICredentialComponent):
credential_type: CredentialType = field(default=CredentialType.PASSWORD, init=False)
credential_type: CredentialComponentType = field(
default=CredentialComponentType.PASSWORD.value, init=False
)
password: str

View File

@ -1,10 +1,13 @@
from dataclasses import dataclass, field
from infection_monkey.i_puppet import CredentialType, ICredentialComponent
from common.common_consts.credential_component_type import CredentialComponentType
from infection_monkey.i_puppet import ICredentialComponent
@dataclass(frozen=True)
class SSHKeypair(ICredentialComponent):
credential_type: CredentialType = field(default=CredentialType.SSH_KEYPAIR, init=False)
credential_type: CredentialComponentType = field(
default=CredentialComponentType.SSH_KEYPAIR.value, init=False
)
private_key: str
public_key: str

View File

@ -1,9 +1,12 @@
from dataclasses import dataclass, field
from infection_monkey.i_puppet import CredentialType, ICredentialComponent
from common.common_consts.credential_component_type import CredentialComponentType
from infection_monkey.i_puppet import ICredentialComponent
@dataclass(frozen=True)
class Username(ICredentialComponent):
credential_type: CredentialType = field(default=CredentialType.USERNAME, init=False)
credential_type: CredentialComponentType = field(
default=CredentialComponentType.USERNAME.value, init=False
)
username: str

View File

@ -1,3 +1,4 @@
import logging
from typing import Sequence
from infection_monkey.credential_collectors import LMHash, NTHash, Password, Username
@ -6,10 +7,15 @@ from infection_monkey.i_puppet.credential_collection import Credentials, ICreden
from . import pypykatz_handler
from .windows_credentials import WindowsCredentials
logger = logging.getLogger(__name__)
class MimikatzCredentialCollector(ICredentialCollector):
def collect_credentials(self, options=None) -> Sequence[Credentials]:
logger.info("Attempting to collect windows credentials with pypykatz.")
creds = pypykatz_handler.get_windows_creds()
logger.info(f"Pypykatz gathered {len(creds)} credentials.")
return MimikatzCredentialCollector._to_credentials(creds)
@staticmethod

View File

@ -30,7 +30,6 @@ def get_ssh_info(telemetry_messenger: ITelemetryMessenger) -> Iterable[Dict]:
def _get_home_dirs() -> Iterable[Dict]:
import pwd
root_dir = _get_ssh_struct("root", "")
home_dirs = [
_get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall() if x.pw_dir.startswith("/home")

View File

@ -1,10 +1,4 @@
from .plugin_type import PluginType
from .credential_collection import (
Credentials,
CredentialType,
ICredentialCollector,
ICredentialComponent,
)
from .i_puppet import (
IPuppet,
ExploiterResultData,
@ -16,3 +10,8 @@ from .i_puppet import (
UnknownPluginError,
)
from .i_fingerprinter import IFingerprinter
from .credential_collection import (
Credentials,
ICredentialCollector,
ICredentialComponent,
)

View File

@ -1,4 +1,3 @@
from .i_credential_collector import ICredentialCollector
from .credentials import Credentials
from .i_credential_component import ICredentialComponent
from .credential_type import CredentialType

View File

@ -1,9 +0,0 @@
from enum import Enum
class CredentialType(Enum):
USERNAME = 1
PASSWORD = 2
NT_HASH = 3
LM_HASH = 4
SSH_KEYPAIR = 5

View File

@ -1,10 +1,10 @@
from abc import ABC, abstractmethod
from .credential_type import CredentialType
from common.common_consts.credential_component_type import CredentialComponentType
class ICredentialComponent(ABC):
@property
@abstractmethod
def credential_type(self) -> CredentialType:
def credential_type(self) -> CredentialComponentType:
pass

View File

@ -4,7 +4,8 @@ from collections import namedtuple
from enum import Enum
from typing import Dict, List, Sequence
from . import Credentials, PluginType
from . import PluginType
from .credential_collection import Credentials
class PortStatus(Enum):

View File

@ -17,6 +17,9 @@ class CredentialsTelem(BaseTelem):
"""
self._credentials = credentials
def send(self, log_data=True):
super().send(log_data=False)
def get_data(self) -> Dict:
# TODO: At a later time we can consider factoring this into a Serializer class or similar.
return json.loads(json.dumps(self._credentials, default=_serialize))

View File

@ -0,0 +1,29 @@
import logging
from typing import Mapping
from common.common_consts.credential_component_type import CredentialComponentType
from .identities.username_processor import process_username
from .secrets.lm_hash_processor import process_lm_hash
from .secrets.nt_hash_processor import process_nt_hash
from .secrets.password_processor import process_password
logger = logging.getLogger(__name__)
SECRET_PROCESSORS = {
CredentialComponentType.PASSWORD.value: process_password,
CredentialComponentType.NT_HASH.value: process_nt_hash,
CredentialComponentType.LM_HASH.value: process_lm_hash,
}
IDENTITY_PROCESSORS = {
CredentialComponentType.USERNAME.value: process_username,
}
def parse_credentials(credentials: Mapping):
for credential in credentials["data"]:
for identity in credential["identities"]:
IDENTITY_PROCESSORS[identity["credential_type"]](identity)
for secret in credential["secrets"]:
SECRET_PROCESSORS[secret["credential_type"]](secret)

View File

@ -0,0 +1,5 @@
from monkey_island.cc.services.config import ConfigService
def process_username(username: dict):
ConfigService.creds_add_username(username["username"])

View File

@ -0,0 +1,5 @@
from monkey_island.cc.services.config import ConfigService
def process_lm_hash(lm_hash: dict):
ConfigService.creds_add_lm_hash(lm_hash["lm_hash"])

View File

@ -0,0 +1,5 @@
from monkey_island.cc.services.config import ConfigService
def process_nt_hash(nt_hash: dict):
ConfigService.creds_add_ntlm_hash(nt_hash["nt_hash"])

View File

@ -0,0 +1,5 @@
from monkey_island.cc.services.config import ConfigService
def process_password(password: dict):
ConfigService.creds_add_password(password["password"])

View File

@ -2,21 +2,22 @@ import logging
from common.common_consts.telem_categories import TelemCategoryEnum
from monkey_island.cc.services.telemetry.processing.aws_info import process_aws_telemetry
from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import\
parse_credentials
from monkey_island.cc.services.telemetry.processing.exploit import process_exploit_telemetry
from monkey_island.cc.services.telemetry.processing.post_breach import process_post_breach_telemetry
from monkey_island.cc.services.telemetry.processing.scan import process_scan_telemetry
from monkey_island.cc.services.telemetry.processing.state import process_state_telemetry
from monkey_island.cc.services.telemetry.processing.system_info import process_system_info_telemetry
from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel_telemetry
logger = logging.getLogger(__name__)
TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = {
TelemCategoryEnum.CREDENTIALS: parse_credentials,
TelemCategoryEnum.TUNNEL: process_tunnel_telemetry,
TelemCategoryEnum.STATE: process_state_telemetry,
TelemCategoryEnum.EXPLOIT: process_exploit_telemetry,
TelemCategoryEnum.SCAN: process_scan_telemetry,
TelemCategoryEnum.SYSTEM_INFO: process_system_info_telemetry,
TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry,
TelemCategoryEnum.AWS_INFO: process_aws_telemetry,
# `lambda *args, **kwargs: None` is a no-op.

View File

@ -1,107 +0,0 @@
import logging
from monkey_island.cc.server_utils.encryption import get_datastore_encryptor
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501
SystemInfoTelemetryDispatcher,
)
from monkey_island.cc.services.wmi_handler import WMIHandler
logger = logging.getLogger(__name__)
def process_system_info_telemetry(telemetry_json):
dispatcher = SystemInfoTelemetryDispatcher()
telemetry_processing_stages = [
process_ssh_info,
process_credential_info,
process_wmi_info,
dispatcher.dispatch_collector_results_to_relevant_processors,
]
# Calling safe_process_telemetry so if one of the stages fail, we log and move on instead of
# failing the rest of
# them, as they are independent.
for stage in telemetry_processing_stages:
safe_process_telemetry(stage, telemetry_json)
def safe_process_telemetry(processing_function, telemetry_json):
# noinspection PyBroadException
try:
processing_function(telemetry_json)
except Exception as err:
logger.error(
"Error {} while in {} stage of processing telemetry.".format(
str(err), processing_function.__name__
),
exc_info=True,
)
def process_ssh_info(telemetry_json):
if "ssh_info" in telemetry_json["data"]:
ssh_info = telemetry_json["data"]["ssh_info"]
encrypt_system_info_ssh_keys(ssh_info)
if telemetry_json["data"]["network_info"]["networks"]:
# We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip
# from telemetry
add_ip_to_ssh_keys(telemetry_json["data"]["network_info"]["networks"][0], ssh_info)
add_system_info_ssh_keys_to_config(ssh_info)
def add_system_info_ssh_keys_to_config(ssh_info):
for user in ssh_info:
ConfigService.creds_add_username(user["name"])
# Public key is useless without private key
if user["public_key"] and user["private_key"]:
ConfigService.ssh_add_keys(
user["public_key"], user["private_key"], user["name"], user["ip"]
)
def add_ip_to_ssh_keys(ip, ssh_info):
for key in ssh_info:
key["ip"] = ip["addr"]
def encrypt_system_info_ssh_keys(ssh_info):
for idx, user in enumerate(ssh_info):
for field in ["public_key", "private_key", "known_hosts"]:
if ssh_info[idx][field]:
ssh_info[idx][field] = get_datastore_encryptor().encrypt(ssh_info[idx][field])
def process_credential_info(telemetry_json):
if "credentials" in telemetry_json["data"]:
creds = telemetry_json["data"]["credentials"]
add_system_info_creds_to_config(creds)
replace_user_dot_with_comma(creds)
def replace_user_dot_with_comma(creds):
for user in creds:
if -1 != user.find("."):
new_user = user.replace(".", ",")
creds[new_user] = creds.pop(user)
def add_system_info_creds_to_config(creds):
for user in creds:
ConfigService.creds_add_username(creds[user]["username"])
if "password" in creds[user] and creds[user]["password"]:
ConfigService.creds_add_password(creds[user]["password"])
if "lm_hash" in creds[user] and creds[user]["lm_hash"]:
ConfigService.creds_add_lm_hash(creds[user]["lm_hash"])
if "ntlm_hash" in creds[user] and creds[user]["ntlm_hash"]:
ConfigService.creds_add_ntlm_hash(creds[user]["ntlm_hash"])
def process_wmi_info(telemetry_json):
users_secrets = {}
if "wmi" in telemetry_json["data"]:
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.process_and_handle_wmi_info()

View File

@ -1,181 +0,0 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.groups_and_users_consts import GROUPTYPE, USERTYPE
class WMIHandler(object):
ADMINISTRATORS_GROUP_KNOWN_SID = "1-5-32-544"
def __init__(self, monkey_id, wmi_info, user_secrets):
self.monkey_id = monkey_id
self.info_for_mongo = {}
self.users_secrets = user_secrets
if not wmi_info:
self.users_info = ""
self.groups_info = ""
self.groups_and_users = ""
self.services = ""
self.products = ""
else:
self.users_info = wmi_info["Win32_UserAccount"]
self.groups_info = wmi_info["Win32_Group"]
self.groups_and_users = wmi_info["Win32_GroupUser"]
self.services = wmi_info["Win32_Service"]
self.products = wmi_info["Win32_Product"]
def process_and_handle_wmi_info(self):
self.add_groups_to_collection()
self.add_users_to_collection()
self.create_group_user_connection()
self.insert_info_to_mongo()
if self.info_for_mongo:
self.add_admin(self.info_for_mongo[self.ADMINISTRATORS_GROUP_KNOWN_SID], self.monkey_id)
self.update_admins_retrospective()
self.update_critical_services()
def update_critical_services(self):
critical_names = ("W3svc", "MSExchangeServiceHost", "dns", "MSSQL$SQLEXPRES")
mongo.db.monkey.update({"_id": self.monkey_id}, {"$set": {"critical_services": []}})
services_names_list = [str(i["Name"])[2:-1] for i in self.services]
products_names_list = [str(i["Name"])[2:-2] for i in self.products]
for name in critical_names:
if name in services_names_list or name in products_names_list:
mongo.db.monkey.update(
{"_id": self.monkey_id}, {"$addToSet": {"critical_services": name}}
)
def build_entity_document(self, entity_info, monkey_id=None):
general_properties_dict = {
"SID": str(entity_info["SID"])[4:-1],
"name": str(entity_info["Name"])[2:-1],
"machine_id": monkey_id,
"member_of": [],
"admin_on_machines": [],
}
if monkey_id:
general_properties_dict["domain_name"] = None
else:
general_properties_dict["domain_name"] = str(entity_info["Domain"])[2:-1]
return general_properties_dict
def add_users_to_collection(self):
for user in self.users_info:
if not user.get("LocalAccount"):
base_entity = self.build_entity_document(user)
else:
base_entity = self.build_entity_document(user, self.monkey_id)
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["secret_location"] = []
base_entity["type"] = USERTYPE
self.info_for_mongo[base_entity.get("SID")] = base_entity
def add_groups_to_collection(self):
for group in self.groups_info:
if not group.get("LocalAccount"):
base_entity = self.build_entity_document(group)
else:
base_entity = self.build_entity_document(group, self.monkey_id)
base_entity["entities_list"] = []
base_entity["type"] = GROUPTYPE
self.info_for_mongo[base_entity.get("SID")] = base_entity
def create_group_user_connection(self):
for group_user_couple in self.groups_and_users:
group_part = group_user_couple["GroupComponent"]
child_part = group_user_couple["PartComponent"]
group_sid = str(group_part["SID"])[4:-1]
groups_entities_list = self.info_for_mongo[group_sid]["entities_list"]
child_sid = ""
if isinstance(child_part, str):
child_part = str(child_part)
name = None
domain_name = None
if "cimv2:Win32_UserAccount" in child_part:
# domain user
domain_name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split(
'",Name="'
)[0]
name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split(
'",Name="'
)[1][:-2]
if "cimv2:Win32_Group" in child_part:
# domain group
domain_name = child_part.split('cimv2:Win32_Group.Domain="')[1].split(
'",Name="'
)[0]
name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[1][
:-2
]
for entity in self.info_for_mongo:
if (
self.info_for_mongo[entity]["name"] == name
and self.info_for_mongo[entity]["domain"] == domain_name
):
child_sid = self.info_for_mongo[entity]["SID"]
else:
child_sid = str(child_part["SID"])[4:-1]
if child_sid and child_sid not in groups_entities_list:
groups_entities_list.append(child_sid)
if child_sid:
if child_sid in self.info_for_mongo:
self.info_for_mongo[child_sid]["member_of"].append(group_sid)
def insert_info_to_mongo(self):
for entity in list(self.info_for_mongo.values()):
if entity["machine_id"]:
# Handling for local entities.
mongo.db.groupsandusers.update(
{"SID": entity["SID"], "machine_id": entity["machine_id"]}, entity, upsert=True
)
else:
# Handlings for domain entities.
if not mongo.db.groupsandusers.find_one({"SID": entity["SID"]}):
mongo.db.groupsandusers.insert_one(entity)
else:
# if entity is domain entity, add the monkey id of current machine to
# secrets_location.
# (found on this machine)
if entity.get("NTLM_secret"):
mongo.db.groupsandusers.update_one(
{"SID": entity["SID"], "type": USERTYPE},
{"$addToSet": {"secret_location": self.monkey_id}},
)
def update_admins_retrospective(self):
for profile in self.info_for_mongo:
groups_from_mongo = mongo.db.groupsandusers.find(
{"SID": {"$in": self.info_for_mongo[profile]["member_of"]}},
{"admin_on_machines": 1},
)
for group in groups_from_mongo:
if group["admin_on_machines"]:
mongo.db.groupsandusers.update_one(
{"SID": self.info_for_mongo[profile]["SID"]},
{"$addToSet": {"admin_on_machines": {"$each": group["admin_on_machines"]}}},
)
def add_admin(self, group, machine_id):
for sid in group["entities_list"]:
mongo.db.groupsandusers.update_one(
{"SID": sid}, {"$addToSet": {"admin_on_machines": machine_id}}
)
entity_details = mongo.db.groupsandusers.find_one(
{"SID": sid}, {"type": USERTYPE, "entities_list": 1}
)
if entity_details.get("type") == GROUPTYPE:
self.add_admin(entity_details, machine_id)

View File

@ -0,0 +1,92 @@
from copy import deepcopy
from datetime import datetime
import dpath.util
import mongoengine
import pytest
from common.config_value_paths import (
LM_HASH_LIST_PATH,
NTLM_HASH_LIST_PATH,
PASSWORD_LIST_PATH,
USER_LIST_PATH,
)
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import (
parse_credentials,
)
CREDENTIAL_TELEM_TEMPLATE = {
"monkey_guid": "272405690278083",
"telem_category": "credentials",
"timestamp": datetime(2022, 2, 18, 11, 51, 15, 338953),
"command_control_channel": {"src": "10.2.2.251", "dst": "10.2.2.251:5000"},
"data": None,
}
fake_username = "m0nk3y_user"
cred_telem_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE)
cred_telem_usernames["data"] = [
{"identities": [{"username": fake_username, "credential_type": "username"}], "secrets": []}
]
fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c"
fake_lm_hash = "299BD128C1101FD6"
fake_password = "trytostealthis"
cred_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE)
cred_telem["data"] = [
{
"identities": [{"username": fake_username, "credential_type": "username"}],
"secrets": [
{"nt_hash": fake_nt_hash, "credential_type": "nt_hash"},
{"lm_hash": fake_lm_hash, "credential_type": "lm_hash"},
{"password": fake_password, "credential_type": "password"},
],
}
]
cred_empty_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE)
cred_empty_telem["data"] = [{"identities": [], "secrets": []}]
@pytest.fixture
def fake_mongo(monkeypatch):
mongo = mongoengine.connection.get_connection()
monkeypatch.setattr("monkey_island.cc.services.config.mongo", mongo)
config = ConfigService.get_default_config()
ConfigService.update_config(config, should_encrypt=True)
return mongo
@pytest.mark.usefixtures("uses_database")
def test_cred_username_parsing(fake_mongo):
parse_credentials(cred_telem_usernames)
config = ConfigService.get_config(should_decrypt=True)
assert fake_username in dpath.util.get(config, USER_LIST_PATH)
@pytest.mark.usefixtures("uses_database")
def test_cred_telemetry_parsing(fake_mongo):
parse_credentials(cred_telem)
config = ConfigService.get_config(should_decrypt=True)
assert fake_username in dpath.util.get(config, USER_LIST_PATH)
assert fake_nt_hash in dpath.util.get(config, NTLM_HASH_LIST_PATH)
assert fake_lm_hash in dpath.util.get(config, LM_HASH_LIST_PATH)
assert fake_password in dpath.util.get(config, PASSWORD_LIST_PATH)
@pytest.mark.usefixtures("uses_database")
def test_empty_cred_telemetry_parsing(fake_mongo):
default_config = deepcopy(ConfigService.get_config(should_decrypt=True))
default_usernames = dpath.util.get(default_config, USER_LIST_PATH)
default_nt_hashes = dpath.util.get(default_config, NTLM_HASH_LIST_PATH)
default_lm_hashes = dpath.util.get(default_config, LM_HASH_LIST_PATH)
default_passwords = dpath.util.get(default_config, PASSWORD_LIST_PATH)
parse_credentials(cred_empty_telem)
config = ConfigService.get_config(should_decrypt=True)
assert default_usernames == dpath.util.get(config, USER_LIST_PATH)
assert default_nt_hashes == dpath.util.get(config, NTLM_HASH_LIST_PATH)
assert default_lm_hashes == dpath.util.get(config, LM_HASH_LIST_PATH)
assert default_passwords == dpath.util.get(config, PASSWORD_LIST_PATH)