diff --git a/CHANGELOG.md b/CHANGELOG.md index 148637c72..295fc9442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [Unreleased] +## [1.12.0] - 2021-10-27 ### Added - A new exploiter that allows propagation via PowerShell Remoting. #1246 - A warning regarding antivirus when agent binaries are missing. #1450 @@ -13,12 +13,12 @@ Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - The name of the "Communicate as new user" post-breach action to "Communicate - as backdoor user". #1410 + as backdoor user". #1410 - Resetting login credentials also cleans the contents of the database. #1495 - ATT&CK report messages (more accurate now). #1483 - T1086 (PowerShell) now also reports if ps1 scripts were run by PBAs. #1513 -- ATT&CK report messages to include empty internal config options as reasons for unscanned attack - techniques. #1518 +- ATT&CK report messages to include internal config options as reasons + for unscanned attack techniques. #1518 ### Removed - Internet access check on agent start. #1402 @@ -29,16 +29,15 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Stale code in the Windows system info collector that collected installed packages and WMI info. #1389 - Insecure access feature in the Monkey Island. #1418 -- The "deployment" field from the server_config.json #1205 +- The "deployment" field from the server_config.json. #1205 - The "Execution through module load" ATT&CK technique, since it can no longer be exercise with current code. #1416 -- Browser window popup when Monkey Island starts on Windows. #1428 +- Browser window pop-up when Monkey Island starts on Windows. #1428 ### Fixed - Misaligned buttons and input fields on exploiter and network configuration pages. #1353 - Credentials shown in plain text on configuration screens. #1183 -- Typo "trough" -> "through" in telemetry and docstring. - Crash when unexpected character encoding is used by ping command on German language systems. #1175 - Malfunctioning timestomping PBA. #1405 @@ -47,24 +46,28 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Overlapping Guardicore logo in the landing page. #1441 - PBA table collapse in security report on data change. #1423 - Unsigned Windows agent binaries in Linux packages are now signed. #1444 -- Some of the gathered credentials no longer appear in database plaintext. #1454 -- Encryptor breaking with UTF-8 characters. (Passwords in different languages can be submitted in - the config successfully now.) #1490 -- Mimikatz collector no longer fails if Azure credential collector is disabled. #1512 #1493 -- Unhandled error when "modify shell startup files PBA" is unable to find regular users. #1507 -- ATT&CK report bug that showed different techniques' results under a technique if the PBA behind - them was the same. #1514 -- ATT&CK report bug that said that the technique "`.bash_profile` and `.bashrc`" was not attempted - when it actually was attempted but failed. #1511 +- Some of the gathered credentials no longer appear in plaintext in the + database. #1454 +- Encryptor breaking with UTF-8 characters. (Passwords in different languages + can be submitted in the config successfully now.) #1490 +- Mimikatz collector no longer fails if Azure credential collector is disabled. + #1512, #1493 +- Unhandled error when "modify shell startup files PBA" is unable to find + regular users. #1507 +- ATT&CK report bug that showed different techniques' results under a technique + if the PBA behind them was the same. #1514 +- ATT&CK report bug that said that the technique "`.bash_profile` and + `.bashrc`" was not attempted when it actually was attempted but failed. #1511 - Bug that periodically cleared the telemetry table's filter. #1392 -- Crashes, stack traces, and other malfunctions when data from older versions of Infection Monkey is - present in the data directory. #1114 +- Crashes, stack traces, and other malfunctions when data from older versions + of Infection Monkey is present in the data directory. #1114 - Broken update links. #1524 ### Security - Generate a random password when creating a new user for CommunicateAsNewUser PBA. #1434 -- Credentials gathered from victim machines are no longer stored plaintext in the database. #1454 +- Credentials gathered from victim machines are no longer stored plaintext in + the database. #1454 - Encrypt the database key with user's credentials. #1463 diff --git a/build_scripts/docker/Dockerfile b/build_scripts/docker/Dockerfile index 2637d3725..ecd2ce296 100644 --- a/build_scripts/docker/Dockerfile +++ b/build_scripts/docker/Dockerfile @@ -18,6 +18,7 @@ COPY --from=builder /monkey /monkey WORKDIR /monkey EXPOSE 5000 EXPOSE 5001 +ENV MONKEY_DOCKER_CONTAINER=true RUN groupadd -r monkey-island && useradd --no-log-init -r -g monkey-island monkey-island RUN chmod 444 /monkey/monkey_island/cc/server.key RUN chmod 444 /monkey/monkey_island/cc/server.csr diff --git a/docs/content/reference/exploiters/Zerologon.md b/docs/content/reference/exploiters/Zerologon.md index 76a524c03..90ece682b 100644 --- a/docs/content/reference/exploiters/Zerologon.md +++ b/docs/content/reference/exploiters/Zerologon.md @@ -10,7 +10,7 @@ The Zerologon exploiter exploits [CVE-2020-1472](https://cve.mitre.org/cgi-bin/c ### Description -An elevation of privilege vulnerability exists when an attacker establishes a vulnerable Netlogon secure channel connection to a domain controller, using the Netlogon Remote Protocol (MS-NRPC). +An elevation of privilege vulnerability exists when an attacker establishes a vulnerable Netlogon secure channel connection to a domain controller, using the Netlogon Remote Protocol (MS-NRPC). The Zerologon exploiter takes advantage of this vulnerability to steal credentials from the domain controller. This allows the Infection Monkey to propagate to the machine using one of the brute force exploiters (for example, the SMB Exploiter). To download the relevant security update and read more, click [here](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1472). diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index f195caf34..db5979fc6 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -33,6 +33,10 @@ The Infection Monkey Docker container works on Linux only. It is not compatible ``` ### 2. Start MongoDB +{{% notice info %}} +If you are upgrading the Infection Monkey to a new version, be sure to remove +any MongoDB containers or volumes associated with the previous version. +{{% /notice %}} 1. Start a MongoDB Docker container: @@ -56,16 +60,22 @@ been signed by a private certificate authority. 1. Run the Monkey Island server ```bash sudo docker run \ + --tty \ + --interactive \ --name monkey-island \ --network=host \ guardicore/monkey-island:VERSION ``` ### 3b. Start Monkey Island with user-provided certificate +{{% notice info %}} +If you are upgrading the Infection Monkey to a new version, be sure to remove +any volumes associated with the previous version. +{{% /notice %}} -1. Create a directory named `monkey_island_data`. This will serve as the - location where Infection Monkey stores its configuration and runtime - artifacts. +1. Create a directory named `monkey_island_data`. If you already have it, + **make sure it's empty**. This will serve as the location where Infection + Monkey stores its configuration and runtime artifacts. ```bash mkdir ./monkey_island_data @@ -118,6 +128,8 @@ been signed by a private certificate authority. ```bash sudo docker run \ + --tty \ + --interactive \ --name monkey-island \ --network=host \ --user "$(id -u ${USER}):$(id -g ${USER})" \ @@ -132,8 +144,9 @@ After the Monkey Island docker container starts, you can access Monkey Island by ## Upgrading Currently, there's no "upgrade-in-place" option when a new version is released. -To get an updated version, download it, stop the current container and run the -installation commands again with the new file. +To get an updated version, download it, stop and remove the current Monkey +Island and MongoDB containers and volumes, and run the installation commands +again with the new file. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. diff --git a/envs/monkey_zoo/blackbox/config_templates/zerologon.py b/envs/monkey_zoo/blackbox/config_templates/zerologon.py index 93ebd5301..0c0266857 100644 --- a/envs/monkey_zoo/blackbox/config_templates/zerologon.py +++ b/envs/monkey_zoo/blackbox/config_templates/zerologon.py @@ -10,7 +10,7 @@ class Zerologon(ConfigTemplate): config_values.update( { - "basic.exploiters.exploiter_classes": ["ZerologonExploiter"], + "basic.exploiters.exploiter_classes": ["ZerologonExploiter", "SmbExploiter"], "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 cc4d6ba97..3b74f8961 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -221,7 +221,10 @@ class TestMonkeyBlackbox: "2864b62ea4496934a5d6e86f50b834a5", ] raw_config = IslandConfigParser.get_raw_config(Zerologon, island_client) - analyzer = ZerologonAnalyzer(island_client, expected_creds) + zero_logon_analyzer = ZerologonAnalyzer(island_client, expected_creds) + communication_analyzer = CommunicationAnalyzer( + island_client, IslandConfigParser.get_ips_of_targets(raw_config) + ) log_handler = TestLogsHandler( test_name, island_client, TestMonkeyBlackbox.get_log_dir_path() ) @@ -229,7 +232,7 @@ class TestMonkeyBlackbox: name=test_name, island_client=island_client, raw_config=raw_config, - analyzers=[analyzer], + analyzers=[zero_logon_analyzer, communication_analyzer], timeout=DEFAULT_TIMEOUT_SECONDS, log_handler=log_handler, ).run() diff --git a/monkey/common/version.py b/monkey/common/version.py index 3582caa72..5c94adc8d 100644 --- a/monkey/common/version.py +++ b/monkey/common/version.py @@ -4,7 +4,7 @@ import argparse from pathlib import Path MAJOR = "1" -MINOR = "11" +MINOR = "12" PATCH = "0" build_file_path = Path(__file__).parent.joinpath("BUILD") diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py index 8e0758c77..dba5daad4 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_backdoor_user.py @@ -41,7 +41,7 @@ class CommunicateAsBackdoorUser(PBA): def run(self): username = CommunicateAsBackdoorUser.get_random_new_user_name() try: - password = get_random_password() + password = get_random_password(14) with create_auto_new_user(username, password) as new_user: http_request_commandline = ( CommunicateAsBackdoorUser.get_commandline_for_http_request( diff --git a/monkey/infection_monkey/utils/random_password_generator.py b/monkey/infection_monkey/utils/random_password_generator.py index 273343c22..3d77f1629 100644 --- a/monkey/infection_monkey/utils/random_password_generator.py +++ b/monkey/infection_monkey/utils/random_password_generator.py @@ -1,8 +1,10 @@ import secrets +import string -SECRET_BYTE_LENGTH = 32 +SECRET_LENGTH = 32 -def get_random_password(length: int = SECRET_BYTE_LENGTH) -> str: - password = secrets.token_urlsafe(length) +def get_random_password(length: int = SECRET_LENGTH) -> str: + alphabet = string.ascii_letters + string.digits + string.punctuation + password = "".join(secrets.choice(alphabet) for i in range(length)) return password diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index dd5547659..c6dc9c0b9 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -70,7 +70,8 @@ def _setup_data_dir(island_args: IslandCmdArgs) -> Tuple[IslandConfigOptions, st except json.JSONDecodeError as ex: print(f"Error loading server config: {ex}") exit(1) - except IncompatibleDataDirectory: + except IncompatibleDataDirectory as ex: + print(f"Incompatible data directory: {ex}") exit(1) diff --git a/monkey/monkey_island/cc/server_utils/encryption/password_based_bytes_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/password_based_bytes_encryptor.py index b50e77e87..dd9ea329f 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/password_based_bytes_encryptor.py +++ b/monkey/monkey_island/cc/server_utils/encryption/password_based_bytes_encryptor.py @@ -47,7 +47,7 @@ class PasswordBasedBytesEncryptor(IEncryptor): ) except ValueError as ex: if str(ex).startswith("Wrong password"): - logger.error("Wrong password provided for decryption.") + logger.debug("Wrong password provided for decryption.") raise InvalidCredentialsError else: logger.error("The provided ciphertext was corrupt.") diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 92898fdad..85cc09014 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -144,10 +144,10 @@ EXPLOITER_CLASSES = { "title": "Zerologon Exploiter", "safe": False, "info": "Exploits a privilege escalation vulnerability (CVE-2020-1472) in a Windows " - "server domain controller by using the Netlogon Remote Protocol (MS-NRPC). " - "This exploiter changes the password of a Windows server domain controller " - "account and then attempts to restore it. The victim domain controller " - "will be unable to communicate with other domain controllers until the original " + "server domain controller (DC) by using the Netlogon Remote Protocol (MS-NRPC). " + "This exploiter changes the password of a Windows server DC account, steals " + "credentials, and then attempts to restore the original DC password. The victim DC " + "will be unable to communicate with other DCs until the original " "password has been restored. If Infection Monkey fails to restore the " "password automatically, you'll have to do it manually. For more " "information, see the documentation.", diff --git a/monkey/monkey_island/cc/setup/data_dir.py b/monkey/monkey_island/cc/setup/data_dir.py index c728dca04..af01da050 100644 --- a/monkey/monkey_island/cc/setup/data_dir.py +++ b/monkey/monkey_island/cc/setup/data_dir.py @@ -1,9 +1,11 @@ import logging +import os import shutil from pathlib import Path from common.version import get_version from monkey_island.cc.server_utils.file_utils import create_secure_directory +from monkey_island.cc.setup.env_utils import is_running_on_docker from monkey_island.cc.setup.version_file_setup import get_version_from_dir, write_version logger = logging.getLogger(__name__) @@ -15,7 +17,7 @@ class IncompatibleDataDirectory(Exception): def setup_data_dir(data_dir_path: Path) -> None: logger.info(f"Setting up data directory at {data_dir_path}.") - if data_dir_path.exists() and _data_dir_version_mismatch_exists(data_dir_path): + if _is_data_dir_old(data_dir_path): logger.info("Version in data directory does not match the Island's version.") _handle_old_data_directory(data_dir_path) create_secure_directory(str(data_dir_path)) @@ -23,18 +25,48 @@ def setup_data_dir(data_dir_path: Path) -> None: logger.info("Data directory set up.") +def _is_data_dir_old(data_dir_path: Path) -> bool: + dir_exists = data_dir_path.exists() + + if is_running_on_docker(): + return _is_docker_data_dir_old(data_dir_path) + + if not dir_exists or _is_directory_empty(data_dir_path): + return False + + return _data_dir_version_mismatch_exists(data_dir_path) + + +def _is_docker_data_dir_old(data_dir_path: Path) -> bool: + if _data_dir_version_mismatch_exists(data_dir_path): + if _is_directory_empty(data_dir_path): + return False + else: + raise IncompatibleDataDirectory( + "Found an old volume. " + "You must create an empty volume for each docker container " + "as specified in setup documentation: " + "https://www.guardicore.com/infectionmonkey/docs/setup/docker/" + ) + else: + return False + + +def _is_directory_empty(path: Path) -> bool: + return not os.listdir(path) + + def _handle_old_data_directory(data_dir_path: Path) -> None: should_delete_data_directory = _prompt_user_to_delete_data_directory(data_dir_path) if should_delete_data_directory: shutil.rmtree(data_dir_path) logger.info(f"{data_dir_path} was deleted.") else: - logger.error( + raise IncompatibleDataDirectory( "Unable to set up data directory. Please backup and delete the existing data directory" f" ({data_dir_path}). Then, try again. To learn how to restore and use a backup, please" " refer to the documentation." ) - raise IncompatibleDataDirectory() def _prompt_user_to_delete_data_directory(data_dir_path: Path) -> bool: diff --git a/monkey/monkey_island/cc/setup/env_utils.py b/monkey/monkey_island/cc/setup/env_utils.py new file mode 100644 index 000000000..07f6417ea --- /dev/null +++ b/monkey/monkey_island/cc/setup/env_utils.py @@ -0,0 +1,8 @@ +import os + +# Must match evn var name in build_scripts/docker/Dockerfile:21 +DOCKER_ENV_VAR = "MONKEY_DOCKER_CONTAINER" + + +def is_running_on_docker(): + return os.environ.get(DOCKER_ENV_VAR) == "true" diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 7689d95b7..da6200c25 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.11.0", + "version": "1.12.0", "name": "infection-monkey", "description": "Infection Monkey C&C UI", "scripts": { diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx index 9456e7dd8..8a600ab28 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx @@ -37,7 +37,7 @@ const ConfigImportModal = (props: Props) => { if (configContents !== null) { sendConfigToServer(); } - }, [configContents]) + }, [configContents, unsafeOptionsVerified]) function sendConfigToServer() { @@ -67,6 +67,7 @@ const ConfigImportModal = (props: Props) => { } else if (res['import_status'] === 'unsafe_options_verification_required') { setUploadStatus(UploadStatuses.success); setErrorMessage(''); + if (isUnsafeOptionSelected(res['config_schema'], JSON.parse(res['config']))) { setShowUnsafeOptionsConfirmation(true); setCandidateConfig(res['config']); diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_random_password_generator.py b/monkey/tests/unit_tests/infection_monkey/utils/test_random_password_generator.py index bdd97cdfd..6131fa34b 100644 --- a/monkey/tests/unit_tests/infection_monkey/utils/test_random_password_generator.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_random_password_generator.py @@ -2,12 +2,17 @@ from infection_monkey.utils.random_password_generator import get_random_password def test_get_random_password__length(): - password_byte_length = len(get_random_password().encode()) + password_length = len(get_random_password()) # 32 is the recommended secure byte length for secrets - assert password_byte_length >= 32 + assert password_length == 32 + + +def test_get_random_password__custom_length(): + password_length = len(get_random_password(14)) + assert password_length == 14 def test_get_random_password__randomness(): random_password1 = get_random_password() random_password2 = get_random_password() - assert not random_password1 == random_password2 + assert random_password1 != random_password2 diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/test_data_dir.py b/monkey/tests/unit_tests/monkey_island/cc/setup/test_data_dir.py index fe60d227d..e476784b1 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/setup/test_data_dir.py +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/test_data_dir.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest from monkey_island.cc.setup.data_dir import IncompatibleDataDirectory, setup_data_dir +from monkey_island.cc.setup.env_utils import DOCKER_ENV_VAR from monkey_island.cc.setup.version_file_setup import _version_filename current_version = "1.1.1" @@ -27,6 +28,12 @@ def temp_version_file_path(temp_data_dir_path) -> Path: return temp_data_dir_path / _version_filename +def create_bogus_file(dir_path: Path) -> Path: + bogus_file_path = dir_path / "test.txt" + bogus_file_path.touch() + return bogus_file_path + + def test_setup_data_dir(temp_data_dir_path, temp_version_file_path): data_dir_path = temp_data_dir_path setup_data_dir(data_dir_path) @@ -41,8 +48,7 @@ def test_old_version_removed(monkeypatch, temp_data_dir_path, temp_version_file_ temp_data_dir_path.mkdir() temp_version_file_path.write_text(old_version) - bogus_file_path = temp_data_dir_path / "test.txt" - bogus_file_path.touch() + bogus_file_path = create_bogus_file(temp_data_dir_path) setup_data_dir(temp_data_dir_path) @@ -58,8 +64,7 @@ def test_old_version_not_removed( temp_data_dir_path.mkdir() temp_version_file_path.write_text(old_version) - bogus_file_path = temp_data_dir_path / "test.txt" - bogus_file_path.touch() + bogus_file_path = create_bogus_file(temp_data_dir_path) with pytest.raises(IncompatibleDataDirectory): setup_data_dir(temp_data_dir_path) @@ -71,9 +76,56 @@ def test_old_version_not_removed( def test_data_dir_setup_not_needed(temp_data_dir_path, temp_version_file_path): temp_data_dir_path.mkdir() temp_version_file_path.write_text(current_version) - bogus_file_path = temp_data_dir_path / "test.txt" - bogus_file_path.touch() + bogus_file_path = create_bogus_file(temp_data_dir_path) setup_data_dir(temp_data_dir_path) assert temp_version_file_path.read_text() == current_version assert bogus_file_path.is_file() + + +def test_empty_data_dir(temp_data_dir_path, temp_version_file_path): + temp_data_dir_path.mkdir() + + setup_data_dir(temp_data_dir_path) + assert temp_version_file_path.read_text() == current_version + + +def test_new_data_dir_docker(monkeypatch, temp_data_dir_path, temp_version_file_path): + monkeypatch.setenv(DOCKER_ENV_VAR, "true") + + temp_data_dir_path.mkdir() + bogus_file_path = create_bogus_file(temp_data_dir_path) + temp_version_file_path.write_text(current_version) + + setup_data_dir(temp_data_dir_path) + assert temp_version_file_path.read_text() == current_version + assert bogus_file_path.is_file() + + +def test_data_dir_docker_old_version(monkeypatch, temp_data_dir_path, temp_version_file_path): + monkeypatch.setenv(DOCKER_ENV_VAR, "true") + + temp_data_dir_path.mkdir() + temp_version_file_path.write_text(old_version) + + with pytest.raises(IncompatibleDataDirectory): + setup_data_dir(temp_data_dir_path) + + +def test_empty_data_dir_docker(monkeypatch, temp_data_dir_path, temp_version_file_path): + monkeypatch.setenv(DOCKER_ENV_VAR, "true") + + temp_data_dir_path.mkdir() + + setup_data_dir(temp_data_dir_path) + assert temp_version_file_path.read_text() == current_version + + +def test_old_data_dir_docker_no_version(monkeypatch, temp_data_dir_path): + monkeypatch.setenv(DOCKER_ENV_VAR, "true") + + temp_data_dir_path.mkdir() + create_bogus_file(temp_data_dir_path) + + with pytest.raises(IncompatibleDataDirectory): + setup_data_dir(temp_data_dir_path)