From 21a9c4fa1472f19d5300cd9471d3cd955ba29530 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 19:24:02 -0500 Subject: [PATCH 1/6] Island: Remove disused MonkeyConfiguration resource --- monkey/monkey_island/cc/app.py | 2 -- .../cc/resources/monkey_configuration.py | 26 ------------------- 2 files changed, 28 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/monkey_configuration.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a45800b9f..c9328d5d0 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -30,7 +30,6 @@ from monkey_island.cc.resources.island_mode import IslandMode from monkey_island.cc.resources.local_run import LocalRun from monkey_island.cc.resources.log import Log from monkey_island.cc.resources.monkey import Monkey -from monkey_island.cc.resources.monkey_configuration import MonkeyConfiguration from monkey_island.cc.resources.monkey_control.remote_port_check import RemotePortCheck from monkey_island.cc.resources.monkey_control.started_on_island import StartedOnIsland from monkey_island.cc.resources.monkey_control.stop_agent_check import StopAgentCheck @@ -132,7 +131,6 @@ def init_api_resources(api): ) api.add_resource(IslandMode, "/api/island-mode") - api.add_resource(MonkeyConfiguration, "/api/configuration", "/api/configuration/") api.add_resource(IslandConfiguration, "/api/configuration/island", "/api/configuration/island/") api.add_resource(ConfigurationExport, "/api/configuration/export") api.add_resource(ConfigurationImport, "/api/configuration/import") diff --git a/monkey/monkey_island/cc/resources/monkey_configuration.py b/monkey/monkey_island/cc/resources/monkey_configuration.py deleted file mode 100644 index 608030e5c..000000000 --- a/monkey/monkey_island/cc/resources/monkey_configuration.py +++ /dev/null @@ -1,26 +0,0 @@ -import json - -import flask_restful -from flask import abort, jsonify, request - -from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.config import ConfigService - - -class MonkeyConfiguration(flask_restful.Resource): - @jwt_required - def get(self): - return jsonify( - schema=ConfigService.get_config_schema(), - configuration=ConfigService.get_config(False, True), - ) - - @jwt_required - def post(self): - config_json = json.loads(request.data) - if "reset" in config_json: - ConfigService.reset_config() - else: - if not ConfigService.update_config(config_json, should_encrypt=True): - abort(400) - return self.get() From 7cda2b8e585ca0eb8325b45a4e0534efd61fd940 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 20:17:21 -0500 Subject: [PATCH 2/6] Island: Add "/legacy" config format option to monkey config endpoint The schema of the configuration that is given to the agent when it requests configuration from the island is heavily influenced by the GUI and how configuration options should be displayed to the user. It is not formatted in a way that is easy for the agent to utilize. This commit adds a `/api/monkey//` endpoint that allows legacy code to continue to function, while the agent's new AutomatedMaster component (issue #1597) can receive configuration in a way that makes sense for the agent. --- monkey/monkey_island/cc/app.py | 8 +++++++- monkey/monkey_island/cc/resources/monkey.py | 10 ++++++++-- monkey/monkey_island/cc/services/config.py | 5 +++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c9328d5d0..376d0221b 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -122,7 +122,13 @@ def init_api_resources(api): api.add_resource(Root, "/api") api.add_resource(Registration, "/api/registration") api.add_resource(Authenticate, "/api/auth") - api.add_resource(Monkey, "/api/monkey", "/api/monkey/", "/api/monkey/") + api.add_resource( + Monkey, + "/api/monkey", + "/api/monkey/", + "/api/monkey/", + "/api/monkey//", + ) api.add_resource(Bootloader, "/api/bootloader/") api.add_resource(LocalRun, "/api/local-monkey", "/api/local-monkey/") api.add_resource(ClientRun, "/api/client-monkey", "/api/client-monkey/") diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index f607b81e1..fbd093a8e 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -19,14 +19,20 @@ from monkey_island.cc.services.node import NodeService class Monkey(flask_restful.Resource): # Used by monkey. can't secure. - def get(self, guid=None, **kw): + def get(self, guid=None, config_format=None, **kw): NodeService.update_dead_monkeys() # refresh monkeys status if not guid: guid = request.args.get("guid") if guid: monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) - monkey_json["config"] = ConfigService.decrypt_flat_config(monkey_json["config"]) + # TODO: When the "legacy" format is no longer needed, update this logic and remove the + # "/api/monkey//" route. + if config_format == "legacy": + ConfigService.decrypt_flat_config(monkey_json["config"]) + else: + ConfigService.format_config_for_agent(monkey_json["config"]) + return monkey_json return {} diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 280cdf763..13a4cb214 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -2,6 +2,7 @@ import collections import copy import functools import logging +from typing import Dict from jsonschema import Draft4Validator, validators @@ -425,3 +426,7 @@ class ConfigService: ), "exploit_ssh_keys": ConfigService.get_config_value(SSH_KEYS_PATH, should_decrypt=False), } + + @staticmethod + def format_config_for_agent(config: Dict): + ConfigService.decrypt_flat_config(config) From 8730b2bbbce8ffe98280a06459d7b014c2822d4c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 20:58:17 -0500 Subject: [PATCH 3/6] Agent: Call /legacy config endpoint from ControlClient --- monkey/infection_monkey/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 367433cb6..88a8e43fa 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -208,7 +208,7 @@ class ControlClient(object): return try: reply = requests.get( # noqa: DUO123 - "https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + "https://%s/api/monkey/%s/legacy" % (WormConfiguration.current_server, GUID), verify=False, proxies=ControlClient.proxies, timeout=MEDIUM_REQUEST_TIMEOUT, From 9ed4f2687ead0ad34848cd7c08d6570fc165b345 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 20:59:38 -0500 Subject: [PATCH 4/6] Tests: Add flat monkey config for use in tests --- .../monkey_configs/flat_config.json | 134 ++++++++++++++++++ .../unit_tests/monkey_island/cc/conftest.py | 27 +++- .../test_password_based_encryption.py | 1 + 3 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 monkey/tests/data_for_tests/monkey_configs/flat_config.json diff --git a/monkey/tests/data_for_tests/monkey_configs/flat_config.json b/monkey/tests/data_for_tests/monkey_configs/flat_config.json new file mode 100644 index 000000000..82cc895a1 --- /dev/null +++ b/monkey/tests/data_for_tests/monkey_configs/flat_config.json @@ -0,0 +1,134 @@ +{ + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001, + 9200 + ], + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "alive": true, + "aws_access_key_id": "", + "aws_secret_access_key": "", + "aws_session_token": "", + "blocked_ips": [], + "command_servers": [ + "10.197.94.72:5000" + ], + "current_server": "10.197.94.72:5000", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "depth": 2, + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "dropper_set_date": true, + "dropper_target_path_linux": "/tmp/monkey", + "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", + "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_password_list": [ + "root", + "123456", + "password", + "123456789", + "qwerty", + "111111", + "iloveyou" + ], + "exploit_ssh_keys": [ + ], + "exploit_user_list": [ + "Administrator", + "root", + "user", + "ubuntu" + ], + "exploiter_classes": [ + "SmbExploiter", + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "MSSQLExploiter", + "DrupalExploiter", + "PowerShellExploiter" + ], + "export_monkey_telems": false, + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ], + "inaccessible_subnets": [], + "keep_tunnel_open_time": 60, + "local_network_scan": true, + "max_depth": null, + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "ms08_067_exploit_attempts": 5, + "ping_scan_timeout": 1000, + "post_breach_actions": [ + "CommunicateAsBackdoorUser", + "ModifyShellStartupFiles", + "HiddenFiles", + "TrapCommand", + "ChangeSetuidSetgid", + "ScheduleJobs", + "Timestomping", + "AccountDiscovery" + ], + "ransomware": { + "encryption": { + "enabled": true, + "directories": { + "linux_target_dir": "", + "windows_target_dir": "" + } + }, + "other_behaviors": { + "readme": true + } + }, + "skip_exploit_if_file_exist": false, + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey", + "started_on_island": false, + "subnet_scan_list": [], + "system_info_collector_classes": [ + "AwsCollector", + "ProcessListCollector", + "MimikatzCollector" + ], + "tcp_scan_get_banner": true, + "tcp_scan_interval": 0, + "tcp_scan_timeout": 3000, + "tcp_target_ports": [ + 22, + 2222, + 445, + 135, + 3389, + 80, + 8080, + 443, + 8008, + 3306, + 7001, + 8088 + ], + "user_to_add": "Monkey_IUSER_SUPPORT", + "victims_max_exploit": 100, + "victims_max_find": 100 +} diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index dfd927f4a..5777b3492 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -1,11 +1,12 @@ # Without these imports pytests can't use fixtures, # because they are not found import json -import os +from typing import Dict import pytest from tests.unit_tests.monkey_island.cc.mongomock_fixtures import * # noqa: F401,F403,E402 from tests.unit_tests.monkey_island.cc.server_utils.encryption.test_password_based_encryption import ( # noqa: E501 + FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME, MONKEY_CONFIGS_DIR_PATH, STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME, ) @@ -14,12 +15,24 @@ from monkey_island.cc.server_utils.encryption import unlock_datastore_encryptor @pytest.fixture -def monkey_config(data_for_tests_dir): - plaintext_monkey_config_standard_path = os.path.join( - data_for_tests_dir, MONKEY_CONFIGS_DIR_PATH, STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME - ) - plaintext_config = json.loads(open(plaintext_monkey_config_standard_path, "r").read()) - return plaintext_config +def load_monkey_config(data_for_tests_dir) -> Dict: + def inner(filename: str) -> Dict: + config_path = ( + data_for_tests_dir / MONKEY_CONFIGS_DIR_PATH / FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME + ) + return json.loads(open(config_path, "r").read()) + + return inner + + +@pytest.fixture +def monkey_config(load_monkey_config): + return load_monkey_config(STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME) + + +@pytest.fixture +def flat_monkey_config(load_monkey_config): + return load_monkey_config(FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME) @pytest.fixture diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py index 0e044c84a..ce0b46705 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py @@ -15,6 +15,7 @@ pytestmark = pytest.mark.slow MONKEY_CONFIGS_DIR_PATH = "monkey_configs" STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME = "monkey_config_standard.json" +FLAT_PLAINTEXT_MONKEY_CONFIG_FILENAME = "flat_config.json" PASSWORD = "hello123" INCORRECT_PASSWORD = "goodbye321" From 30afe3cc858cf6a96345bf68ae4dead303410db2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 21:03:45 -0500 Subject: [PATCH 5/6] Island: Strip credentials out of config before sending to agent The credentials for credential reuse attacks will now be retrieved by the agent via a new endpoint that returns only credentials in order to reduce unnecessary network traffic (issue #1538). --- CHANGELOG.md | 1 + monkey/monkey_island/cc/resources/monkey.py | 2 +- monkey/monkey_island/cc/services/config.py | 17 +++++++++++++++-- .../monkey_island/cc/services/test_config.py | 14 ++++++++++---- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c2a177e..7f5a59bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Hostname system info collector. #1535 - Max iterations and timeout between iterations config options. #1600 - MITRE ATT&CK configuration screen. #1532 +- Propagation credentials from "GET /api/monkey/" endpoint. #1538 ### Fixed - A bug in network map page that caused delay of telemetry log loading. #1545 diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index fbd093a8e..4e02fc258 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -31,7 +31,7 @@ class Monkey(flask_restful.Resource): if config_format == "legacy": ConfigService.decrypt_flat_config(monkey_json["config"]) else: - ConfigService.format_config_for_agent(monkey_json["config"]) + ConfigService.format_flat_config_for_agent(monkey_json["config"]) return monkey_json diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 13a4cb214..4e5290a19 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -428,5 +428,18 @@ class ConfigService: } @staticmethod - def format_config_for_agent(config: Dict): - ConfigService.decrypt_flat_config(config) + def format_flat_config_for_agent(config: Dict): + ConfigService._remove_credentials_from_flat_config(config) + + @staticmethod + def _remove_credentials_from_flat_config(config: Dict): + fields_to_remove = { + "exploit_lm_hash_list", + "exploit_ntlm_hash_list", + "exploit_password_list", + "exploit_ssh_keys", + "exploit_user_list", + } + + for field in fields_to_remove: + config.pop(field, None) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 751ca98ed..30e56e05e 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -6,10 +6,6 @@ from monkey_island.cc.services.config import ConfigService # monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js -class MockClass: - pass - - @pytest.fixture(scope="function", autouse=True) def mock_port(monkeypatch, PORT): monkeypatch.setattr("monkey_island.cc.services.config.ISLAND_PORT", PORT) @@ -27,3 +23,13 @@ def test_set_server_ips_in_config_current_server(config, IPS, PORT): ConfigService.set_server_ips_in_config(config) expected_config_current_server = f"{IPS[0]}:{PORT}" assert config["internal"]["island_server"]["current_server"] == expected_config_current_server + + +def test_format_config_for_agent__credentials_removed(flat_monkey_config): + ConfigService.format_flat_config_for_agent(flat_monkey_config) + + assert "exploit_lm_hash_list" not in flat_monkey_config + assert "exploit_ntlm_hash_list" not in flat_monkey_config + assert "exploit_password_list" not in flat_monkey_config + assert "exploit_ssh_keys" not in flat_monkey_config + assert "exploit_user_list" not in flat_monkey_config From 02c725d1f8702d38461ff85cd45d240507fb4fd7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 2 Dec 2021 21:25:20 -0500 Subject: [PATCH 6/6] Agent: Call get "/api/monkey" endpoint from ControlChannel.get_config() --- .../master/control_channel.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/master/control_channel.py b/monkey/infection_monkey/master/control_channel.py index 9e8046baf..12bf3a52f 100644 --- a/monkey/infection_monkey/master/control_channel.py +++ b/monkey/infection_monkey/master/control_channel.py @@ -19,37 +19,51 @@ class ControlChannel(IControlChannel): self._control_channel_server = server def should_agent_stop(self) -> bool: - if not self._control_channel_server: - return - try: response = requests.get( # noqa: DUO123 f"{self._control_channel_server}/api/monkey_control/{self._agent_id}", verify=False, + proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, ) response = json.loads(response.content.decode()) return response["stop_agent"] except Exception as e: + # TODO: Evaluate how this exception is handled; don't just log and ignore it. logger.error(f"An error occurred while trying to connect to server. {e}") + return True + def get_config(self) -> dict: - ControlClient.load_control_config() - return WormConfiguration.as_dict() + try: + response = requests.get( # noqa: DUO123 + "https://%s/api/monkey/%s" % (WormConfiguration.current_server, self._agent_id), + verify=False, + proxies=ControlClient.proxies, + timeout=SHORT_REQUEST_TIMEOUT, + ) + + return json.loads(response.content.decode()) + except Exception as exc: + # TODO: Evaluate how this exception is handled; don't just log and ignore it. + logger.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) + + return {} def get_credentials_for_propagation(self) -> dict: - if not self._control_channel_server: - return - try: response = requests.get( # noqa: DUO123 f"{self._control_channel_server}/api/propagationCredentials", verify=False, + proxies=ControlClient.proxies, timeout=SHORT_REQUEST_TIMEOUT, ) response = json.loads(response.content.decode())["propagation_credentials"] return response except Exception as e: + # TODO: Evaluate how this exception is handled; don't just log and ignore it. logger.error(f"An error occurred while trying to connect to server. {e}")