Merge branch 'attack_file_copy' into attack_bugfixes

# Conflicts:
#	monkey/infection_monkey/exploit/sshexec.py
This commit is contained in:
VakarisZ 2019-08-02 09:54:44 +03:00
commit 281440962c
46 changed files with 294 additions and 147 deletions

View File

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

View File

@ -13,9 +13,10 @@ Don't forget to add python to PATH or do so while installing it via this script.
## Linux
You must have root permissions, but there is no need to run the script as root.<br>
You must have root permissions, but don't run the script as root.<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>
./deploy_linux.sh (deploys under ./infection_monkey)<br>
./deploy_linux.sh "/home/test/monkey" (deploys under /home/test/monkey)<br>

View File

@ -10,6 +10,19 @@ class ScanStatus(Enum):
USED = 2
class UsageEnum(Enum):
SMB = {ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.",
ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service via MS-SCMR."}
MIMIKATZ = {ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.",
ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed."}
MIMIKATZ_FILE_COPY = {ScanStatus.USED.value: "WinAPI was called to load mimikatz.",
ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz, but failed."}
SINGLETON_FILE_COPY = {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_FILE_COPY = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
# Dict that describes what BITS job was used for
BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system."

View File

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

View File

@ -168,7 +168,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",

View File

@ -15,7 +15,7 @@ from infection_monkey.exploit.tools.helpers import build_monkey_commandline_expl
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
from infection_monkey.system_info import SystemInfoCollector, OperatingSystem
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
from common.utils.attack_utils import ScanStatus
from common.utils.attack_utils import ScanStatus, UsageEnum
if "win32" == sys.platform:
from win32process import DETACHED_PROCESS
@ -158,7 +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, "WinAPI was used to mark monkey files"
" for deletion on next boot.").send()
T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_FILE_COPY).send()
except AttributeError:
LOG.error("Invalid configuration options. Failing")

View File

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

View File

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

View File

@ -11,7 +11,7 @@ from infection_monkey.network import SMBFinger
from infection_monkey.network.tools import check_tcp_port
from common.utils.exploit_enum import ExploitType
from infection_monkey.telemetry.attack.t1035_telem import T1035Telem
from common.utils.attack_utils import ScanStatus
from common.utils.attack_utils import ScanStatus, UsageEnum
LOG = getLogger(__name__)
@ -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:
@ -133,11 +133,11 @@ class SmbExploiter(HostExploiter):
service = resp['lpServiceHandle']
try:
scmr.hRStartServiceW(scmr_rpc, service)
T1035Telem(ScanStatus.USED, "SMB exploiter ran the monkey by creating a service via MS-SCMR.").send()
status = ScanStatus.USED
except:
T1035Telem(ScanStatus.SCANNED,
"SMB exploiter failed to run the monkey by creating a service via MS-SCMR.").send()
status = ScanStatus.SCANNED
pass
T1035Telem(status, UsageEnum.SMB).send()
scmr.hRDeleteService(scmr_rpc, service)
scmr.hRCloseServiceHandle(scmr_rpc, service)

View File

@ -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
@ -73,26 +74,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
@ -111,7 +112,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)
@ -164,17 +165,17 @@ 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)
T1105Telem(ScanStatus.USED,
get_interface_to_target(self.host.ip_addr),
self.host.ip_addr,
src_path).send()
status = ScanStatus.USED
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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import socket
import zipfile
import infection_monkey.config
from common.utils.attack_utils import ScanStatus
from common.utils.attack_utils import ScanStatus, UsageEnum
from infection_monkey.telemetry.attack.t1129_telem import T1129Telem
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path
@ -47,16 +47,16 @@ class MimikatzCollector(object):
collect_proto = ctypes.WINFUNCTYPE(ctypes.c_int)
get_proto = ctypes.WINFUNCTYPE(MimikatzCollector.LogonData)
get_text_output_proto = ctypes.WINFUNCTYPE(ctypes.c_wchar_p)
T1106Telem(ScanStatus.USED, "WinAPI was called to load mimikatz.").send()
self._collect = collect_proto(("collect", self._dll))
self._get = get_proto(("get", self._dll))
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
self._isInit = True
T1129Telem(ScanStatus.USED, "Windows module loader was used to load Mimikatz DLL.").send()
status = ScanStatus.USED
except Exception:
LOG.exception("Error initializing mimikatz collector")
T1129Telem(ScanStatus.SCANNED, "Monkey tried to load Mimikatz DLL, but failed.").send()
T1106Telem(ScanStatus.SCANNED, "Monkey tried to call WinAPI to load mimikatz.").send()
status = ScanStatus.SCANNED
T1129Telem(status, UsageEnum.MIMIKATZ).send()
T1106Telem(status, UsageEnum.MIMIKATZ_FILE_COPY).send()
def get_logon_info(self):
"""

View File

@ -5,7 +5,7 @@ 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
from common.utils.attack_utils import ScanStatus, UsageEnum
__author__ = 'itamar'
@ -45,22 +45,25 @@ 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)
T1106Telem(ScanStatus.SCANNED, "WinAPI call to acquire system singleton "
"for monkey process wasn't successful.").send()
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_FILE_COPY).send()
if status == ScanStatus.SCANNED:
return False
self._mutex_handle = handle
T1106Telem(ScanStatus.USED, "WinAPI was called to acquire system singleton for monkey's process.").send()
LOG.debug("Global singleton mutex %r acquired",
self._mutex_name)

View File

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

View File

@ -6,6 +6,6 @@ class T1106Telem(UsageTelem):
"""
T1129 telemetry.
:param status: ScanStatus of technique
:param usage: Usage string
:param usage: UsageEnum type value
"""
super(T1106Telem, self).__init__("T1106", status, usage)

View File

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

View File

@ -5,12 +5,12 @@ class UsageTelem(AttackTelem):
def __init__(self, technique, status, usage):
"""
T1035 telemetry.
:param technique: Id of technique
:param status: ScanStatus of technique
:param usage: Usage string
: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()

View File

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

View File

@ -1,11 +1,11 @@
"""
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
class Monkey(Document):
@ -26,24 +26,37 @@ 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")
# 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
@ -54,7 +67,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
@ -67,6 +80,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()
class MonkeyNotFoundError(Exception):
pass

View File

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

View File

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

View File

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

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.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__)
@ -49,6 +49,9 @@ class Telemetry(flask_restful.Resource):
telemetry_json = json.loads(request.data)
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'])
try:
@ -59,7 +62,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})
@ -188,7 +191,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)

View File

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

View File

@ -1,10 +1,10 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
from monkey_island.cc.services.attack.technique_reports import UsageTechnique
__author__ = "VakarisZ"
class T1035(AttackTechnique):
class T1035(UsageTechnique):
tech_id = "T1035"
unscanned_msg = "Monkey didn't try to interact with Windows services."
scanned_msg = "Monkey tried to interact with Windows services, but failed."
@ -13,5 +13,7 @@ class T1035(AttackTechnique):
@staticmethod
def get_report_data():
data = T1035.get_tech_base_data()
data.update({'services': list(mongo.db.telemetry.aggregate(T1035.get_usage_query()))})
services = list(mongo.db.telemetry.aggregate(T1035.get_usage_query()))
services = list(map(T1035.parse_usages, services))
data.update({'services': services})
return data

View File

@ -1,10 +1,10 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
from monkey_island.cc.services.attack.technique_reports import UsageTechnique
__author__ = "VakarisZ"
class T1129(AttackTechnique):
class T1129(UsageTechnique):
tech_id = "T1129"
unscanned_msg = "Monkey didn't try to load any DLL's."
scanned_msg = "Monkey tried to load DLL's, but failed."
@ -13,5 +13,7 @@ class T1129(AttackTechnique):
@staticmethod
def get_report_data():
data = T1129.get_tech_base_data()
data.update({'dlls': list(mongo.db.telemetry.aggregate(T1129.get_usage_query()))})
dlls = list(mongo.db.telemetry.aggregate(T1129.get_usage_query()))
dlls = list(map(T1129.parse_usages, dlls))
data.update({'dlls': dlls})
return data

View File

@ -1,10 +1,13 @@
import abc
import logging
from monkey_island.cc.database import mongo
from common.utils.attack_utils import ScanStatus
from common.utils.attack_utils import ScanStatus, UsageEnum
from monkey_island.cc.services.attack.attack_config import AttackConfig
from common.utils.code_utils import abstractstatic
logger = logging.getLogger(__name__)
class AttackTechnique(object):
""" Abstract class for ATT&CK report components """
@ -113,6 +116,24 @@ class AttackTechnique(object):
data.update({'title': cls.technique_title()})
return data
class UsageTechnique(AttackTechnique):
__metaclass__ = abc.ABCMeta
@staticmethod
def parse_usages(usage):
"""
Parses data from database and translates usage enums into strings
:param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1}
:return: usage string
"""
try:
usage['usage'] = UsageEnum[usage['usage']].value[usage['status']]
except KeyError:
logger.error("Error translating usage enum. into string. "
"Check if usage enum field exists and covers all telem. statuses.")
return usage
@classmethod
def get_usage_query(cls):
"""
@ -131,4 +152,5 @@ class AttackTechnique(object):
{'$addFields': {'_id': 0,
'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
'monkey': 0}},
{'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}}]
{'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}},
{"$replaceRoot": {"newRoot": "$_id"}}]

View File

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

View File

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

View File

@ -27,16 +27,16 @@ export function getUsageColumns() {
columns: [
{Header: 'Machine',
id: 'machine',
accessor: x => renderMachineFromSystemData(x._id.machine),
accessor: x => renderMachineFromSystemData(x.machine),
style: { 'whiteSpace': 'unset' },
width: 300},
{Header: 'Usage',
id: 'usage',
accessor: x => x._id.usage,
accessor: x => x.usage,
style: { 'whiteSpace': 'unset' }}]
}])}
export const scanStatus = {
export const ScanStatus = {
UNSCANNED: 0,
SCANNED: 1,
USED: 2

View File

@ -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 {
<div>
<div>{this.props.data.message}</div>
<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)}/>
: ""}
</div>

View File

@ -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 {
<div>
<div>{this.props.data.message}</div>
<br/>
{this.props.data.status === scanStatus.USED ?
{this.props.data.status === ScanStatus.USED ?
<ReactTable
columns={T1059.getCommandColumns()}
data={this.props.data.cmds}

View File

@ -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 T1075 extends React.Component {
@ -34,7 +34,7 @@ class T1075 extends React.Component {
<div>
<div>{this.props.data.message}</div>
<br/>
{this.props.data.status !== scanStatus.UNSCANNED ?
{this.props.data.status === ScanStatus.USED ?
<ReactTable
columns={T1075.getHashColumns()}
data={this.props.data.successful_logins}

View File

@ -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 T1082 extends React.Component {
@ -33,7 +33,7 @@ class T1082 extends React.Component {
<div>
<div>{this.props.data.message}</div>
<br/>
{this.props.data.status === scanStatus.USED ?
{this.props.data.status === ScanStatus.USED ?
<ReactTable
columns={T1082.getSystemInfoColumns()}
data={this.props.data.system_info}

View File

@ -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 T1086 extends React.Component {
@ -25,7 +25,7 @@ class T1086 extends React.Component {
<div>
<div>{this.props.data.message}</div>
<br/>
{this.props.data.status === scanStatus.USED ?
{this.props.data.status === ScanStatus.USED ?
<ReactTable
columns={T1086.getPowershellColumns()}
data={this.props.data.cmds}

View File

@ -12,7 +12,7 @@ class T1105 extends React.Component {
static getFilesColumns() {
return ([{
Header: 'Files copied.',
Header: 'Files copied',
columns: [
{Header: 'Src. Machine', id: 'srcMachine', accessor: x => x.src, style: { 'whiteSpace': 'unset'}, width: 170 },
{Header: 'Dst. Machine', id: 'dstMachine', accessor: x => x.dst, style: { 'whiteSpace': 'unset'}, width: 170},

View File

@ -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 T1107 extends React.Component {
@ -11,7 +11,7 @@ class T1107 extends React.Component {
}
static renderDelete(status){
if(status === scanStatus.USED){
if(status === ScanStatus.USED){
return <span>Yes</span>
} else {
return <span>No</span>

View File

@ -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 {
<div>
<div>{this.props.data.message}</div>
<br/>
{this.props.data.status !== scanStatus.UNSCANNED ?
{this.props.data.status !== ScanStatus.UNSCANNED ?
<ReactTable
columns={T1110.getServiceColumns()}
data={this.props.data.services}

View File

@ -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 T1145 extends React.Component {
@ -38,7 +38,7 @@ class T1145 extends React.Component {
<div>
<div>{this.props.data.message}</div>
<br/>
{this.props.data.status === scanStatus.USED ?
{this.props.data.status === ScanStatus.USED ?
<ReactTable
columns={T1145.getKeysInfoColumns()}
data={this.props.data.ssh_info}

View File

@ -241,7 +241,7 @@ class RunMonkeyPageComponent extends AuthComponent {
<div style={{'marginTop': '1em', 'marginBottom': '1em'}}>
<p className="alert alert-info">
<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>
</div>
{

View File

@ -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';
@ -88,9 +88,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';

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