diff --git a/monkey/common/utils/attack_utils.py b/monkey/common/utils/attack_utils.py index 28feaa537..cb3c8f029 100644 --- a/monkey/common/utils/attack_utils.py +++ b/monkey/common/utils/attack_utils.py @@ -9,8 +9,9 @@ class ScanStatus(Enum): # Technique was attempted and succeeded USED = 2 + # Dict that describes what BITS job was used for -BITS_UPLOAD_STRING = {"usage": "BITS job was used to upload monkey to a remote system."} +BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system." def format_time(time): diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 97b66690e..f7e4cfae4 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -157,9 +157,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 diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 3fae277e8..6ce94451d 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -123,11 +123,11 @@ class ControlClient(object): return {} @staticmethod - def send_telemetry(telem_type, data): + def send_telemetry(telem_catagory, data): if not WormConfiguration.current_server: return try: - telemetry = {'monkey_guid': GUID, 'telem_type': telem_type, 'data': data} + telemetry = {'monkey_guid': GUID, 'telem_catagory': telem_catagory, 'data': data} reply = requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,), data=json.dumps(telemetry), headers={'content-type': 'application/json'}, diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 8c8668bef..8dba50352 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -29,9 +29,7 @@ "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", diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index 3a129ebc0..24a902eea 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -11,7 +11,7 @@ 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,\ DOWNLOAD_TIMEOUT from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE -from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem +from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING import re @@ -64,7 +64,7 @@ class ElasticGroovyExploiter(WebRCE): def upload_monkey(self, url, commands=None): result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands) if 'windows' in self.host.os['type'] and result: - VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send() + T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() return result def get_results(self, response): diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 84719c6ea..9e1692fb9 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,15 +1,16 @@ -import os import logging -from time import sleep -import pymssql +import os import textwrap +from time import sleep + +import pymssql -from infection_monkey.exploit import HostExploiter, tools from common.utils.exploit_enum import ExploitType +from infection_monkey.exploit import HostExploiter, tools from infection_monkey.exploit.tools import HTTPTools -from infection_monkey.config import WormConfiguration -from infection_monkey.model import DROPPER_ARG from infection_monkey.exploit.tools import get_monkey_dest_path +from infection_monkey.model import DROPPER_ARG +from infection_monkey.utils import get_monkey_dir_path LOG = logging.getLogger(__name__) @@ -52,10 +53,10 @@ class MSSQLExploiter(HostExploiter): LOG.info("Started http server on %s", http_path) dst_path = get_monkey_dest_path(http_path) - tmp_file_path = os.path.join(WormConfiguration.monkey_dir_windows, MSSQLExploiter.TMP_FILE_NAME) + tmp_file_path = os.path.join(get_monkey_dir_path(), MSSQLExploiter.TMP_FILE_NAME) # Create monkey dir. - commands = ["xp_cmdshell \"mkdir %s\"" % WormConfiguration.monkey_dir_windows] + commands = ["xp_cmdshell \"mkdir %s\"" % get_monkey_dir_path()] MSSQLExploiter.execute_command(cursor, commands) # Form download command in a file diff --git a/monkey/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py index 1e66040fc..819ff00fb 100644 --- a/monkey/infection_monkey/exploit/rdpgrinder.py +++ b/monkey/infection_monkey/exploit/rdpgrinder.py @@ -15,9 +15,9 @@ 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.telemetry.attack.t1197_telem import T1197Telem from infection_monkey.utils import utf_to_ascii from common.utils.exploit_enum import ExploitType -from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING __author__ = 'hoffer' @@ -316,7 +316,7 @@ class RdpExploiter(HostExploiter): if client_factory.success: if not self._config.rdp_use_vbs_download: - VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send() + T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() self.add_vuln_port(RDP_PORT) exploited = True self.report_login_attempt(True, user, password) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index eec6484ac..b73547710 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -7,7 +7,7 @@ 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.network.tools import check_tcp_port, tcp_port_to_service -from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem +from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING __author__ = 'VakarisZ' @@ -210,6 +210,7 @@ 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: @@ -307,7 +308,7 @@ 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} - VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send() + T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() resp = self.exploit(url, backup_command) return resp diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index b152e58be..92714c7d6 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -15,10 +15,10 @@ 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.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 import get_interface_to_target __author__ = 'itamar' @@ -186,9 +186,11 @@ class InfectionMonkey(object): 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 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..efbedcaff --- /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_catagory = 'attack' + + def get_data(self): + return { + 'status': self.status.value, + 'technique': self.technique + } 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/test_victim_host_telem.py b/monkey/infection_monkey/telemetry/attack/test_victim_host_telem.py new file mode 100644 index 000000000..6b5bbcb73 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/test_victim_host_telem.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_catagory, '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/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/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py new file mode 100644 index 000000000..eaafc6aa8 --- /dev/null +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -0,0 +1,36 @@ +import abc + +from infection_monkey.control import ControlClient + +__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 + """ + ControlClient.send_telemetry(self.telem_catagory, self.get_data()) + + @abc.abstractproperty + def telem_catagory(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/transport/attack_telems/__init__.py b/monkey/infection_monkey/transport/attack_telems/__init__.py deleted file mode 100644 index 98867ed4d..000000000 --- a/monkey/infection_monkey/transport/attack_telems/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'VakarisZ' 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 93d5bbbf7..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: Dictionary of other relevant info. E.g. {'brute_force_blocked': True} - """ - 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 fc0da7fbf..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 = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr} - self.data.update({'machine': victim_host}) diff --git a/monkey/infection_monkey/utils.py b/monkey/infection_monkey/utils.py index 68b39163d..6eb3aefb5 100644 --- a/monkey/infection_monkey/utils.py +++ b/monkey/infection_monkey/utils.py @@ -1,7 +1,9 @@ import os -import sys import shutil import struct +import sys +import tempfile + from infection_monkey.config import WormConfiguration @@ -16,10 +18,9 @@ def get_dropper_log_path(): def is_64bit_windows_os(): - ''' + """ Checks for 64 bit Windows OS using environment variables. - :return: - ''' + """ return 'PROGRAMFILES(X86)' in os.environ @@ -53,7 +54,4 @@ def remove_monkey_dir(): def get_monkey_dir_path(): - if is_windows_os(): - return WormConfiguration.monkey_dir_windows - else: - return WormConfiguration.monkey_dir_linux + return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 82392a7b4..2e04ef0be 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -33,7 +33,6 @@ 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.attack_telem import AttackTelem from monkey_island.cc.resources.attack.attack_config import AttackConfiguration from monkey_island.cc.resources.attack.attack_report import AttackReport @@ -133,7 +132,6 @@ def init_api_resources(api): '/api/fileUpload/?restore=') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') api.add_resource(AttackConfiguration, '/api/attack') - api.add_resource(AttackTelem, '/api/attack/') api.add_resource(AttackReport, '/api/attack/report') api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/') 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/main.py b/monkey/monkey_island/cc/main.py index 412c3c399..5b9bda8cb 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: @@ -22,7 +24,7 @@ from monkey_island.cc.app import init_app from monkey_island.cc.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 def main(): @@ -31,10 +33,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) @@ -55,5 +55,27 @@ def main(): IOLoop.instance().start() +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/monkey.py b/monkey/monkey_island/cc/models/monkey.py index bb018caa9..3e1e3d7c5 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -55,6 +55,14 @@ class Monkey(Document): 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 + class MonkeyNotFoundError(Exception): pass diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py index 008fb0ce6..a744db6b6 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -1,13 +1,13 @@ import uuid from time import sleep -from unittest import TestCase 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(TestCase): +class TestMonkey(IslandTestCase): """ Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and won't work. @@ -15,7 +15,11 @@ class TestMonkey(TestCase): 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_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() @@ -43,6 +47,9 @@ class TestMonkey(TestCase): self.assertFalse(alive_monkey.is_dead()) 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() @@ -52,3 +59,21 @@ class TestMonkey(TestCase): 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()))) diff --git a/monkey/monkey_island/cc/resources/attack/attack_report.py b/monkey/monkey_island/cc/resources/attack/attack_report.py index 714836925..a55137d7e 100644 --- a/monkey/monkey_island/cc/resources/attack/attack_report.py +++ b/monkey/monkey_island/cc/resources/attack/attack_report.py @@ -1,7 +1,7 @@ import flask_restful from flask import jsonify -from cc.auth import jwt_required -from cc.services.attack.attack_report import AttackReportService +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.attack.attack_report import AttackReportService __author__ = "VakarisZ" diff --git a/monkey/monkey_island/cc/resources/attack/attack_telem.py b/monkey/monkey_island/cc/resources/attack/attack_telem.py deleted file mode 100644 index 8c30bb13c..000000000 --- a/monkey/monkey_island/cc/resources/attack/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 AttackTelemService -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) - AttackTelemService.set_results(technique, data) - return {} diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 2f464f068..9a96abe3b 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -95,7 +95,7 @@ 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_catagory': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}, 'monkey_guid': {'$eq': parent}})] if 1 == len(exploit_telem): @@ -104,7 +104,7 @@ class Monkey(flask_restful.Resource): parent_to_add = (parent, None) 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_catagory': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}})] if 1 == len(exploit_telem): diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index f57235feb..2af73a45e 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -8,7 +8,7 @@ from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.report import ReportService -from cc.services.attack.attack_report import AttackReportService +from monkey_island.cc.services.attack.attack_report import AttackReportService from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.services.database import Database diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 5d63a96ca..69b6bcdd4 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -26,7 +26,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_catagory = request.args.get('telem_catagory') timestamp = request.args.get('timestamp') if "null" == timestamp: # special case to avoid ugly JS code... timestamp = None @@ -36,8 +36,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_catagory: + find_filter["telem_catagory"] = {'$eq': telem_catagory} if timestamp: find_filter['timestamp'] = {'$gt': dateutil.parser.parse(timestamp)} @@ -53,11 +53,11 @@ class Telemetry(flask_restful.Resource): 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) + telem_catagory = telemetry_json.get('telem_catagory') + if telem_catagory in TELEM_PROCESS_DICT: + TELEM_PROCESS_DICT[telem_catagory](telemetry_json) else: - logger.info('Got unknown type of telemetry: %s' % telem_type) + logger.info('Got unknown type of telemetry: %s' % telem_catagory) except Exception as ex: logger.error("Exception caught while processing telemetry", exc_info=True) @@ -79,7 +79,7 @@ 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_catagory'] == 'system_info_collection' and 'credentials' in x['data']: for user in x['data']['credentials']: if -1 != user.find(','): new_user = user.replace(',', '.') @@ -265,6 +265,12 @@ class Telemetry(flask_restful.Resource): {'guid': telemetry_json['monkey_guid']}, {'$push': {'pba_results': telemetry_json['data']}}) + @staticmethod + def process_attack_telemetry(telemetry_json): + # No processing required + pass + + TELEM_PROCESS_DICT = \ { 'tunnel': Telemetry.process_tunnel_telemetry, @@ -273,5 +279,6 @@ TELEM_PROCESS_DICT = \ '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 + 'post_breach': Telemetry.process_post_breach_telemetry, + 'attack': Telemetry.process_attack_telemetry } diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index 01fdcc51c..57a655297 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -38,7 +38,7 @@ 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': TELEM_PROCESS_DICT[telem['telem_catagory']](telem) } @staticmethod @@ -86,6 +86,10 @@ class TelemetryFeed(flask_restful.Resource): telem['data']['hostname'], telem['data']['ip']) + @staticmethod + def get_attack_telem_brief(telem): + return 'Monkey collected MITRE ATT&CK info.' + TELEM_PROCESS_DICT = \ { @@ -95,5 +99,6 @@ TELEM_PROCESS_DICT = \ 'scan': TelemetryFeed.get_scan_telem_brief, 'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief, 'trace': TelemetryFeed.get_trace_telem_brief, - 'post_breach': TelemetryFeed.get_post_breach_telem_brief + 'post_breach': TelemetryFeed.get_post_breach_telem_brief, + 'attack': TelemetryFeed.get_attack_telem_brief } diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py index 9a5e57633..f5c0f8765 100644 --- a/monkey/monkey_island/cc/services/attack/attack_report.py +++ b/monkey/monkey_island/cc/services/attack/attack_report.py @@ -30,7 +30,7 @@ class AttackReportService: Generates new report based on telemetries, replaces old report in db with new one. :return: Report object """ - report = {'techniques': {}, 'meta': AttackTelemService.get_latest_telem(), 'name': REPORT_NAME} + report = {'techniques': {}, 'latest_telem_time': AttackReportService.get_latest_attack_telem_time(), 'name': REPORT_NAME} for tech_id, value in AttackConfig.get_technique_values().items(): if value: try: @@ -41,6 +41,14 @@ class AttackReportService: 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_catagory': 'attack'}).sort('timestamp', -1).limit(1)][0] + @staticmethod def get_latest_report(): """ @@ -48,9 +56,9 @@ class AttackReportService: :return: report dict. """ if AttackReportService.is_report_generated(): - telem_time = AttackTelemService.get_latest_telem() + telem_time = AttackReportService.get_latest_attack_telem_time() latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME}) - if telem_time and latest_report['meta'] and telem_time['time'] == latest_report['meta']['time']: + if telem_time and latest_report['latest_telem_time'] and telem_time == latest_report['latest_telem_time']: return latest_report return AttackReportService.generate_new_report() 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 b402bda56..000000000 --- a/monkey/monkey_island/cc/services/attack/attack_telem.py +++ /dev/null @@ -1,30 +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__) - - -class AttackTelemService(object): - def __init__(self): - pass - - @staticmethod - 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) - mongo.db.attack_results.update({'name': 'latest'}, {'name': 'latest', 'time': data['time']}, upsert=True) - - - @staticmethod - def get_latest_telem(): - return mongo.db.attack_results.find_one({'name': 'latest'}) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py index 002e2eeb7..abe1963f3 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py @@ -17,9 +17,10 @@ class T1003(AttackTechnique): @staticmethod def get_report_data(): - data = {'title': T1003.technique_title(T1003.tech_id)} + data = {'title': T1003.technique_title()} if mongo.db.telemetry.count_documents(T1003.query): - data.update({'message': T1003.used_msg, 'status': ScanStatus.USED.name}) + status = ScanStatus.USED else: - data.update({'message': T1003.unscanned_msg, 'status': ScanStatus.UNSCANNED.name}) + status = ScanStatus.UNSCANNED + data.update(T1003.get_message_and_status(status)) 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 index e85e27415..6f126b175 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py @@ -22,9 +22,10 @@ class T1059(AttackTechnique): @staticmethod def get_report_data(): cmd_data = list(mongo.db.telemetry.aggregate(T1059.query)) - data = {'title': T1059.technique_title(T1059.tech_id), 'cmds': cmd_data} + data = {'title': T1059.technique_title(), 'cmds': cmd_data} if cmd_data: - data.update({'message': T1059.used_msg, 'status': ScanStatus.USED.name}) + status = ScanStatus.USED else: - data.update({'message': T1059.unscanned_msg, 'status': ScanStatus.UNSCANNED.name}) + status = ScanStatus.UNSCANNED + data.update(T1059.get_message_and_status(status)) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py index 2ac3d5420..fa65a66c2 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py @@ -16,7 +16,7 @@ class T1075(AttackTechnique): {'lm_hash': {'$ne': ''}}]}}} # Gets data about successful PTH logins - query = [{'$match': {'telem_type': 'exploit', + query = [{'$match': {'telem_category': 'exploit', 'data.attempts': {'$not': {'$size': 0}, '$elemMatch': {'$and': [{'$or': [{'ntlm_hash': {'$ne': ''}}, {'lm_hash': {'$ne': ''}}]}, @@ -31,13 +31,14 @@ class T1075(AttackTechnique): @staticmethod def get_report_data(): - data = {'title': T1075.technique_title(T1075.tech_id)} + data = {'title': T1075.technique_title()} successful_logins = list(mongo.db.telemetry.aggregate(T1075.query)) data.update({'successful_logins': successful_logins}) if successful_logins: - data.update({'message': T1075.used_msg, 'status': ScanStatus.USED.name}) + status = ScanStatus.USED elif mongo.db.telemetry.count_documents(T1075.login_attempt_query): - data.update({'message': T1075.scanned_msg, 'status': ScanStatus.SCANNED.name}) + status = ScanStatus.SCANNED else: - data.update({'message': T1075.unscanned_msg, 'status': ScanStatus.UNSCANNED.name}) + status = ScanStatus.UNSCANNED + data.update(T1075.get_message_and_status(status)) 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 index 040b05be2..0f09fb0fe 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py @@ -35,12 +35,16 @@ class T1110(AttackTechnique): result['successful_creds'].append(T1110.parse_creds(attempt)) if succeeded: - data = {'message': T1110.used_msg, 'status': ScanStatus.USED.name} + status = ScanStatus.USED elif attempts: - data = {'message': T1110.scanned_msg, 'status': ScanStatus.SCANNED.name} + status = ScanStatus.SCANNED else: - data = {'message': T1110.unscanned_msg, 'status': ScanStatus.UNSCANNED.name} - data.update({'services': attempts, 'title': T1110.technique_title(T1110.tech_id)}) + status = ScanStatus.UNSCANNED + data = T1110.get_message_and_status(status) + # Remove data with no successful brute force attempts + attempts = [attempt for attempt in attempts if attempt['attempts']] + + data.update({'services': attempts, 'title': T1110.technique_title()}) return data @staticmethod @@ -51,21 +55,39 @@ class T1110(AttackTechnique): :return: string with username and used password/hash """ username = attempt['user'] - if attempt['lm_hash']: - return '%s ; LM hash %s ...' % (username, encryptor.dec(attempt['lm_hash'])[0:5]) - if attempt['ntlm_hash']: - return '%s ; NTLM hash %s ...' % (username, encryptor.dec(attempt['ntlm_hash'])[0:20]) - if attempt['ssh_key']: - return '%s ; SSH key %s ...' % (username, encryptor.dec(attempt['ssh_key'])[0:15]) - if attempt['password']: - return '%s : %s' % (username, T1110.obfuscate_password(encryptor.dec(attempt['password']))) + creds = {'lm_hash': {'type': 'LM hash', 'output': T1110.censor_hash(attempt['lm_hash'])}, + 'ntlm_hash': {'type': 'NTLM hash', 'output': T1110.censor_hash(attempt['ntlm_hash'], 20)}, + 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']}, + 'password': {'type': 'Plaintext password', 'output': T1110.censor_password(attempt['password'])}} + for key, cred in creds.items(): + if attempt[key]: + return '%s ; %s : %s' % (username, + cred['type'], + cred['output']) @staticmethod - def obfuscate_password(password, plain_chars=3): + def censor_password(password, plain_chars=3, secret_chars=5): """ - Obfuscates password by changing characters to * + 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**** """ - return password[0:plain_chars] + '*' * (len(password) - plain_chars) + if not password: + return "" + password = encryptor.dec(password) + return password[0:plain_chars] + '*' * secret_chars + + @staticmethod + 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/T1197.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py index 0aaab082b..3201bb940 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py @@ -12,13 +12,13 @@ class T1197(AttackTechnique): @staticmethod def get_report_data(): - data = T1197.get_tech_base_data(T1197) - bits_results = mongo.db.attack_results.aggregate([{'$match': {'technique': T1197.tech_id}}, - {'$group': {'_id': {'ip_addr': '$machine.ip_addr', 'usage': '$usage'}, - 'ip_addr': {'$first': '$machine.ip_addr'}, - 'domain_name': {'$first': '$machine.domain_name'}, - 'usage': {'$first': '$usage'}, - 'time': {'$first': '$time'}} + data = T1197.get_tech_base_data() + bits_results = mongo.db.telemetry.aggregate([{'$match': {'telem_catagory': '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}) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py index b6335422f..08019699b 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py @@ -14,21 +14,22 @@ class T1210(AttackTechnique): @staticmethod def get_report_data(): - data = {'title': T1210.technique_title(T1210.tech_id)} + data = {'title': T1210.technique_title()} scanned_services = T1210.get_scanned_services() exploited_services = T1210.get_exploited_services() if exploited_services: - data.update({'status': ScanStatus.USED.name, 'message': T1210.used_msg}) + status = ScanStatus.USED elif scanned_services: - data.update({'status': ScanStatus.SCANNED.name, 'message': T1210.scanned_msg}) + status = ScanStatus.SCANNED else: - data.update({'status': ScanStatus.UNSCANNED.name, 'message': T1210.unscanned_msg}) + status = ScanStatus.UNSCANNED.name + 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_type': 'scan'}}, + results = mongo.db.telemetry.aggregate([{'$match': {'telem_catagory': 'scan'}}, {'$sort': {'data.service_count': -1}}, {'$group': { '_id': {'ip_addr': '$data.machine.ip_addr'}, @@ -38,7 +39,7 @@ class T1210(AttackTechnique): @staticmethod def get_exploited_services(): - results = mongo.db.telemetry.aggregate([{'$match': {'telem_type': 'exploit', 'data.result': True}}, + results = mongo.db.telemetry.aggregate([{'$match': {'telem_catagory': 'exploit', 'data.result': True}}, {'$group': { '_id': {'ip_addr': '$data.machine.ip_addr'}, 'service': {'$first': '$data.info'}, diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py index 0346a1857..fe2beb424 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py @@ -46,43 +46,63 @@ class AttackTechnique(object): """ pass - @staticmethod - def technique_status(technique): + @classmethod + def technique_status(cls): """ Gets the status of a certain attack technique. - :param technique: technique's id. :return: ScanStatus Enum object """ - if mongo.db.attack_results.find_one({'status': ScanStatus.USED.value, 'technique': technique}): + if mongo.db.attack_results.find_one({'telem_catagory': 'attack', + 'status': ScanStatus.USED.value, + 'technique': cls.tech_id}): return ScanStatus.USED - elif mongo.db.attack_results.find_one({'status': ScanStatus.SCANNED.value, 'technique': technique}): + elif mongo.db.attack_results.find_one({'telem_catagory': 'attack', + 'status': ScanStatus.SCANNED.value, + 'technique': cls.tech_id}): return ScanStatus.SCANNED else: return ScanStatus.UNSCANNED - @staticmethod - def technique_title(technique): + @classmethod + def get_message_and_status(cls, status): + """ + Returns a dict with attack technique's message and status. + :param status: Enum type value from common/attack_utils.py + :return: Dict with message and status + """ + return {'message': cls.get_message_by_status(status), 'status': status.name} + + @classmethod + def get_message_by_status(cls, status): + """ + Picks a message to return based on status. + :param status: Enum type value from common/attack_utils.py + :return: message string + """ + if status == ScanStatus.UNSCANNED: + return cls.unscanned_msg + elif status == ScanStatus.SCANNED: + return cls.scanned_msg + else: + return cls.used_msg + + @classmethod + def technique_title(cls): """ - :param technique: Technique's id. E.g. T1110 :return: techniques title. E.g. "T1110 Brute force" """ - return AttackConfig.get_technique(technique)['title'] + return AttackConfig.get_technique(cls.tech_id)['title'] - @staticmethod - def get_tech_base_data(technique): + @classmethod + def get_tech_base_data(cls): """ Gathers basic attack technique data into a dict. - :param technique: Technique's id. E.g. T1110 :return: dict E.g. {'message': 'Brute force used', 'status': 'Used', 'title': 'T1110 Brute force'} """ data = {} - status = AttackTechnique.technique_status(technique.tech_id) - title = AttackTechnique.technique_title(technique.tech_id) - data.update({'status': status.name, 'title': title}) - if status == ScanStatus.UNSCANNED: - data.update({'message': technique.unscanned_msg}) - elif status == ScanStatus.SCANNED: - data.update({'message': technique.scanned_msg}) - else: - data.update({'message': technique.used_msg}) + status = cls.technique_status() + title = cls.technique_title() + data.update({'status': status.name, + 'title': title, + 'message': cls.get_message_by_status(status)}) return data diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 8a96a0d78..46129266c 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -499,17 +499,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" }, } }, diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/pth_report.py index 93fd51989..9f3b9769f 100644 --- a/monkey/monkey_island/cc/services/pth_report.py +++ b/monkey/monkey_island/cc/services/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/report.py index 702a42cd1..2cd0e82fa 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -171,7 +171,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_catagory': 'system_info_collection', 'data.credentials': {'$exists': True}}, {'data.credentials': 1, 'monkey_guid': 1} ): monkey_creds = telem['data']['credentials'] @@ -199,7 +199,7 @@ class ReportService: """ creds = [] for telem in mongo.db.telemetry.find( - {'telem_type': 'system_info_collection', 'data.ssh_info': {'$exists': True}}, + {'telem_catagory': 'system_info_collection', 'data.ssh_info': {'$exists': True}}, {'data.ssh_info': 1, 'monkey_guid': 1} ): origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] @@ -220,7 +220,7 @@ class ReportService: """ creds = [] for telem in mongo.db.telemetry.find( - {'telem_type': 'system_info_collection', 'data.Azure': {'$exists': True}}, + {'telem_catagory': 'system_info_collection', 'data.Azure': {'$exists': True}}, {'data.Azure': 1, 'monkey_guid': 1} ): azure_users = telem['data']['Azure']['usernames'] @@ -373,7 +373,7 @@ class ReportService: @staticmethod def get_exploits(): exploits = [] - for exploit in mongo.db.telemetry.find({'telem_type': 'exploit', 'data.result': True}): + for exploit in mongo.db.telemetry.find({'telem_catagory': 'exploit', 'data.result': True}): new_exploit = ReportService.process_exploit(exploit) if new_exploit not in exploits: exploits.append(new_exploit) @@ -382,7 +382,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_catagory': 'system_info_collection', 'monkey_guid': monkey_guid}, {'data.network_info.networks': 1} ) if network_info is None: @@ -540,7 +540,7 @@ class ReportService: @staticmethod def get_cross_segment_issues(): - scans = mongo.db.telemetry.find({'telem_type': 'scan'}, + scans = mongo.db.telemetry.find({'telem_catagory': 'scan'}, {'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1}) cross_segment_issues = [] diff --git a/monkey/monkey_island/cc/services/test_PTHReportService.py b/monkey/monkey_island/cc/services/test_PTHReportService.py new file mode 100644 index 000000000..24e560ff0 --- /dev/null +++ b/monkey/monkey_island/cc/services/test_PTHReportService.py @@ -0,0 +1,69 @@ +import uuid + +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.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/testing/IslandTestCase.py b/monkey/monkey_island/cc/testing/IslandTestCase.py new file mode 100644 index 000000000..e894f13df --- /dev/null +++ b/monkey/monkey_island/cc/testing/IslandTestCase.py @@ -0,0 +1,12 @@ +import unittest +from monkey_island.cc.environment.environment import env +from monkey_island.cc.models import Monkey + + +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() 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.json b/monkey/monkey_island/cc/ui/package.json index 6f0b58560..ca13c259d 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", @@ -100,6 +101,7 @@ "sass-loader": "^7.1.0", "sha3": "^2.0.0", "react-spinners": "^0.5.4", - "@emotion/core": "^10.0.10" + "@emotion/core": "^10.0.10", + "react-desktop-notification": "^1.0.9" } } diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 8229133e6..619d5e922 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -14,6 +14,8 @@ import ReportPage from 'components/pages/ReportPage'; 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,7 @@ 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'); class AppComponent extends AuthComponent { updateStatus = () => { @@ -50,6 +53,7 @@ class AppComponent extends AuthComponent { } if (isChanged) { this.setState({completedSteps: res['completed_steps']}); + this.showInfectionDoneNotification(); } }); } @@ -194,6 +198,20 @@ class AppComponent extends AuthComponent { ); } + + showInfectionDoneNotification() { + if (this.state.completedSteps.infection_done) { + let hostname = window.location.hostname; + let url = `https://${hostname}:5000/report`; + console.log("Trying to show notification. URL: " + url + " | icon: " + notificationIcon); + + Notifier.start( + "Monkey Island", + "Infection is done! Click here to go to the report page.", + url, + notificationIcon); + } + } } AppComponent.defaultProps = {}; 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 index b15bba693..785e4a295 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js @@ -1,6 +1,6 @@ import React from "react"; -export function RenderMachine(val){ +export function renderMachine(val){ return ( {val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")} ) 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 index d1c809651..5678b8c14 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js @@ -10,7 +10,7 @@ class T1059 extends React.Component { super(props); } - static getHashColumns() { + static getCommandColumns() { return ([{ Header: 'Example commands used', columns: [ @@ -27,7 +27,7 @@ class T1059 extends React.Component {
{this.props.data.status === 'USED' ? { - if(login.attempts[0].ntlm_hash !== ""){ - login.attempts[0].hashType = 'NTLM'; - } else if(login.attempts[0].lm_hash !== ""){ - login.attempts[0].hashType = 'LM'; - } - }) + 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: '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' }}, 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 index 468a9e79a..294606d25 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import { RenderMachine } from "./Helpers" +import { renderMachine } from "./Helpers" class T1110 extends React.Component { @@ -13,7 +13,7 @@ class T1110 extends React.Component { static getServiceColumns() { return ([{ columns: [ - {Header: 'Machine', id: 'machine', accessor: x => RenderMachine(x.machine), + {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' }}, 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 index e4c7d841d..8dc655aee 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1197.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1197.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import { RenderMachine } from "./Helpers" +import { renderMachine } from "./Helpers" class T1210 extends React.Component { @@ -9,7 +9,7 @@ class T1210 extends React.Component { constructor(props) { super(props); this.columns = [ {Header: 'Machine', - id: 'machine', accessor: x => RenderMachine(x), + id: 'machine', accessor: x => renderMachine(x), style: { 'whiteSpace': 'unset' }, width: 200}, {Header: 'Time', 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 index 76bc5d196..1b3daa86c 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import { RenderMachine } from "./Helpers" +import { renderMachine } from "./Helpers" class T1210 extends React.Component { @@ -13,7 +13,7 @@ class T1210 extends React.Component { static getScanColumns() { return ([{ columns: [ - {Header: 'Machine', id: 'machine', accessor: x => RenderMachine(x.machine), + {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' }, width: 170}, {Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }}, @@ -24,7 +24,7 @@ class T1210 extends React.Component { static getExploitColumns() { return ([{ columns: [ - {Header: 'Machine', id: 'machine', accessor: x => RenderMachine(x.machine), + {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' }, width: 170}, {Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }}, diff --git a/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js b/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js index a23dd1d36..120344eea 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js @@ -11,7 +11,7 @@ const renderTime = (val) => val.split('.')[0]; const columns = [ { title: 'Time', prop: 'timestamp', render: renderTime}, { title: 'Monkey', prop: 'monkey'}, - { title: 'Type', prop: 'telem_type'}, + { title: 'Type', prop: 'telem_catagory'}, { title: 'Details', prop: 'data', render: renderJson, width: '40%' } ]; 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