Merge pull request #1734 from guardicore/1695-parsing-ssh-keys
1695 ssh keys processing
This commit is contained in:
commit
48e8420b4d
|
@ -0,0 +1 @@
|
||||||
|
from .credentials import Credentials
|
|
@ -0,0 +1,19 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Sequence, Mapping, Any
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Credentials:
|
||||||
|
identities: Sequence[Mapping]
|
||||||
|
secrets: Sequence[Mapping]
|
||||||
|
monkey_guid: str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_mapping(cred_dict: Mapping[str, Any], monkey_guid: str) -> Credentials:
|
||||||
|
return Credentials(
|
||||||
|
identities=cred_dict["identities"],
|
||||||
|
secrets=cred_dict["secrets"],
|
||||||
|
monkey_guid=monkey_guid,
|
||||||
|
)
|
|
@ -1,31 +1,34 @@
|
||||||
import logging
|
import logging
|
||||||
|
from itertools import chain
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
|
||||||
from common.common_consts.credential_component_type import CredentialComponentType
|
from common.common_consts.credential_component_type import CredentialComponentType
|
||||||
|
|
||||||
|
from .credentials import Credentials
|
||||||
from .identities.username_processor import process_username
|
from .identities.username_processor import process_username
|
||||||
from .secrets.lm_hash_processor import process_lm_hash
|
from .secrets.lm_hash_processor import process_lm_hash
|
||||||
from .secrets.nt_hash_processor import process_nt_hash
|
from .secrets.nt_hash_processor import process_nt_hash
|
||||||
from .secrets.password_processor import process_password
|
from .secrets.password_processor import process_password
|
||||||
|
from .secrets.ssh_key_processor import process_ssh_key
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SECRET_PROCESSORS = {
|
CREDENTIAL_COMPONENT_PROCESSORS = {
|
||||||
CredentialComponentType.PASSWORD: process_password,
|
|
||||||
CredentialComponentType.NT_HASH: process_nt_hash,
|
|
||||||
CredentialComponentType.LM_HASH: process_lm_hash,
|
CredentialComponentType.LM_HASH: process_lm_hash,
|
||||||
}
|
CredentialComponentType.NT_HASH: process_nt_hash,
|
||||||
|
CredentialComponentType.PASSWORD: process_password,
|
||||||
IDENTITY_PROCESSORS = {
|
CredentialComponentType.SSH_KEYPAIR: process_ssh_key,
|
||||||
CredentialComponentType.USERNAME: process_username,
|
CredentialComponentType.USERNAME: process_username,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_credentials(credentials: Mapping):
|
def parse_credentials(telemetry_dict: Mapping):
|
||||||
for credential in credentials["data"]:
|
credentials = [
|
||||||
for identity in credential["identities"]:
|
Credentials.from_mapping(credential, telemetry_dict["monkey_guid"])
|
||||||
credential_type = CredentialComponentType[identity["credential_type"]]
|
for credential in telemetry_dict["data"]
|
||||||
IDENTITY_PROCESSORS[credential_type](identity)
|
]
|
||||||
for secret in credential["secrets"]:
|
|
||||||
credential_type = CredentialComponentType[secret["credential_type"]]
|
for credential in credentials:
|
||||||
SECRET_PROCESSORS[credential_type](secret)
|
for cred_comp in chain(credential.identities, credential.secrets):
|
||||||
|
credential_type = CredentialComponentType[cred_comp["credential_type"]]
|
||||||
|
CREDENTIAL_COMPONENT_PROCESSORS[credential_type](cred_comp, credential)
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
from monkey_island.cc.services.config import ConfigService
|
from monkey_island.cc.services.config import ConfigService
|
||||||
|
from monkey_island.cc.services.telemetry.processing.credentials import Credentials
|
||||||
|
|
||||||
|
|
||||||
def process_username(username: dict):
|
def process_username(username: Mapping, _: Credentials):
|
||||||
ConfigService.creds_add_username(username["username"])
|
ConfigService.creds_add_username(username["username"])
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
from monkey_island.cc.services.config import ConfigService
|
from monkey_island.cc.services.config import ConfigService
|
||||||
|
from monkey_island.cc.services.telemetry.processing.credentials import Credentials
|
||||||
|
|
||||||
|
|
||||||
def process_lm_hash(lm_hash: dict):
|
def process_lm_hash(lm_hash: Mapping, _: Credentials):
|
||||||
ConfigService.creds_add_lm_hash(lm_hash["lm_hash"])
|
ConfigService.creds_add_lm_hash(lm_hash["lm_hash"])
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
from monkey_island.cc.services.config import ConfigService
|
from monkey_island.cc.services.config import ConfigService
|
||||||
|
from monkey_island.cc.services.telemetry.processing.credentials import Credentials
|
||||||
|
|
||||||
|
|
||||||
def process_nt_hash(nt_hash: dict):
|
def process_nt_hash(nt_hash: Mapping, _: Credentials):
|
||||||
ConfigService.creds_add_ntlm_hash(nt_hash["nt_hash"])
|
ConfigService.creds_add_ntlm_hash(nt_hash["nt_hash"])
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
from monkey_island.cc.services.config import ConfigService
|
from monkey_island.cc.services.config import ConfigService
|
||||||
|
from monkey_island.cc.services.telemetry.processing.credentials import Credentials
|
||||||
|
|
||||||
|
|
||||||
def process_password(password: dict):
|
def process_password(password: Mapping, _: Credentials):
|
||||||
ConfigService.creds_add_password(password["password"])
|
ConfigService.creds_add_password(password["password"])
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
from monkey_island.cc.models import Monkey
|
||||||
|
from monkey_island.cc.server_utils.encryption import get_datastore_encryptor
|
||||||
|
from monkey_island.cc.services.config import ConfigService
|
||||||
|
from monkey_island.cc.services.telemetry.processing.credentials import Credentials
|
||||||
|
|
||||||
|
|
||||||
|
class SSHKeyProcessingError(ValueError):
|
||||||
|
def __init__(self, msg=""):
|
||||||
|
self.msg = f"Error while processing ssh keypair: {msg}"
|
||||||
|
super().__init__(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
def process_ssh_key(keypair: Mapping, credentials: Credentials):
|
||||||
|
if len(credentials.identities) != 1:
|
||||||
|
raise SSHKeyProcessingError(
|
||||||
|
f"SSH credentials have {len(credentials.identities)} users associated with it!"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not _contains_both_keys(keypair):
|
||||||
|
raise SSHKeyProcessingError("Private or public key missing")
|
||||||
|
|
||||||
|
# TODO investigate if IP is needed at all
|
||||||
|
ip = Monkey.get_single_monkey_by_guid(credentials.monkey_guid).ip_addresses[0]
|
||||||
|
username = credentials.identities[0]["username"]
|
||||||
|
|
||||||
|
encrypted_keys = _encrypt_ssh_keys(keypair)
|
||||||
|
|
||||||
|
ConfigService.ssh_add_keys(
|
||||||
|
user=username,
|
||||||
|
public_key=encrypted_keys["public_key"],
|
||||||
|
private_key=encrypted_keys["private_key"],
|
||||||
|
ip=ip,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _contains_both_keys(ssh_key: Mapping) -> bool:
|
||||||
|
try:
|
||||||
|
return ssh_key["public_key"] and ssh_key["private_key"]
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _encrypt_ssh_keys(ssh_key: Mapping) -> Mapping:
|
||||||
|
encrypted_keys = {}
|
||||||
|
for field in ["public_key", "private_key"]:
|
||||||
|
encrypted_keys[field] = get_datastore_encryptor().encrypt(ssh_key[field])
|
||||||
|
return encrypted_keys
|
|
@ -13,16 +13,16 @@ from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = {
|
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.POST_BREACH: process_post_breach_telemetry,
|
|
||||||
TelemCategoryEnum.AWS_INFO: process_aws_telemetry,
|
|
||||||
# `lambda *args, **kwargs: None` is a no-op.
|
# `lambda *args, **kwargs: None` is a no-op.
|
||||||
TelemCategoryEnum.TRACE: lambda *args, **kwargs: None,
|
|
||||||
TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None,
|
TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None,
|
||||||
|
TelemCategoryEnum.AWS_INFO: process_aws_telemetry,
|
||||||
|
TelemCategoryEnum.CREDENTIALS: parse_credentials,
|
||||||
|
TelemCategoryEnum.EXPLOIT: process_exploit_telemetry,
|
||||||
|
TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry,
|
||||||
|
TelemCategoryEnum.SCAN: process_scan_telemetry,
|
||||||
|
TelemCategoryEnum.STATE: process_state_telemetry,
|
||||||
|
TelemCategoryEnum.TRACE: lambda *args, **kwargs: None,
|
||||||
|
TelemCategoryEnum.TUNNEL: process_tunnel_telemetry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import mongoengine
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from monkey_island.cc.services.config import ConfigService
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import dpath.util
|
import dpath.util
|
||||||
import mongoengine
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from tests.unit_tests.monkey_island.cc.services.telemetry.processing.credentials.conftest import (
|
||||||
|
CREDENTIAL_TELEM_TEMPLATE,
|
||||||
|
)
|
||||||
|
|
||||||
from common.config_value_paths import (
|
from common.config_value_paths import (
|
||||||
LM_HASH_LIST_PATH,
|
LM_HASH_LIST_PATH,
|
||||||
|
@ -16,20 +17,21 @@ from monkey_island.cc.services.telemetry.processing.credentials.credentials_pars
|
||||||
parse_credentials,
|
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"
|
fake_username = "m0nk3y_user"
|
||||||
cred_telem_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE)
|
cred_telem_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE)
|
||||||
cred_telem_usernames["data"] = [
|
cred_telem_usernames["data"] = [
|
||||||
{"identities": [{"username": fake_username, "credential_type": "USERNAME"}], "secrets": []}
|
{"identities": [{"username": fake_username, "credential_type": "USERNAME"}], "secrets": []}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
fake_special_username = "$m0nk3y.user"
|
||||||
|
cred_telem_special_usernames = deepcopy(CREDENTIAL_TELEM_TEMPLATE)
|
||||||
|
cred_telem_special_usernames["data"] = [
|
||||||
|
{
|
||||||
|
"identities": [{"username": fake_special_username, "credential_type": "USERNAME"}],
|
||||||
|
"secrets": [],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c"
|
fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c"
|
||||||
fake_lm_hash = "299BD128C1101FD6"
|
fake_lm_hash = "299BD128C1101FD6"
|
||||||
fake_password = "trytostealthis"
|
fake_password = "trytostealthis"
|
||||||
|
@ -49,24 +51,22 @@ cred_empty_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE)
|
||||||
cred_empty_telem["data"] = [{"identities": [], "secrets": []}]
|
cred_empty_telem["data"] = [{"identities": [], "secrets": []}]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.mark.usefixtures("uses_database", "fake_mongo")
|
||||||
def fake_mongo(monkeypatch):
|
def test_cred_username_parsing():
|
||||||
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)
|
parse_credentials(cred_telem_usernames)
|
||||||
config = ConfigService.get_config(should_decrypt=True)
|
config = ConfigService.get_config(should_decrypt=True)
|
||||||
assert fake_username in dpath.util.get(config, USER_LIST_PATH)
|
assert fake_username in dpath.util.get(config, USER_LIST_PATH)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("uses_database")
|
@pytest.mark.usefixtures("uses_database", "fake_mongo")
|
||||||
def test_cred_telemetry_parsing(fake_mongo):
|
def test_cred_special_username_parsing():
|
||||||
|
parse_credentials(cred_telem_special_usernames)
|
||||||
|
config = ConfigService.get_config(should_decrypt=True)
|
||||||
|
assert fake_special_username in dpath.util.get(config, USER_LIST_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("uses_database", "fake_mongo")
|
||||||
|
def test_cred_telemetry_parsing():
|
||||||
parse_credentials(cred_telem)
|
parse_credentials(cred_telem)
|
||||||
config = ConfigService.get_config(should_decrypt=True)
|
config = ConfigService.get_config(should_decrypt=True)
|
||||||
assert fake_username in dpath.util.get(config, USER_LIST_PATH)
|
assert fake_username in dpath.util.get(config, USER_LIST_PATH)
|
||||||
|
@ -75,8 +75,8 @@ def test_cred_telemetry_parsing(fake_mongo):
|
||||||
assert fake_password in dpath.util.get(config, PASSWORD_LIST_PATH)
|
assert fake_password in dpath.util.get(config, PASSWORD_LIST_PATH)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("uses_database")
|
@pytest.mark.usefixtures("uses_database", "fake_mongo")
|
||||||
def test_empty_cred_telemetry_parsing(fake_mongo):
|
def test_empty_cred_telemetry_parsing():
|
||||||
default_config = deepcopy(ConfigService.get_config(should_decrypt=True))
|
default_config = deepcopy(ConfigService.get_config(should_decrypt=True))
|
||||||
default_usernames = dpath.util.get(default_config, USER_LIST_PATH)
|
default_usernames = dpath.util.get(default_config, USER_LIST_PATH)
|
||||||
default_nt_hashes = dpath.util.get(default_config, NTLM_HASH_LIST_PATH)
|
default_nt_hashes = dpath.util.get(default_config, NTLM_HASH_LIST_PATH)
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
import dpath.util
|
||||||
|
import pytest
|
||||||
|
from tests.unit_tests.monkey_island.cc.services.telemetry.processing.credentials.conftest import (
|
||||||
|
CREDENTIAL_TELEM_TEMPLATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
from common.config_value_paths import SSH_KEYS_PATH, USER_LIST_PATH
|
||||||
|
from monkey_island.cc.models import Monkey
|
||||||
|
from monkey_island.cc.services.config import ConfigService
|
||||||
|
from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import (
|
||||||
|
parse_credentials,
|
||||||
|
)
|
||||||
|
|
||||||
|
fake_monkey_guid = "272405690278083"
|
||||||
|
fake_ip_address = "192.168.56.1"
|
||||||
|
|
||||||
|
fake_private_key = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1N\n"
|
||||||
|
fake_partial_secret = {"private_key": fake_private_key, "credential_type": "SSH_KEYPAIR"}
|
||||||
|
|
||||||
|
fake_username = "ubuntu"
|
||||||
|
fake_public_key = (
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1u2+50OFRnzOGHpWo69"
|
||||||
|
"tc02oMXudeML7pOl7rqXLmdxuj monkey@krk-wpas5"
|
||||||
|
)
|
||||||
|
fake_secret_full = {
|
||||||
|
"private_key": fake_private_key,
|
||||||
|
"public_key": fake_public_key,
|
||||||
|
"credential_type": "SSH_KEYPAIR",
|
||||||
|
}
|
||||||
|
fake_identity = {"username": fake_username, "credential_type": "USERNAME"}
|
||||||
|
|
||||||
|
ssh_telem = deepcopy(CREDENTIAL_TELEM_TEMPLATE)
|
||||||
|
ssh_telem["data"] = [{"identities": [fake_identity], "secrets": [fake_secret_full]}]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def insert_fake_monkey():
|
||||||
|
monkey = Monkey(guid=fake_monkey_guid, ip_addresses=[fake_ip_address])
|
||||||
|
monkey.save()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("uses_encryptor", "uses_database", "fake_mongo", "insert_fake_monkey")
|
||||||
|
def test_ssh_credential_parsing():
|
||||||
|
parse_credentials(ssh_telem)
|
||||||
|
config = ConfigService.get_config(should_decrypt=True)
|
||||||
|
ssh_keypairs = dpath.util.get(config, SSH_KEYS_PATH)
|
||||||
|
assert len(ssh_keypairs) == 1
|
||||||
|
assert ssh_keypairs[0]["private_key"] == fake_private_key
|
||||||
|
assert ssh_keypairs[0]["public_key"] == fake_public_key
|
||||||
|
assert ssh_keypairs[0]["user"] == fake_username
|
||||||
|
assert ssh_keypairs[0]["ip"] == fake_ip_address
|
||||||
|
assert fake_username in dpath.util.get(config, USER_LIST_PATH)
|
Loading…
Reference in New Issue