Merge branch 'release/1.12.0' into develop

This commit is contained in:
Mike Salvatore 2021-10-27 10:15:02 -04:00
commit 8554ab6fd5
18 changed files with 174 additions and 53 deletions

View File

@ -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

View File

@ -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

View File

@ -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).

View File

@ -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.

View File

@ -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": [],

View File

@ -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()

View File

@ -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")

View File

@ -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(

View File

@ -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

View File

@ -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)

View File

@ -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.")

View File

@ -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.",

View File

@ -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:

View File

@ -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"

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.11.0",
"version": "1.12.0",
"name": "infection-monkey",
"description": "Infection Monkey C&C UI",
"scripts": {

View File

@ -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']);

View File

@ -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

View File

@ -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)