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/monkey/common/utils/attack_utils.py b/monkey/common/utils/attack_utils.py index 77d813351..708bc8f3c 100644 --- a/monkey/common/utils/attack_utils.py +++ b/monkey/common/utils/attack_utils.py @@ -18,6 +18,10 @@ class UsageEnum(Enum): 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 diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index f7e4cfae4..cb5bf881b 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,9 +14,11 @@ 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') @@ -53,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): @@ -174,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, @@ -272,5 +281,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 ba30bf515..4e917e5a6 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -169,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", diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index e41ad8681..7c576fc30 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -158,6 +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.name).send() + T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_WINAPI).send() except AttributeError: LOG.error("Invalid configuration options. Failing") diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index bac923063..e4eaf3151 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -124,8 +124,9 @@ 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() diff --git a/monkey/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py index 70b5da262..0cf225637 100644 --- a/monkey/infection_monkey/exploit/rdpgrinder.py +++ b/monkey/infection_monkey/exploit/rdpgrinder.py @@ -9,6 +9,8 @@ from rdpy.core.error import RDPSecurityNegoFail from rdpy.protocol.rdp import rdp from twisted.internet import reactor +from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING +from common.utils.exploit_enum import ExploitType from infection_monkey.exploit import HostExploiter from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey, build_monkey_commandline from infection_monkey.exploit.tools.http_tools import HTTPTools @@ -16,8 +18,6 @@ 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.telemetry.attack.t1197_telem import T1197Telem from infection_monkey.utils import utf_to_ascii -from common.utils.exploit_enum import ExploitType -from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING __author__ = 'hoffer' @@ -298,8 +298,8 @@ class RdpExploiter(HostExploiter): 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("Trying RDP logging into victim %r with user %s and password (SHA-512) '%s'", + self.host, user, self._config.hash_sensitive_data(password)) LOG.info("RDP connected to %r", self.host) @@ -326,8 +326,8 @@ class RdpExploiter(HostExploiter): except Exception as exc: LOG.debug("Error logging into victim %r with user" - " %s and password '%s': (%s)", self.host, - user, password, exc) + " %s and password (SHA-512) '%s': (%s)", self.host, + user, self._config.hash_sensitive_data(password), exc) continue http_thread.join(DOWNLOAD_TIMEOUT) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 3b09cbf48..0a17d7622 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -68,8 +68,8 @@ 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 : %s : %s)", + self.host, user, self._config.hash_sensitive_data(password), lm_hash, 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])) @@ -81,8 +81,8 @@ class SmbExploiter(HostExploiter): 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) + " %s, password (SHA-512): '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host, + user, self._config.hash_sensitive_data(password), lm_hash, ntlm_hash, exc) continue if not exploited: @@ -137,7 +137,7 @@ class SmbExploiter(HostExploiter): except: status = ScanStatus.SCANNED pass - T1035Telem(status, UsageEnum.SMB.name).send() + T1035Telem(status, UsageEnum.SMB).send() scmr.hRDeleteService(scmr_rpc, service) scmr.hRCloseServiceHandle(scmr_rpc, service) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index a08da4f45..ffd584d24 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -1,10 +1,11 @@ +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.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline from infection_monkey.exploit.tools.helpers import get_interface_to_target @@ -74,26 +75,26 @@ 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.add_vuln_port(port) - self.report_login_attempt(True, user, curpass) + 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 @@ -112,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) @@ -165,18 +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() - T1105Telem(ScanStatus.USED, - get_interface_to_target(self.host.ip_addr), - self.host.ip_addr, - src_path).send() ftp.close() except Exception as exc: LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc) - T1105Telem(ScanStatus.SCANNED, - get_interface_to_target(self.host.ip_addr), - self.host.ip_addr, - src_path).send() + 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: diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 83a8bfd92..bc74128e2 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -19,6 +19,7 @@ def get_interface_to_target(dst): 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() diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index c9287a25e..1f3e1cecc 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -36,8 +36,10 @@ 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) + LOG.debug("Attempting to connect %r using WMI with " + "user,password (SHA-512),lm hash,ntlm hash: ('%s','%s','%s','%s')", + self.host, user, password_hashed, lm_hash, ntlm_hash) wmi_connection = WmiTools.WmiConnection() @@ -47,23 +49,23 @@ class WmiExploiter(HostExploiter): 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) + self.host, user, password_hashed, lm_hash, ntlm_hash) 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) + self.host, user, password_hashed, lm_hash, ntlm_hash) 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) + self.host, user, password_hashed, lm_hash, ntlm_hash) 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()) + self.host, user, password_hashed, lm_hash, ntlm_hash, exc, traceback.format_exc()) return False self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) @@ -94,7 +96,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) @@ -121,3 +124,4 @@ class WmiExploiter(HostExploiter): return success return False + diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index b8e292243..2ddf9127e 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -68,7 +68,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( diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 50af09e4a..ce5ab2093 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -184,7 +184,7 @@ class InfectionMonkey(object): (':'+self._default_server_port if self._default_server_port else '')) else: machine.set_default_server(self._default_server) - LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine)) + LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server)) # Order exploits according to their type if WormConfiguration.should_exploit: @@ -255,6 +255,7 @@ class InfectionMonkey(object): 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() @@ -265,10 +266,12 @@ class InfectionMonkey(object): close_fds=True, startupinfo=startupinfo) else: os.remove(sys.executable) - T1107Telem(ScanStatus.USED, sys.executable).send() + status = ScanStatus.USED except Exception as exc: LOG.error("Exception in self delete: %s", exc) - T1107Telem(ScanStatus.SCANNED, sys.executable).send() + status = ScanStatus.SCANNED + if status: + T1107Telem(status, sys.executable).send() def send_log(self): monkey_log_path = utils.get_monkey_log_path() diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index d923cb60e..a388813ab 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -82,17 +82,22 @@ class UsersPBA(PBA): 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.") - T1105Telem(ScanStatus.SCANNED, - WormConfiguration.current_server.split(':')[0], - get_interface_to_target(WormConfiguration.current_server.split(':')[0]), - filename).send() - return False - T1105Telem(ScanStatus.USED, + 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) diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py index 0e6a913c9..2951b7ebc 100644 --- a/monkey/infection_monkey/system_info/mimikatz_collector.py +++ b/monkey/infection_monkey/system_info/mimikatz_collector.py @@ -55,9 +55,8 @@ class MimikatzCollector(object): except Exception: LOG.exception("Error initializing mimikatz collector") status = ScanStatus.SCANNED - T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI.name).send() - T1129Telem(status, UsageEnum.MIMIKATZ.name).send() - + T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI).send() + T1129Telem(status, UsageEnum.MIMIKATZ).send() def get_logon_info(self): """ diff --git a/monkey/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py index d8c3ec51c..50fa6363b 100644 --- a/monkey/infection_monkey/system_singleton.py +++ b/monkey/infection_monkey/system_singleton.py @@ -4,6 +4,8 @@ import sys from abc import ABCMeta, abstractmethod from infection_monkey.config import WormConfiguration +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem +from common.utils.attack_utils import ScanStatus, UsageEnum __author__ = 'itamar' @@ -43,14 +45,22 @@ class WindowsSystemSingleton(_SystemSingleton): ctypes.c_bool(True), ctypes.c_char_p(self._mutex_name)) last_error = ctypes.windll.kernel32.GetLastError() + + status = None if not handle: LOG.error("Cannot acquire system singleton %r, unknown error %d", self._mutex_name, last_error) - return False + status = ScanStatus.SCANNED if winerror.ERROR_ALREADY_EXISTS == last_error: + status = ScanStatus.SCANNED LOG.debug("Cannot acquire system singleton %r, mutex already exist", self._mutex_name) + + if not status: + status = ScanStatus.USED + T1106Telem(status, UsageEnum.SINGLETON_WINAPI).send() + if status == ScanStatus.SCANNED: return False self._mutex_handle = handle diff --git a/monkey/infection_monkey/telemetry/attack/t1005_telem.py b/monkey/infection_monkey/telemetry/attack/t1005_telem.py index 228ccb67c..999d8622a 100644 --- a/monkey/infection_monkey/telemetry/attack/t1005_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1005_telem.py @@ -2,21 +2,21 @@ from infection_monkey.telemetry.attack.attack_telem import AttackTelem class T1005Telem(AttackTelem): - def __init__(self, status, _type, info=""): + def __init__(self, status, gathered_data_type, info=""): """ T1005 telemetry. :param status: ScanStatus of technique - :param _type: Type of data collected + :param gathered_data_type: Type of data collected from local system :param info: Additional info about data """ super(T1005Telem, self).__init__('T1005', status) - self._type = _type + self.gathered_data_type = gathered_data_type self.info = info def get_data(self): data = super(T1005Telem, self).get_data() data.update({ - 'type': self._type, + '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 index 13d0bcc59..4ca9dc93c 100644 --- a/monkey/infection_monkey/telemetry/attack/t1035_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1035_telem.py @@ -6,6 +6,6 @@ class T1035Telem(UsageTelem): """ T1035 telemetry. :param status: ScanStatus of technique - :param usage: Enum name of UsageEnum + :param usage: Enum of UsageEnum type """ super(T1035Telem, self).__init__('T1035', status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1129_telem.py b/monkey/infection_monkey/telemetry/attack/t1129_telem.py index a04834959..4e7a12ce8 100644 --- a/monkey/infection_monkey/telemetry/attack/t1129_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1129_telem.py @@ -6,6 +6,6 @@ class T1129Telem(UsageTelem): """ T1129 telemetry. :param status: ScanStatus of technique - :param usage: Enum name of UsageEnum + :param usage: Enum of UsageEnum type """ super(T1129Telem, self).__init__("T1129", status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1222_telem.py b/monkey/infection_monkey/telemetry/attack/t1222_telem.py index c8d16061e..4708c230a 100644 --- a/monkey/infection_monkey/telemetry/attack/t1222_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1222_telem.py @@ -7,6 +7,7 @@ class T1222Telem(VictimHostTelem): 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 diff --git a/monkey/infection_monkey/telemetry/attack/usage_telem.py b/monkey/infection_monkey/telemetry/attack/usage_telem.py index d493c64d8..4b47d8be3 100644 --- a/monkey/infection_monkey/telemetry/attack/usage_telem.py +++ b/monkey/infection_monkey/telemetry/attack/usage_telem.py @@ -7,10 +7,10 @@ class UsageTelem(AttackTelem): """ :param technique: Id of technique :param status: ScanStatus of technique - :param usage: Enum name of UsageEnum + :param usage: Enum of UsageEnum type """ super(UsageTelem, self).__init__(technique, status) - self.usage = usage + self.usage = usage.name def get_data(self): data = super(UsageTelem, self).get_data() 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/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index 9f82e472d..58e950914 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -16,5 +16,5 @@ from config import Config from creds import Creds from monkey_ttl import MonkeyTtl from pba_results import PbaResults -from c2_info import C2Info +from command_control_channel import CommandControlChannel from monkey import Monkey diff --git a/monkey/monkey_island/cc/models/c2_info.py b/monkey/monkey_island/cc/models/c2_info.py deleted file mode 100644 index d0f07a3f3..000000000 --- a/monkey/monkey_island/cc/models/c2_info.py +++ /dev/null @@ -1,6 +0,0 @@ -from mongoengine import EmbeddedDocument, StringField - - -class C2Info(EmbeddedDocument): - src = StringField() - dst = StringField() 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/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 6c4c90214..381dcbf2d 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -1,11 +1,12 @@ """ Define a Document Schema for the Monkey document. """ -import mongoengine from mongoengine import Document, StringField, ListField, BooleanField, EmbeddedDocumentField, ReferenceField, \ - DateTimeField + DateTimeField, DynamicField, DoesNotExist -from monkey_island.cc.models.monkey_ttl import MonkeyTtl +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 class Monkey(Document): @@ -26,26 +27,38 @@ class Monkey(Document): ip_addresses = ListField(StringField()) keepalive = DateTimeField() modifytime = DateTimeField() - # TODO change this to an embedded document as well - RN it's an unnamed tuple which is confusing. - parent = ListField(ListField(StringField())) + # 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") - c2_info = EmbeddedDocumentField('C2Info') + command_control_channel = EmbeddedDocumentField(CommandControlChannel) # LOGIC @staticmethod def get_single_monkey_by_id(db_id): try: - return Monkey.objects(id=db_id)[0] - except IndexError: - raise MonkeyNotFoundError("id: {0}".format(str(db_id))) + 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(): - return Monkey.objects.order_by('-modifytime').first().modifytime + if Monkey.objects.count() > 0: + return Monkey.objects.order_by('-modifytime').first().modifytime + return None def is_dead(self): monkey_is_dead = False @@ -56,7 +69,7 @@ class Monkey(Document): 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 (mongoengine.DoesNotExist, AttributeError): + except (DoesNotExist, AttributeError): # Trying to dereference unknown document - the monkey is MIA. monkey_is_dead = True return monkey_is_dead @@ -69,6 +82,10 @@ class Monkey(Document): os = "windows" return os + def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS): + self.ttl_ref = create_monkey_ttl_document(duration) + self.save() + @staticmethod def get_tunneled_monkeys(): return Monkey.objects(tunnel__exists=True) diff --git a/monkey/monkey_island/cc/models/monkey_ttl.py b/monkey/monkey_island/cc/models/monkey_ttl.py index 9ccf77974..b3e59d5ed 100644 --- a/monkey/monkey_island/cc/models/monkey_ttl.py +++ b/monkey/monkey_island/cc/models/monkey_ttl.py @@ -38,3 +38,16 @@ class MonkeyTtl(Document): } 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/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py index a744db6b6..717fb309a 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -46,6 +46,19 @@ class TestMonkey(IslandTestCase): 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() diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 36720e465..8e523a8a7 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -5,26 +5,17 @@ import dateutil.parser 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 MonkeyTtl +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 -MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5 - __author__ = 'Barak' # TODO: separate logic from interface -def create_monkey_ttl(): - # The TTL data uses the new `models` module which depends on mongoengine. - current_ttl = MonkeyTtl.create_ttl_expire_in(MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) - current_ttl.save() - ttlid = current_ttl.id - return ttlid - - class Monkey(flask_restful.Resource): # Used by monkey. can't secure. @@ -58,8 +49,8 @@ class Monkey(flask_restful.Resource): tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip) - ttlid = create_monkey_ttl() - update['$set']['ttl_ref'] = ttlid + 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) @@ -120,7 +111,8 @@ class Monkey(flask_restful.Resource): tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") monkey_json.pop('tunnel') - monkey_json['ttl_ref'] = create_monkey_ttl() + 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}, diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 7a34c13de..fa942d174 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -15,10 +15,10 @@ 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.models.monkey import Monkey __author__ = 'Barak' - logger = logging.getLogger(__name__) @@ -48,7 +48,10 @@ class Telemetry(flask_restful.Resource): def post(self): telemetry_json = json.loads(request.data) telemetry_json['timestamp'] = datetime.now() - telemetry_json['c2_channel'] = {'src': request.remote_addr, 'dst': request.host} + 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']) @@ -60,7 +63,7 @@ class Telemetry(flask_restful.Resource): else: logger.info('Got unknown type of telemetry: %s' % telem_category) except Exception as ex: - logger.error("Exception caught while processing telemetry", exc_info=True) + logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True) telem_id = mongo.db.telemetry.insert(telemetry_json) return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) @@ -111,7 +114,7 @@ class Telemetry(flask_restful.Resource): @staticmethod def process_state_telemetry(telemetry_json): monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - NodeService.add_communication_info(monkey, telemetry_json['c2_channel']) + NodeService.add_communication_info(monkey, telemetry_json['command_control_channel']) if telemetry_json['data']['done']: NodeService.set_monkey_dead(monkey, True) else: @@ -190,7 +193,7 @@ class Telemetry(flask_restful.Resource): 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.\ + 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) diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index a4f090758..e271c45c5 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], + '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): diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py index 06f408784..b84fe4a6f 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py @@ -19,12 +19,12 @@ class T1005(AttackTechnique): 'as': 'monkey'}}, {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, 'status': '$data.status', - 'type': '$data.type', + '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', 'type': '$type', 'info': '$info'}}}, + {'$group': {'_id': {'machine': '$machine', 'gathered_data_type': '$gathered_data_type', 'info': '$info'}}}, {"$replaceRoot": {"newRoot": "$_id"}}] @staticmethod diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py index 4525fd035..43d7c42b0 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py @@ -21,7 +21,7 @@ class T1016(AttackTechnique): 'networks': 0, 'info': [ {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$netstat', {}]}]}, - 'name': {'$literal': 'Network connections (via netstat command)'}}, + 'name': {'$literal': 'Network connections (netstat)'}}, {'used': {'$and': [{'$ifNull': ['$networks', False]}, {'$gt': ['$networks', {}]}]}, 'name': {'$literal': 'Network interface info'}}, ]}}] @@ -29,10 +29,7 @@ class T1016(AttackTechnique): @staticmethod def get_report_data(): network_info = list(mongo.db.telemetry.aggregate(T1016.query)) - if network_info: - status = ScanStatus.USED.value - else: - status = ScanStatus.UNSCANNED.value + 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/T1021.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py index 6f69f39ab..2baa7a872 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py @@ -1,7 +1,8 @@ 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.T1110 import T1110 +from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds + __author__ = "VakarisZ" @@ -44,7 +45,7 @@ class T1021(AttackTechnique): for result in attempts: result['successful_creds'] = [] for attempt in result['attempts']: - result['successful_creds'].append(T1110.parse_creds(attempt)) + result['successful_creds'].append(parse_creds(attempt)) else: status = ScanStatus.SCANNED.value else: diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py index 741ee2ae9..1342b646e 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py @@ -15,13 +15,13 @@ class T1041(AttackTechnique): @staticmethod def get_report_data(): monkeys = list(Monkey.objects()) - info = [{'src': monkey['c2_info']['src'], - 'dst': monkey['c2_info']['dst']} - for monkey in monkeys if monkey['c2_info']] + 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({'c2_info': info}) + data.update({'command_control_channel': info}) 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 b918de7f4..72bb0af76 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py @@ -1,7 +1,7 @@ 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.encryptor import encryptor +from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds __author__ = "VakarisZ" @@ -32,7 +32,7 @@ class T1110(AttackTechnique): result['successful_creds'] = [] for attempt in result['attempts']: succeeded = True - result['successful_creds'].append(T1110.parse_creds(attempt)) + result['successful_creds'].append(parse_creds(attempt)) if succeeded: status = ScanStatus.USED.value @@ -47,47 +47,4 @@ class T1110(AttackTechnique): data.update({'services': attempts}) return data - @staticmethod - 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': 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 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 - - @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/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py index cc702de62..ec5ee7781 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py @@ -124,8 +124,8 @@ class UsageTechnique(AttackTechnique): def parse_usages(usage): """ Parses data from database and translates usage enums into strings - :param usage: - :return: + :param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1} + :return: usage string """ try: usage['usage'] = UsageEnum[usage['usage']].value[usage['status']] 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/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index ea538c945..3a7398663 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -389,7 +389,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": { diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index c7b82cbfa..2c75d7187 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -250,7 +250,7 @@ class NodeService: @staticmethod def add_communication_info(monkey, info): mongo.db.monkey.update({"guid": monkey["guid"]}, - {"$set": {'c2_info': info}}, + {"$set": {'command_control_channel': info}}, upsert=False) @staticmethod 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 3331fbeaf..3025b4bc9 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 @@ -36,20 +36,20 @@ export function getUsageColumns() { style: { 'whiteSpace': 'unset' }}] }])} -/* Renders fields that contains 'used' boolean value and 'name' string value. +/* Renders table fields that contains 'used' boolean value and 'name' string value. 'Used' value determines if 'name' value will be shown. */ -export function renderCollections(info){ +export function renderUsageFields(usages){ let output = []; - info.forEach(function(collection){ - if(collection['used']){ - output.push(
{collection['name']}
) + usages.forEach(function(usage){ + if(usage['used']){ + output.push(
{usage['name']}
) } }); return (
{output}
); } -export const scanStatus = { +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 index 208840cf3..07fd4a400 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js @@ -2,7 +2,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import '../../report-components/StolenPasswords' import StolenPasswordsComponent from "../../report-components/StolenPasswords"; -import {scanStatus} from "./Helpers" +import {ScanStatus} from "./Helpers" class T1003 extends React.Component { @@ -16,7 +16,7 @@ class T1003 extends React.Component {
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ? : ""}
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 index 6746d16ed..6d46c2285 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import {renderMachineFromSystemData, scanStatus} from "./Helpers"; +import {renderMachineFromSystemData, ScanStatus} from "./Helpers"; class T1005 extends React.Component { @@ -11,10 +11,10 @@ class T1005 extends React.Component { static getDataColumns() { return ([{ - Header: "Data gathered from local systems", + Header: "Sensitive data", columns: [ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, - {Header: 'Type', id: 'type', accessor: x => x.type, 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' }}, ]}])}; @@ -23,7 +23,7 @@ class T1005 extends React.Component {
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ? renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, - {Header: 'Network info', id: 'info', accessor: x => renderCollections(x.info), style: { 'whiteSpace': 'unset' }}, + {Header: 'Network info', id: 'info', accessor: x => renderUsageFields(x.info), style: { 'whiteSpace': 'unset' }}, ] }])}; @@ -24,7 +24,7 @@ class T1016 extends React.Component {
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ? renderMachineFromSystemData(x.monkey), style: { 'whiteSpace': 'unset' }}, - {Header: 'Started', id: 'started', accessor: x => x.started, style: { 'whiteSpace': 'unset' }}, - {Header: 'Finished', id: 'finished', accessor: x => x.finished, 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' }}, ] }])}; @@ -33,7 +33,7 @@ class T1018 extends React.Component {
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ?
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ?
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ? : ""}
); 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 8d5585829..4651f5c41 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 @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' import ReactTable from "react-table"; -import { renderMachine, scanStatus } from "./Helpers" +import { renderMachine, ScanStatus } from "./Helpers" class T1059 extends React.Component { @@ -25,7 +25,7 @@ class T1059 extends React.Component {
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ?
{this.props.data.message}

- {this.props.data.status !== scanStatus.UNSCANNED ? + {this.props.data.status === ScanStatus.USED ? renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }}, - {Header: 'Gathered info', id: 'info', accessor: x => renderCollections(x.collections), style: { 'whiteSpace': 'unset' }}, + {Header: 'Gathered info', id: 'info', accessor: x => renderUsageFields(x.collections), style: { 'whiteSpace': 'unset' }}, ] }])}; @@ -23,7 +23,7 @@ class T1082 extends React.Component {
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ?
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ?
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ? x.src, style: { 'whiteSpace': 'unset'}, width: 170 }, {Header: 'Dst. Machine', id: 'dstMachine', accessor: x => x.dst, style: { 'whiteSpace': 'unset'}, width: 170}, @@ -25,7 +25,7 @@ class T1105 extends React.Component {
{this.props.data.message}

- {this.props.data.status !== scanStatus.UNSCANNED ? + {this.props.data.status !== ScanStatus.UNSCANNED ? Yes } else { return No 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 0519199b4..da9682da3 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, scanStatus } from "./Helpers" +import { renderMachine, ScanStatus } from "./Helpers" class T1110 extends React.Component { @@ -32,7 +32,7 @@ class T1110 extends React.Component {
{this.props.data.message}

- {this.props.data.status !== scanStatus.UNSCANNED ? + {this.props.data.status !== ScanStatus.UNSCANNED ?
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ?
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ?
{this.props.data.message}

- {this.props.data.status === scanStatus.USED ? + {this.props.data.status === ScanStatus.USED ?

- Not sure what this is? Not seeing your AWS EC2 instances? Read the documentation! + Not sure what this is? Not seeing your AWS EC2 instances? Read the documentation!

{ diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js index e7e4d7850..10381fa5e 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js @@ -4,7 +4,7 @@ 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 {ScanStatus} from "../attack/techniques/Helpers"; import Collapse from '@kunukn/react-collapse'; import T1210 from '../attack/techniques/T1210'; import T1197 from '../attack/techniques/T1197'; @@ -106,9 +106,9 @@ class AttackReportPageComponent extends AuthComponent { getComponentClass(tech_id){ switch (this.state.report[tech_id].status) { - case scanStatus.SCANNED: + case ScanStatus.SCANNED: return 'collapse-info'; - case scanStatus.USED: + case ScanStatus.USED: return 'collapse-danger'; default: return 'collapse-default'; 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()