diff --git a/envs/monkey_zoo/blackbox/analyzers/analyzer.py b/envs/monkey_zoo/blackbox/analyzers/analyzer.py index d6043feeb..13db46cb3 100644 --- a/envs/monkey_zoo/blackbox/analyzers/analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/analyzer.py @@ -4,5 +4,5 @@ from abc import ABCMeta, abstractmethod class Analyzer(object, metaclass=ABCMeta): @abstractmethod - def analyze_test_results(self): + def analyze_test_results(self) -> bool: raise NotImplementedError() diff --git a/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py new file mode 100644 index 000000000..f5da3a2e1 --- /dev/null +++ b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py @@ -0,0 +1,70 @@ +from typing import List +from pprint import pformat + +import dpath.util + +from common.config_value_paths import USER_LIST_PATH, PASSWORD_LIST_PATH, NTLM_HASH_LIST_PATH, LM_HASH_LIST_PATH +from envs.monkey_zoo.blackbox.analyzers.analyzer import Analyzer +from envs.monkey_zoo.blackbox.analyzers.analyzer_log import AnalyzerLog +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient + +# Query for telemetry collection to see if password restoration was successful +TELEM_QUERY = {'telem_category': 'exploit', + 'data.exploiter': 'ZerologonExploiter', + 'data.info.password_restored': True} + + +class ZerologonAnalyzer(Analyzer): + + def __init__(self, island_client: MonkeyIslandClient, expected_credentials: List[str]): + self.island_client = island_client + self.expected_credentials = expected_credentials + self.log = AnalyzerLog(self.__class__.__name__) + + def analyze_test_results(self): + self.log.clear() + is_creds_gathered = self._analyze_credential_gathering() + is_creds_restored = self._analyze_credential_restore() + return is_creds_gathered and is_creds_restored + + def _analyze_credential_gathering(self) -> bool: + config = self.island_client.get_config() + credentials_on_island = ZerologonAnalyzer._get_relevant_credentials(config) + return self._is_all_credentials_in_list(credentials_on_island) + + @staticmethod + def _get_relevant_credentials(config: dict): + credentials_on_island = [] + credentials_on_island.extend(dpath.util.get(config['configuration'], USER_LIST_PATH)) + credentials_on_island.extend(dpath.util.get(config['configuration'], NTLM_HASH_LIST_PATH)) + credentials_on_island.extend(dpath.util.get(config['configuration'], LM_HASH_LIST_PATH)) + return credentials_on_island + + def _is_all_credentials_in_list(self, + all_creds: List[str]) -> bool: + credentials_missing = [cred for cred in self.expected_credentials if cred not in all_creds] + self._log_creds_not_gathered(credentials_missing) + return not credentials_missing + + def _log_creds_not_gathered(self, missing_creds: List[str]): + if not missing_creds: + self.log.add_entry("Zerologon exploiter gathered all credentials expected.") + else: + for cred in missing_creds: + self.log.add_entry(f"Credential Zerologon exploiter failed to gathered:{cred}.") + + def _analyze_credential_restore(self) -> bool: + cred_restore_telems = self.island_client.find_telems_in_db(TELEM_QUERY) + self._log_credential_restore(cred_restore_telems) + return bool(cred_restore_telems) + + def _log_credential_restore(self, telem_list: List[dict]): + if telem_list: + self.log.add_entry("Zerologon exploiter telemetry contains indicators that credentials " + "were successfully restored.") + else: + self.log.add_entry("Credential restore failed or credential restore " + "telemetry not found on the Monkey Island.") + self.log.add_entry(f"Query for credential restore telem: {pformat(TELEM_QUERY)}") + + diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py index 050cfe04c..304996ebd 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -1,6 +1,7 @@ import json import logging from time import sleep +from typing import Union from bson import json_util @@ -8,6 +9,7 @@ from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import Monkey SLEEP_BETWEEN_REQUESTS_SECONDS = 0.5 MONKEY_TEST_ENDPOINT = 'api/test/monkey' +TELEMETRY_TEST_ENDPOINT = 'api/test/telemetry' LOG_TEST_ENDPOINT = 'api/test/log' LOGGER = logging.getLogger(__name__) @@ -67,6 +69,13 @@ class MonkeyIslandClient(object): MonkeyIslandClient.form_find_query_for_request(query)) return MonkeyIslandClient.get_test_query_results(response) + def find_telems_in_db(self, query: dict): + if query is None: + raise TypeError + response = self.requests.get(TELEMETRY_TEST_ENDPOINT, + MonkeyIslandClient.form_find_query_for_request(query)) + return MonkeyIslandClient.get_test_query_results(response) + def get_all_monkeys_from_db(self): response = self.requests.get(MONKEY_TEST_ENDPOINT, MonkeyIslandClient.form_find_query_for_request(None)) @@ -78,7 +87,7 @@ class MonkeyIslandClient(object): return MonkeyIslandClient.get_test_query_results(response) @staticmethod - def form_find_query_for_request(query): + def form_find_query_for_request(query: Union[dict, None]) -> dict: return {'find_query': json_util.dumps(query)} @staticmethod diff --git a/envs/monkey_zoo/blackbox/island_configs/zerologon.py b/envs/monkey_zoo/blackbox/island_configs/zerologon.py new file mode 100644 index 000000000..6b84589fb --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/zerologon.py @@ -0,0 +1,15 @@ +from copy import copy + +from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate + + +class Zerologon(BaseTemplate): + + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["ZerologonExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.25"], + # Empty list to make sure ZeroLogon adds "Administrator" username + "basic.credentials.exploit_user_list": [] + }) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index d895f7cfe..b54fa5393 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -7,6 +7,7 @@ from typing_extensions import Type from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import \ CommunicationAnalyzer +from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer from envs.monkey_zoo.blackbox.island_client.island_config_parser import \ IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import \ @@ -25,6 +26,7 @@ from envs.monkey_zoo.blackbox.island_configs.tunneling import Tunneling from envs.monkey_zoo.blackbox.island_configs.weblogic import Weblogic from envs.monkey_zoo.blackbox.island_configs.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.island_configs.wmi_pth import WmiPth +from envs.monkey_zoo.blackbox.island_configs.zerologon import Zerologon from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import \ TestLogsHandler from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest @@ -160,6 +162,22 @@ class TestMonkeyBlackbox: def test_wmi_pth(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, WmiPth, "WMI_PTH") + def test_zerologon_exploiter(self, island_client): + test_name = "Zerologon_exploiter" + expected_creds = ["Administrator", + "aad3b435b51404eeaad3b435b51404ee", + "2864b62ea4496934a5d6e86f50b834a5"] + raw_config = IslandConfigParser.get_raw_config(Zerologon, island_client) + analyzer = ZerologonAnalyzer(island_client, expected_creds) + log_handler = TestLogsHandler(test_name, island_client, TestMonkeyBlackbox.get_log_dir_path()) + ExploitationTest( + name=test_name, + island_client=island_client, + raw_config=raw_config, + analyzers=[analyzer], + timeout=DEFAULT_TIMEOUT_SECONDS, + log_handler=log_handler).run() + @pytest.mark.skip(reason="Perfomance test that creates env from fake telemetries is faster, use that instead.") def test_report_generation_performance(self, island_client, quick_performance_tests): """ diff --git a/monkey/monkey_island/cc/services/config_schema/config_value_paths.py b/monkey/common/config_value_paths.py similarity index 100% rename from monkey/monkey_island/cc/services/config_schema/config_value_paths.py rename to monkey/common/config_value_paths.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c53c04caa..c7fd0006f 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -7,6 +7,7 @@ from werkzeug.exceptions import NotFound import monkey_island.cc.environment.environment_singleton as env_singleton from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH +from monkey_island.cc.resources.test.telemetry_test import TelemetryTest from monkey_island.cc.resources.zero_trust.zero_trust_report import ZeroTrustReport from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.custom_json_encoder import CustomJSONEncoder @@ -145,9 +146,11 @@ def init_api_resources(api): api.add_resource(ScoutSuiteAuth, '/api/scoutsuite_auth/') api.add_resource(AWSKeys, '/api/aws_keys') + # Resources used by black box tests api.add_resource(MonkeyTest, '/api/test/monkey') api.add_resource(ClearCaches, '/api/test/clear_caches') api.add_resource(LogTest, '/api/test/log') + api.add_resource(TelemetryTest, '/api/test/telemetry') def init_app(mongo_url): diff --git a/monkey/monkey_island/cc/resources/test/telemetry_test.py b/monkey/monkey_island/cc/resources/test/telemetry_test.py new file mode 100644 index 000000000..29108070e --- /dev/null +++ b/monkey/monkey_island/cc/resources/test/telemetry_test.py @@ -0,0 +1,13 @@ +import flask_restful +from bson import json_util +from flask import request + +from monkey_island.cc.database import mongo +from monkey_island.cc.resources.auth.auth import jwt_required + + +class TelemetryTest(flask_restful.Resource): + @jwt_required + def get(self, **kw): + find_query = json_util.loads(request.args.get('find_query')) + return {'results': list(mongo.db.telemetry.find(find_query))} diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py index c3fcd03e8..3b18be488 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py @@ -4,7 +4,7 @@ from monkey_island.cc.services.config import ConfigService __author__ = "VakarisZ" -from monkey_island.cc.services.config_schema.config_value_paths import CURRENT_SERVER_PATH +from common.config_value_paths import CURRENT_SERVER_PATH class T1065(AttackTechnique): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index b4370a63b..390380131 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -14,10 +14,10 @@ from monkey_island.cc.services.config_schema.config_schema import SCHEMA __author__ = "itay.mizeretz" -from monkey_island.cc.services.config_schema.config_value_paths import (AWS_KEYS_PATH, EXPORT_MONKEY_TELEMS_PATH, - LM_HASH_LIST_PATH, NTLM_HASH_LIST_PATH, - PASSWORD_LIST_PATH, SSH_KEYS_PATH, - STARTED_ON_ISLAND_PATH, USER_LIST_PATH) +from common.config_value_paths import (AWS_KEYS_PATH, EXPORT_MONKEY_TELEMS_PATH, + LM_HASH_LIST_PATH, NTLM_HASH_LIST_PATH, + PASSWORD_LIST_PATH, SSH_KEYS_PATH, + STARTED_ON_ISLAND_PATH, USER_LIST_PATH) logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py index 48857e2e3..493d5af03 100644 --- a/monkey/monkey_island/cc/services/configuration/utils.py +++ b/monkey/monkey_island/cc/services/configuration/utils.py @@ -1,5 +1,5 @@ from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.config_schema.config_value_paths import INACCESSIBLE_SUBNETS_PATH +from common.config_value_paths import INACCESSIBLE_SUBNETS_PATH def get_config_network_segments_as_subnet_groups(): diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 5970a33b7..a23aa6d85 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -12,9 +12,9 @@ from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.config_schema.config_value_paths import (EXPLOITER_CLASSES_PATH, LOCAL_NETWORK_SCAN_PATH, - PASSWORD_LIST_PATH, SUBNET_SCAN_LIST_PATH, - USER_LIST_PATH) +from common.config_value_paths import (EXPLOITER_CLASSES_PATH, LOCAL_NETWORK_SCAN_PATH, + PASSWORD_LIST_PATH, SUBNET_SCAN_LIST_PATH, + USER_LIST_PATH) from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.reporting.pth_report import PTHReportService diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index dc3f8d5ee..b5d405234 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -6,7 +6,7 @@ from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import InvalidAWSKeys from monkey_island.cc.server_utils.encryptor import encryptor from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.config_schema.config_value_paths import AWS_KEYS_PATH +from common.config_value_paths import AWS_KEYS_PATH def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py index 24e700ce6..c35e55a8f 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py @@ -6,7 +6,7 @@ import dpath.util from monkey_island.cc.database import mongo from monkey_island.cc.server_utils import encryptor from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.config_schema.config_value_paths import AWS_KEYS_PATH +from common.config_value_paths import AWS_KEYS_PATH from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import is_aws_keys_setup from monkey_island.cc.test_common.fixtures import FixtureEnum