Merge remote-tracking branch 'upstream/develop' into attack_execution_api

# Conflicts:
#	monkey/infection_monkey/system_info/mimikatz_collector.py
This commit is contained in:
VakarisZ 2019-08-02 09:49:27 +03:00
commit cefe9a7d9c
37 changed files with 208 additions and 109 deletions

View File

@ -3,12 +3,6 @@ language: python
cache: pip cache: pip
python: python:
- 2.7 - 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: install:
#- pip install -r requirements.txt #- pip install -r requirements.txt
- pip install flake8 # pytest # add another testing frameworks later - pip install flake8 # pytest # add another testing frameworks later

View File

@ -13,9 +13,10 @@ Don't forget to add python to PATH or do so while installing it via this script.
## Linux ## Linux
You must have root permissions, but there is no need to run the script as root.<br> You must have root permissions, but don't run the script as root.<br>
Launch deploy_linux.sh from scripts directory.<br> Launch deploy_linux.sh from scripts directory.<br>
First argument is an empty directory (script can create one) and second is branch you want to clone. First argument should be an empty directory (script can create one, default is ./infection_monkey) and second is the branch you want to clone (develop by default).
Choose a directory where you have all the relevant permissions, for e.g. /home/your_username
Example usages:<br> Example usages:<br>
./deploy_linux.sh (deploys under ./infection_monkey)<br> ./deploy_linux.sh (deploys under ./infection_monkey)<br>
./deploy_linux.sh "/home/test/monkey" (deploys under /home/test/monkey)<br> ./deploy_linux.sh "/home/test/monkey" (deploys under /home/test/monkey)<br>

View File

@ -1,3 +1,4 @@
import hashlib
import os import os
import json import json
import sys 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') 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): class Configuration(object):
def from_kv(self, formatted_data): def from_kv(self, formatted_data):
# now we won't work at <2.7 for sure # now we won't work at <2.7 for sure
network_import = importlib.import_module('infection_monkey.network') network_import = importlib.import_module('infection_monkey.network')
@ -53,6 +56,12 @@ class Configuration(object):
result = self.from_kv(formatted_data) result = self.from_kv(formatted_data)
return result 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): def as_dict(self):
result = {} result = {}
for key in dir(Configuration): for key in dir(Configuration):
@ -272,5 +281,17 @@ class Configuration(object):
PBA_linux_filename = None PBA_linux_filename = None
PBA_windows_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() WormConfiguration = Configuration()

View File

@ -169,7 +169,8 @@ class ControlClient(object):
try: try:
unknown_variables = WormConfiguration.from_kv(reply.json().get('config')) 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: except Exception as exc:
# we don't continue with default conf here because it might be dangerous # 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", LOG.error("Error parsing JSON reply from control server %s (%s): %s",

View File

@ -123,8 +123,9 @@ class MSSQLExploiter(HostExploiter):
# Core steps # Core steps
# Trying to connect # Trying to connect
conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT) conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT)
LOG.info('Successfully connected to host: {0}, ' LOG.info(
'using user: {1}, password: {2}'.format(host, user, password)) '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.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT)
self.report_login_attempt(True, user, password) self.report_login_attempt(True, user, password)
cursor = conn.cursor() cursor = conn.cursor()

View File

@ -9,16 +9,16 @@ from rdpy.core.error import RDPSecurityNegoFail
from rdpy.protocol.rdp import rdp from rdpy.protocol.rdp import rdp
from twisted.internet import reactor 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 import HostExploiter
from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth
from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.exploit.tools import get_target_monkey 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.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from infection_monkey.network.tools import check_tcp_port 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.telemetry.attack.t1197_telem import T1197Telem
from infection_monkey.utils import utf_to_ascii 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' __author__ = 'hoffer'
@ -299,8 +299,8 @@ class RdpExploiter(HostExploiter):
for user, password in user_password_pairs: for user, password in user_password_pairs:
try: try:
# run command using rdp. # run command using rdp.
LOG.info("Trying RDP logging into victim %r with user %s and password '%s'", LOG.info("Trying RDP logging into victim %r with user %s and password (SHA-512) '%s'",
self.host, user, password) self.host, user, self._config.hash_sensitive_data(password))
LOG.info("RDP connected to %r", self.host) LOG.info("RDP connected to %r", self.host)
@ -327,8 +327,8 @@ class RdpExploiter(HostExploiter):
except Exception as exc: except Exception as exc:
LOG.debug("Error logging into victim %r with user" LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", self.host, " %s and password (SHA-512) '%s': (%s)", self.host,
user, password, exc) user, self._config.hash_sensitive_data(password), exc)
continue continue
http_thread.join(DOWNLOAD_TIMEOUT) http_thread.join(DOWNLOAD_TIMEOUT)

View File

@ -68,8 +68,8 @@ class SmbExploiter(HostExploiter):
self._config.smb_download_timeout) self._config.smb_download_timeout)
if remote_full_path is not None: if remote_full_path is not None:
LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)", LOG.debug("Successfully logged in %r using SMB (%s : (SHA-512) %s : %s : %s)",
self.host, user, password, lm_hash, ntlm_hash) 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.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1], self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1])) SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
@ -81,8 +81,8 @@ class SmbExploiter(HostExploiter):
except Exception as exc: except Exception as exc:
LOG.debug("Exception when trying to copy file using SMB to %r with user:" 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, " %s, password (SHA-512): '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host,
user, password, lm_hash, ntlm_hash, exc) user, self._config.hash_sensitive_data(password), lm_hash, ntlm_hash, exc)
continue continue
if not exploited: if not exploited:
@ -137,7 +137,7 @@ class SmbExploiter(HostExploiter):
except: except:
status = ScanStatus.SCANNED status = ScanStatus.SCANNED
pass pass
T1035Telem(status, UsageEnum.SMB.name).send() T1035Telem(status, UsageEnum.SMB).send()
scmr.hRDeleteService(scmr_rpc, service) scmr.hRDeleteService(scmr_rpc, service)
scmr.hRCloseServiceHandle(scmr_rpc, service) scmr.hRCloseServiceHandle(scmr_rpc, service)

View File

@ -1,16 +1,16 @@
import StringIO
import logging import logging
import time import time
import paramiko import paramiko
import StringIO
import infection_monkey.monkeyfs as monkeyfs import infection_monkey.monkeyfs as monkeyfs
from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth
from infection_monkey.model import MONKEY_ARG from infection_monkey.model import MONKEY_ARG
from infection_monkey.network.tools import check_tcp_port from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
from common.utils.exploit_enum import ExploitType
__author__ = 'hoffer' __author__ = 'hoffer'
@ -71,26 +71,26 @@ class SSHExploiter(HostExploiter):
exploited = False exploited = False
for user, curpass in user_password_pairs: for user, current_password in user_password_pairs:
try: try:
ssh.connect(self.host.ip_addr, ssh.connect(self.host.ip_addr,
username=user, username=user,
password=curpass, password=current_password,
port=port, port=port,
timeout=None) timeout=None)
LOG.debug("Successfully logged in %r using SSH (%s : %s)", LOG.debug("Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)",
self.host, user, curpass) self.host, user, self._config.hash_sensitive_data(current_password))
exploited = True exploited = True
self.add_vuln_port(port) self.add_vuln_port(port)
self.report_login_attempt(True, user, curpass) self.report_login_attempt(True, user, current_password)
break break
except Exception as exc: except Exception as exc:
LOG.debug("Error logging into victim %r with user" LOG.debug("Error logging into victim %r with user"
" %s and password '%s': (%s)", self.host, " %s and password (SHA-512) '%s': (%s)", self.host,
user, curpass, exc) user, self._config.hash_sensitive_data(current_password), exc)
self.report_login_attempt(False, user, curpass) self.report_login_attempt(False, user, current_password)
continue continue
return exploited return exploited

View File

@ -33,8 +33,10 @@ class WmiExploiter(HostExploiter):
creds = self._config.get_exploit_user_password_or_hash_product() creds = self._config.get_exploit_user_password_or_hash_product()
for user, password, lm_hash, ntlm_hash in creds: 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')", password_hashed = self._config.hash_sensitive_data(password)
self.host, user, password, lm_hash, ntlm_hash) 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() wmi_connection = WmiTools.WmiConnection()
@ -44,23 +46,23 @@ class WmiExploiter(HostExploiter):
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
LOG.debug("Failed connecting to %r using WMI with " LOG.debug("Failed connecting to %r using WMI with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", "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 continue
except DCERPCException: except DCERPCException:
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
LOG.debug("Failed connecting to %r using WMI with " LOG.debug("Failed connecting to %r using WMI with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", "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 continue
except socket.error: except socket.error:
LOG.debug("Network error in WMI connection to %r with " LOG.debug("Network error in WMI connection to %r with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')", "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 return False
except Exception as exc: except Exception as exc:
LOG.debug("Unknown WMI connection error to %r with " LOG.debug("Unknown WMI connection error to %r with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s') (%s):\n%s", "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 return False
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
@ -91,7 +93,8 @@ class WmiExploiter(HostExploiter):
# execute the remote dropper in case the path isn't final # execute the remote dropper in case the path isn't final
elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ 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: else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1) build_monkey_commandline(self.host, get_monkey_depth() - 1)
@ -118,3 +121,4 @@ class WmiExploiter(HostExploiter):
return success return success
return False return False

View File

@ -68,7 +68,7 @@ def main():
else: else:
print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,)) 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 # Make sure we're not in a machine that has the kill file
kill_path = os.path.expandvars( kill_path = os.path.expandvars(

View File

@ -255,6 +255,7 @@ class InfectionMonkey(object):
if WormConfiguration.self_delete_in_cleanup \ if WormConfiguration.self_delete_in_cleanup \
and -1 == sys.executable.find('python'): and -1 == sys.executable.find('python'):
try: try:
status = None
if "win32" == sys.platform: if "win32" == sys.platform:
from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE
startupinfo = subprocess.STARTUPINFO() startupinfo = subprocess.STARTUPINFO()
@ -265,10 +266,12 @@ class InfectionMonkey(object):
close_fds=True, startupinfo=startupinfo) close_fds=True, startupinfo=startupinfo)
else: else:
os.remove(sys.executable) os.remove(sys.executable)
T1107Telem(ScanStatus.USED, sys.executable).send() status = ScanStatus.USED
except Exception as exc: except Exception as exc:
LOG.error("Exception in self delete: %s", 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): def send_log(self):
monkey_log_path = utils.get_monkey_log_path() monkey_log_path = utils.get_monkey_log_path()

View File

@ -56,7 +56,7 @@ class MimikatzCollector(object):
LOG.exception("Error initializing mimikatz collector") LOG.exception("Error initializing mimikatz collector")
status = ScanStatus.SCANNED status = ScanStatus.SCANNED
T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI.name).send() T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI.name).send()
T1129Telem(status, UsageEnum.MIMIKATZ.name).send() T1129Telem(status, UsageEnum.MIMIKATZ).send()
def get_logon_info(self): def get_logon_info(self):

View File

@ -6,6 +6,6 @@ class T1035Telem(UsageTelem):
""" """
T1035 telemetry. T1035 telemetry.
:param status: ScanStatus of technique :param status: ScanStatus of technique
:param usage: Enum name of UsageEnum :param usage: Enum of UsageEnum type
""" """
super(T1035Telem, self).__init__('T1035', status, usage) super(T1035Telem, self).__init__('T1035', status, usage)

View File

@ -6,6 +6,6 @@ class T1129Telem(UsageTelem):
""" """
T1129 telemetry. T1129 telemetry.
:param status: ScanStatus of technique :param status: ScanStatus of technique
:param usage: Enum name of UsageEnum :param usage: Enum of UsageEnum type
""" """
super(T1129Telem, self).__init__("T1129", status, usage) super(T1129Telem, self).__init__("T1129", status, usage)

View File

@ -7,10 +7,10 @@ class UsageTelem(AttackTelem):
""" """
:param technique: Id of technique :param technique: Id of technique
:param status: ScanStatus 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) super(UsageTelem, self).__init__(technique, status)
self.usage = usage self.usage = usage.name
def get_data(self): def get_data(self):
data = super(UsageTelem, self).get_data() data = super(UsageTelem, self).get_data()

View File

@ -3,3 +3,4 @@ import os
__author__ = 'itay.mizeretz' __author__ = 'itay.mizeretz'
MONKEY_ISLAND_ABS_PATH = os.path.join(os.getcwd(), 'monkey_island') MONKEY_ISLAND_ABS_PATH = os.path.join(os.getcwd(), 'monkey_island')
DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5

View File

@ -1,11 +1,11 @@
""" """
Define a Document Schema for the Monkey document. Define a Document Schema for the Monkey document.
""" """
import mongoengine
from mongoengine import Document, StringField, ListField, BooleanField, EmbeddedDocumentField, ReferenceField, \ 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
class Monkey(Document): class Monkey(Document):
@ -26,24 +26,37 @@ class Monkey(Document):
ip_addresses = ListField(StringField()) ip_addresses = ListField(StringField())
keepalive = DateTimeField() keepalive = DateTimeField()
modifytime = DateTimeField() modifytime = DateTimeField()
# TODO change this to an embedded document as well - RN it's an unnamed tuple which is confusing. # TODO make "parent" an embedded document, so this can be removed and the schema explained (and validated) verbosly.
parent = ListField(ListField(StringField())) # 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() config_error = BooleanField()
critical_services = ListField(StringField()) critical_services = ListField(StringField())
pba_results = ListField() pba_results = ListField()
ttl_ref = ReferenceField(MonkeyTtl) ttl_ref = ReferenceField(MonkeyTtl)
tunnel = ReferenceField("self")
# LOGIC # LOGIC
@staticmethod @staticmethod
def get_single_monkey_by_id(db_id): def get_single_monkey_by_id(db_id):
try: try:
return Monkey.objects(id=db_id)[0] return Monkey.objects.get(id=db_id)
except IndexError: except DoesNotExist as ex:
raise MonkeyNotFoundError("id: {0}".format(str(db_id))) 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 @staticmethod
def get_latest_modifytime(): def get_latest_modifytime():
if Monkey.objects.count() > 0:
return Monkey.objects.order_by('-modifytime').first().modifytime return Monkey.objects.order_by('-modifytime').first().modifytime
return None
def is_dead(self): def is_dead(self):
monkey_is_dead = False monkey_is_dead = False
@ -54,7 +67,7 @@ class Monkey(Document):
if MonkeyTtl.objects(id=self.ttl_ref.id).count() == 0: if MonkeyTtl.objects(id=self.ttl_ref.id).count() == 0:
# No TTLs - monkey has timed out. The monkey is MIA. # No TTLs - monkey has timed out. The monkey is MIA.
monkey_is_dead = True monkey_is_dead = True
except (mongoengine.DoesNotExist, AttributeError): except (DoesNotExist, AttributeError):
# Trying to dereference unknown document - the monkey is MIA. # Trying to dereference unknown document - the monkey is MIA.
monkey_is_dead = True monkey_is_dead = True
return monkey_is_dead return monkey_is_dead
@ -67,6 +80,10 @@ class Monkey(Document):
os = "windows" os = "windows"
return os return os
def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS):
self.ttl_ref = create_monkey_ttl_document(duration)
self.save()
class MonkeyNotFoundError(Exception): class MonkeyNotFoundError(Exception):
pass pass

View File

@ -38,3 +38,16 @@ class MonkeyTtl(Document):
} }
expire_at = DateTimeField() 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

View File

@ -46,6 +46,19 @@ class TestMonkey(IslandTestCase):
self.assertTrue(mia_monkey.is_dead()) self.assertTrue(mia_monkey.is_dead())
self.assertFalse(alive_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): def test_get_single_monkey_by_id(self):
self.fail_if_not_testing_env() self.fail_if_not_testing_env()
self.clean_monkey_db() self.clean_monkey_db()

View File

@ -5,26 +5,17 @@ import dateutil.parser
import flask_restful import flask_restful
from flask import request 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.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.config import ConfigService
from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.node import NodeService
MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5
__author__ = 'Barak' __author__ = 'Barak'
# TODO: separate logic from interface # 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): class Monkey(flask_restful.Resource):
# Used by monkey. can't secure. # Used by monkey. can't secure.
@ -58,8 +49,8 @@ class Monkey(flask_restful.Resource):
tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "")
NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip) NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip)
ttlid = create_monkey_ttl() ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS)
update['$set']['ttl_ref'] = ttlid update['$set']['ttl_ref'] = ttl.id
return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) 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("//", "") tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "")
monkey_json.pop('tunnel') 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"]}, mongo.db.monkey.update({"guid": monkey_json["guid"]},
{"$set": monkey_json}, {"$set": monkey_json},

View File

@ -15,10 +15,10 @@ from monkey_island.cc.services.edge import EdgeService
from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.node import NodeService
from monkey_island.cc.encryptor import encryptor from monkey_island.cc.encryptor import encryptor
from monkey_island.cc.services.wmi_handler import WMIHandler from monkey_island.cc.services.wmi_handler import WMIHandler
from monkey_island.cc.models.monkey import Monkey
__author__ = 'Barak' __author__ = 'Barak'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -49,6 +49,9 @@ class Telemetry(flask_restful.Resource):
telemetry_json = json.loads(request.data) telemetry_json = json.loads(request.data)
telemetry_json['timestamp'] = datetime.now() telemetry_json['timestamp'] = datetime.now()
# 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']) monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
try: try:
@ -59,7 +62,7 @@ class Telemetry(flask_restful.Resource):
else: else:
logger.info('Got unknown type of telemetry: %s' % telem_category) logger.info('Got unknown type of telemetry: %s' % telem_category)
except Exception as ex: 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) telem_id = mongo.db.telemetry.insert(telemetry_json)
return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) return mongo.db.telemetry.find_one_or_404({"_id": telem_id})

View File

@ -1,3 +1,4 @@
import logging
from datetime import datetime from datetime import datetime
import dateutil 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.database import mongo
from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.node import NodeService
logger = logging.getLogger(__name__)
__author__ = 'itay.mizeretz' __author__ = 'itay.mizeretz'
@ -23,11 +26,15 @@ class TelemetryFeed(flask_restful.Resource):
telemetries = telemetries.sort([('timestamp', flask_pymongo.ASCENDING)]) telemetries = telemetries.sort([('timestamp', flask_pymongo.ASCENDING)])
try:
return \ return \
{ {
'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries], 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries],
'timestamp': datetime.now().isoformat() '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 @staticmethod
def get_displayed_telemetry(telem): def get_displayed_telemetry(telem):

View File

@ -124,8 +124,8 @@ class UsageTechnique(AttackTechnique):
def parse_usages(usage): def parse_usages(usage):
""" """
Parses data from database and translates usage enums into strings Parses data from database and translates usage enums into strings
:param usage: :param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1}
:return: :return: usage string
""" """
try: try:
usage['usage'] = UsageEnum[usage['usage']].value[usage['status']] usage['usage'] = UsageEnum[usage['usage']].value[usage['status']]

View File

@ -389,7 +389,7 @@ SCHEMA = {
"self_delete_in_cleanup": { "self_delete_in_cleanup": {
"title": "Self delete on cleanup", "title": "Self delete on cleanup",
"type": "boolean", "type": "boolean",
"default": False, "default": True,
"description": "Should the monkey delete its executable when going down" "description": "Should the monkey delete its executable when going down"
}, },
"use_file_logging": { "use_file_logging": {

View File

@ -373,8 +373,13 @@ class ReportService:
@staticmethod @staticmethod
def get_exploits(): def get_exploits():
query = [{'$match': {'telem_category': 'exploit', 'data.result': True}},
{'$group': {'_id': {'ip_address': '$data.machine.ip_addr'},
'data': {'$first': '$$ROOT'},
}},
{"$replaceRoot": {"newRoot": "$data"}}]
exploits = [] exploits = []
for exploit in mongo.db.telemetry.find({'telem_category': 'exploit', 'data.result': True}): for exploit in mongo.db.telemetry.aggregate(query):
new_exploit = ReportService.process_exploit(exploit) new_exploit = ReportService.process_exploit(exploit)
if new_exploit not in exploits: if new_exploit not in exploits:
exploits.append(new_exploit) exploits.append(new_exploit)

View File

@ -36,7 +36,7 @@ export function getUsageColumns() {
style: { 'whiteSpace': 'unset' }}] style: { 'whiteSpace': 'unset' }}]
}])} }])}
export const scanStatus = { export const ScanStatus = {
UNSCANNED: 0, UNSCANNED: 0,
SCANNED: 1, SCANNED: 1,
USED: 2 USED: 2

View File

@ -2,7 +2,7 @@ import React from 'react';
import '../../../styles/Collapse.scss' import '../../../styles/Collapse.scss'
import '../../report-components/StolenPasswords' import '../../report-components/StolenPasswords'
import StolenPasswordsComponent from "../../report-components/StolenPasswords"; import StolenPasswordsComponent from "../../report-components/StolenPasswords";
import {scanStatus} from "./Helpers" import {ScanStatus} from "./Helpers"
class T1003 extends React.Component { class T1003 extends React.Component {
@ -16,7 +16,7 @@ class T1003 extends React.Component {
<div> <div>
<div>{this.props.data.message}</div> <div>{this.props.data.message}</div>
<br/> <br/>
{this.props.data.status === scanStatus.USED ? {this.props.data.status === ScanStatus.USED ?
<StolenPasswordsComponent data={this.props.reportData.glance.stolen_creds.concat(this.props.reportData.glance.ssh_keys)}/> <StolenPasswordsComponent data={this.props.reportData.glance.stolen_creds.concat(this.props.reportData.glance.ssh_keys)}/>
: ""} : ""}
</div> </div>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import '../../../styles/Collapse.scss' import '../../../styles/Collapse.scss'
import ReactTable from "react-table"; import ReactTable from "react-table";
import { renderMachine, scanStatus } from "./Helpers" import { renderMachine, ScanStatus } from "./Helpers"
class T1059 extends React.Component { class T1059 extends React.Component {
@ -25,7 +25,7 @@ class T1059 extends React.Component {
<div> <div>
<div>{this.props.data.message}</div> <div>{this.props.data.message}</div>
<br/> <br/>
{this.props.data.status === scanStatus.USED ? {this.props.data.status === ScanStatus.USED ?
<ReactTable <ReactTable
columns={T1059.getCommandColumns()} columns={T1059.getCommandColumns()}
data={this.props.data.cmds} data={this.props.data.cmds}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import '../../../styles/Collapse.scss' import '../../../styles/Collapse.scss'
import ReactTable from "react-table"; import ReactTable from "react-table";
import { renderMachine, scanStatus } from "./Helpers" import { renderMachine, ScanStatus } from "./Helpers"
class T1075 extends React.Component { class T1075 extends React.Component {
@ -34,7 +34,7 @@ class T1075 extends React.Component {
<div> <div>
<div>{this.props.data.message}</div> <div>{this.props.data.message}</div>
<br/> <br/>
{this.props.data.status !== scanStatus.UNSCANNED ? {this.props.data.status === ScanStatus.USED ?
<ReactTable <ReactTable
columns={T1075.getHashColumns()} columns={T1075.getHashColumns()}
data={this.props.data.successful_logins} data={this.props.data.successful_logins}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import '../../../styles/Collapse.scss' import '../../../styles/Collapse.scss'
import ReactTable from "react-table"; import ReactTable from "react-table";
import { renderMachineFromSystemData, scanStatus } from "./Helpers" import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
class T1082 extends React.Component { class T1082 extends React.Component {
@ -33,7 +33,7 @@ class T1082 extends React.Component {
<div> <div>
<div>{this.props.data.message}</div> <div>{this.props.data.message}</div>
<br/> <br/>
{this.props.data.status === scanStatus.USED ? {this.props.data.status === ScanStatus.USED ?
<ReactTable <ReactTable
columns={T1082.getSystemInfoColumns()} columns={T1082.getSystemInfoColumns()}
data={this.props.data.system_info} data={this.props.data.system_info}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import '../../../styles/Collapse.scss' import '../../../styles/Collapse.scss'
import ReactTable from "react-table"; import ReactTable from "react-table";
import { renderMachine, scanStatus } from "./Helpers" import { renderMachine, ScanStatus } from "./Helpers"
class T1086 extends React.Component { class T1086 extends React.Component {
@ -25,7 +25,7 @@ class T1086 extends React.Component {
<div> <div>
<div>{this.props.data.message}</div> <div>{this.props.data.message}</div>
<br/> <br/>
{this.props.data.status === scanStatus.USED ? {this.props.data.status === ScanStatus.USED ?
<ReactTable <ReactTable
columns={T1086.getPowershellColumns()} columns={T1086.getPowershellColumns()}
data={this.props.data.cmds} data={this.props.data.cmds}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import '../../../styles/Collapse.scss' import '../../../styles/Collapse.scss'
import ReactTable from "react-table"; import ReactTable from "react-table";
import { renderMachineFromSystemData, scanStatus } from "./Helpers" import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
class T1107 extends React.Component { class T1107 extends React.Component {
@ -11,7 +11,7 @@ class T1107 extends React.Component {
} }
static renderDelete(status){ static renderDelete(status){
if(status === scanStatus.USED){ if(status === ScanStatus.USED){
return <span>Yes</span> return <span>Yes</span>
} else { } else {
return <span>No</span> return <span>No</span>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import '../../../styles/Collapse.scss' import '../../../styles/Collapse.scss'
import ReactTable from "react-table"; import ReactTable from "react-table";
import { renderMachine, scanStatus } from "./Helpers" import { renderMachine, ScanStatus } from "./Helpers"
class T1110 extends React.Component { class T1110 extends React.Component {
@ -32,7 +32,7 @@ class T1110 extends React.Component {
<div> <div>
<div>{this.props.data.message}</div> <div>{this.props.data.message}</div>
<br/> <br/>
{this.props.data.status !== scanStatus.UNSCANNED ? {this.props.data.status !== ScanStatus.UNSCANNED ?
<ReactTable <ReactTable
columns={T1110.getServiceColumns()} columns={T1110.getServiceColumns()}
data={this.props.data.services} data={this.props.data.services}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import '../../../styles/Collapse.scss' import '../../../styles/Collapse.scss'
import ReactTable from "react-table"; import ReactTable from "react-table";
import { renderMachineFromSystemData, scanStatus } from "./Helpers" import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
class T1145 extends React.Component { class T1145 extends React.Component {
@ -38,7 +38,7 @@ class T1145 extends React.Component {
<div> <div>
<div>{this.props.data.message}</div> <div>{this.props.data.message}</div>
<br/> <br/>
{this.props.data.status === scanStatus.USED ? {this.props.data.status === ScanStatus.USED ?
<ReactTable <ReactTable
columns={T1145.getKeysInfoColumns()} columns={T1145.getKeysInfoColumns()}
data={this.props.data.ssh_info} data={this.props.data.ssh_info}

View File

@ -241,7 +241,7 @@ class RunMonkeyPageComponent extends AuthComponent {
<div style={{'marginTop': '1em', 'marginBottom': '1em'}}> <div style={{'marginTop': '1em', 'marginBottom': '1em'}}>
<p className="alert alert-info"> <p className="alert alert-info">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/> <i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
Not sure what this is? Not seeing your AWS EC2 instances? <a href="https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS-EC2-instances">Read the documentation</a>! Not sure what this is? Not seeing your AWS EC2 instances? <a href="https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS-EC2-instances" target="_blank">Read the documentation</a>!
</p> </p>
</div> </div>
{ {

View File

@ -4,7 +4,7 @@ import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {edgeGroupToColor, options} from 'components/map/MapOptions'; import {edgeGroupToColor, options} from 'components/map/MapOptions';
import '../../styles/Collapse.scss'; import '../../styles/Collapse.scss';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import {scanStatus} from "../attack/techniques/Helpers"; import {ScanStatus} from "../attack/techniques/Helpers";
import Collapse from '@kunukn/react-collapse'; import Collapse from '@kunukn/react-collapse';
import T1210 from '../attack/techniques/T1210'; import T1210 from '../attack/techniques/T1210';
import T1197 from '../attack/techniques/T1197'; import T1197 from '../attack/techniques/T1197';
@ -86,9 +86,9 @@ class AttackReportPageComponent extends AuthComponent {
getComponentClass(tech_id){ getComponentClass(tech_id){
switch (this.state.report[tech_id].status) { switch (this.state.report[tech_id].status) {
case scanStatus.SCANNED: case ScanStatus.SCANNED:
return 'collapse-info'; return 'collapse-info';
case scanStatus.USED: case ScanStatus.USED:
return 'collapse-danger'; return 'collapse-danger';
default: default:
return 'collapse-default'; return 'collapse-default';

View File

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