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/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, 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}") diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a45800b9f..376d0221b 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 @@ -123,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/") @@ -132,7 +137,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.py b/monkey/monkey_island/cc/resources/monkey.py index f607b81e1..4e02fc258 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_flat_config_for_agent(monkey_json["config"]) + return monkey_json return {} 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() diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 280cdf763..4e5290a19 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,20 @@ class ConfigService: ), "exploit_ssh_keys": ConfigService.get_config_value(SSH_KEYS_PATH, should_decrypt=False), } + + @staticmethod + 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/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" 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