Merge branch 'attack_comand_line_interface' into attack_powershell

This commit is contained in:
VakarisZ 2019-06-19 15:13:55 +03:00
commit 7c41d1e2c9
56 changed files with 526 additions and 277 deletions

View File

@ -9,8 +9,9 @@ class ScanStatus(Enum):
# Technique was attempted and succeeded # Technique was attempted and succeeded
USED = 2 USED = 2
# Dict that describes what BITS job was used for # Dict that describes what BITS job was used for
BITS_UPLOAD_STRING = {"usage": "BITS job was used to upload monkey to a remote system."} BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system."
def format_time(time): def format_time(time):

View File

@ -157,9 +157,8 @@ class Configuration(object):
keep_tunnel_open_time = 60 keep_tunnel_open_time = 60
# Monkey files directories # Monkey files directory name
monkey_dir_linux = '/tmp/monkey_dir' monkey_dir_name = 'monkey_dir'
monkey_dir_windows = r'C:\Windows\Temp\monkey_dir'
########################### ###########################
# scanners config # scanners config

View File

@ -123,11 +123,11 @@ class ControlClient(object):
return {} return {}
@staticmethod @staticmethod
def send_telemetry(telem_type, data): def send_telemetry(telem_catagory, data):
if not WormConfiguration.current_server: if not WormConfiguration.current_server:
return return
try: try:
telemetry = {'monkey_guid': GUID, 'telem_type': telem_type, 'data': data} telemetry = {'monkey_guid': GUID, 'telem_catagory': telem_catagory, 'data': data}
reply = requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,), reply = requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,),
data=json.dumps(telemetry), data=json.dumps(telemetry),
headers={'content-type': 'application/json'}, headers={'content-type': 'application/json'},

View File

@ -29,9 +29,7 @@
"dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
"dropper_target_path_linux": "/tmp/monkey", "dropper_target_path_linux": "/tmp/monkey",
monkey_dir_linux = '/tmp/monkey_dir', "monkey_dir_name": "monkey_dir",
monkey_dir_windows = r'C:\Windows\Temp\monkey_dir',
"kill_file_path_linux": "/var/run/monkey.not", "kill_file_path_linux": "/var/run/monkey.not",
"kill_file_path_windows": "%windir%\\monkey.not", "kill_file_path_windows": "%windir%\\monkey.not",

View File

@ -11,7 +11,7 @@ from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\ from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
DOWNLOAD_TIMEOUT DOWNLOAD_TIMEOUT
from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
import re import re
@ -64,7 +64,7 @@ class ElasticGroovyExploiter(WebRCE):
def upload_monkey(self, url, commands=None): def upload_monkey(self, url, commands=None):
result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands) result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands)
if 'windows' in self.host.os['type'] and result: if 'windows' in self.host.os['type'] and result:
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send() T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
return result return result
def get_results(self, response): def get_results(self, response):

View File

@ -1,15 +1,16 @@
import os
import logging import logging
from time import sleep import os
import pymssql
import textwrap import textwrap
from time import sleep
import pymssql
from infection_monkey.exploit import HostExploiter, tools
from common.utils.exploit_enum import ExploitType from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter, tools
from infection_monkey.exploit.tools import HTTPTools from infection_monkey.exploit.tools import HTTPTools
from infection_monkey.config import WormConfiguration
from infection_monkey.model import DROPPER_ARG
from infection_monkey.exploit.tools import get_monkey_dest_path from infection_monkey.exploit.tools import get_monkey_dest_path
from infection_monkey.model import DROPPER_ARG
from infection_monkey.utils import get_monkey_dir_path
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -52,10 +53,10 @@ class MSSQLExploiter(HostExploiter):
LOG.info("Started http server on %s", http_path) LOG.info("Started http server on %s", http_path)
dst_path = get_monkey_dest_path(http_path) dst_path = get_monkey_dest_path(http_path)
tmp_file_path = os.path.join(WormConfiguration.monkey_dir_windows, MSSQLExploiter.TMP_FILE_NAME) tmp_file_path = os.path.join(get_monkey_dir_path(), MSSQLExploiter.TMP_FILE_NAME)
# Create monkey dir. # Create monkey dir.
commands = ["xp_cmdshell \"mkdir %s\"" % WormConfiguration.monkey_dir_windows] commands = ["xp_cmdshell \"mkdir %s\"" % get_monkey_dir_path()]
MSSQLExploiter.execute_command(cursor, commands) MSSQLExploiter.execute_command(cursor, commands)
# Form download command in a file # Form download command in a file

View File

@ -15,9 +15,9 @@ from infection_monkey.exploit.tools import get_target_monkey
from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS from infection_monkey.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.exploit.tools import build_monkey_commandline
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.exploit_enum import ExploitType
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
__author__ = 'hoffer' __author__ = 'hoffer'
@ -316,7 +316,7 @@ class RdpExploiter(HostExploiter):
if client_factory.success: if client_factory.success:
if not self._config.rdp_use_vbs_download: if not self._config.rdp_use_vbs_download:
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send() T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
self.add_vuln_port(RDP_PORT) self.add_vuln_port(RDP_PORT)
exploited = True exploited = True
self.report_login_attempt(True, user, password) self.report_login_attempt(True, user, password)

View File

@ -7,7 +7,7 @@ from infection_monkey.exploit import HostExploiter
from infection_monkey.model import * from infection_monkey.model import *
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools
from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
__author__ = 'VakarisZ' __author__ = 'VakarisZ'
@ -210,6 +210,7 @@ class WebRCE(HostExploiter):
for url in urls: for url in urls:
if self.check_if_exploitable(url): if self.check_if_exploitable(url):
self.add_vuln_url(url) self.add_vuln_url(url)
self.vulnerable_urls.append(url)
if stop_checking: if stop_checking:
break break
if not self.vulnerable_urls: if not self.vulnerable_urls:
@ -307,7 +308,7 @@ class WebRCE(HostExploiter):
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
LOG.info("Powershell not found in host. Using bitsadmin to download.") LOG.info("Powershell not found in host. Using bitsadmin to download.")
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path} backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send() T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
resp = self.exploit(url, backup_command) resp = self.exploit(url, backup_command)
return resp return resp

View File

@ -15,10 +15,10 @@ from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.network_scanner import NetworkScanner
from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_info import SystemInfoCollector
from infection_monkey.system_singleton import SystemSingleton from infection_monkey.system_singleton import SystemSingleton
from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
from infection_monkey.windows_upgrader import WindowsUpgrader from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach from infection_monkey.post_breach.post_breach_handler import PostBreach
from common.utils.attack_utils import ScanStatus from common.utils.attack_utils import ScanStatus
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from infection_monkey.exploit.tools import get_interface_to_target from infection_monkey.exploit.tools import get_interface_to_target
__author__ = 'itamar' __author__ = 'itamar'
@ -186,9 +186,11 @@ class InfectionMonkey(object):
for exploiter in [exploiter(machine) for exploiter in self._exploiters]: for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
if self.try_exploiting(machine, exploiter): if self.try_exploiting(machine, exploiter):
host_exploited = True host_exploited = True
VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send()
break break
if not host_exploited: if not host_exploited:
self._fail_exploitation_machines.add(machine) self._fail_exploitation_machines.add(machine)
VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
if not self._keep_running: if not self._keep_running:
break break

View File

@ -0,0 +1,24 @@
from infection_monkey.telemetry.base_telem import BaseTelem
__author__ = "VakarisZ"
class AttackTelem(BaseTelem):
def __init__(self, technique, status):
"""
Default ATT&CK telemetry constructor
:param technique: Technique ID. E.g. T111
:param status: ScanStatus of technique
"""
super(AttackTelem, self).__init__()
self.technique = technique
self.status = status
telem_catagory = 'attack'
def get_data(self):
return {
'status': self.status.value,
'technique': self.technique
}

View File

@ -0,0 +1,22 @@
from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
__author__ = "itay.mizeretz"
class T1197Telem(VictimHostTelem):
def __init__(self, status, machine, usage):
"""
T1197 telemetry.
:param status: ScanStatus of technique
:param machine: VictimHost obj from model/host.py
:param usage: Usage string
"""
super(T1197Telem, self).__init__('T1197', status, machine)
self.usage = usage
def get_data(self):
data = super(T1197Telem, self).get_data()
data.update({
'usage': self.usage
})
return data

View File

@ -0,0 +1,29 @@
from unittest import TestCase
from common.utils.attack_utils import ScanStatus
from infection_monkey.model import VictimHost
from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
class TestVictimHostTelem(TestCase):
def test_get_data(self):
machine = VictimHost('127.0.0.1')
status = ScanStatus.USED
technique = 'T1210'
telem = VictimHostTelem(technique, status, machine)
self.assertEqual(telem.telem_catagory, 'attack')
expected_data = {
'machine': {
'domain_name': machine.domain_name,
'ip_addr': machine.ip_addr
},
'status': status.value,
'technique': technique
}
actual_data = telem.get_data()
self.assertEqual(actual_data, expected_data)

View File

@ -0,0 +1,24 @@
from infection_monkey.telemetry.attack.attack_telem import AttackTelem
__author__ = "VakarisZ"
class VictimHostTelem(AttackTelem):
def __init__(self, technique, status, machine):
"""
ATT&CK telemetry.
When `send` is called, it will parse and send the VictimHost's (remote machine's) data.
:param technique: Technique ID. E.g. T111
:param status: ScanStatus of technique
:param machine: VictimHost obj from model/host.py
"""
super(VictimHostTelem, self).__init__(technique, status)
self.machine = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr}
def get_data(self):
data = super(VictimHostTelem, self).get_data()
data.update({
'machine': self.machine
})
return data

View File

@ -0,0 +1,36 @@
import abc
from infection_monkey.control import ControlClient
__author__ = 'itay.mizeretz'
class BaseTelem(object):
"""
Abstract base class for telemetry.
"""
__metaclass__ = abc.ABCMeta
def __init__(self):
pass
def send(self):
"""
Sends telemetry to island
"""
ControlClient.send_telemetry(self.telem_catagory, self.get_data())
@abc.abstractproperty
def telem_catagory(self):
"""
:return: Telemetry type
"""
pass
@abc.abstractmethod
def get_data(self):
"""
:return: Data of telemetry (should be dict)
"""
pass

View File

@ -1 +0,0 @@
__author__ = 'VakarisZ'

View File

@ -1,41 +0,0 @@
from infection_monkey.config import WormConfiguration, GUID
import requests
import json
from infection_monkey.control import ControlClient
import logging
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
class AttackTelem(object):
def __init__(self, technique, status, data=None):
"""
Default ATT&CK telemetry constructor
:param technique: Technique ID. E.g. T111
:param status: int from ScanStatus Enum
:param data: Dictionary of other relevant info. E.g. {'brute_force_blocked': True}
"""
self.technique = technique
self.result = status
self.data = {'status': status, 'id': GUID}
if data:
self.data.update(data)
def send(self):
"""
Sends telemetry to island
"""
if not WormConfiguration.current_server:
return
try:
requests.post("https://%s/api/attack/%s" % (WormConfiguration.current_server, self.technique),
data=json.dumps(self.data),
headers={'content-type': 'application/json'},
verify=False,
proxies=ControlClient.proxies)
except Exception as exc:
LOG.warn("Error connecting to control server %s: %s",
WormConfiguration.current_server, exc)

View File

@ -1,18 +0,0 @@
from infection_monkey.transport.attack_telems.base_telem import AttackTelem
__author__ = "VakarisZ"
class VictimHostTelem(AttackTelem):
def __init__(self, technique, status, machine, data=None):
"""
ATT&CK telemetry that parses and sends VictimHost's (remote machine's) data
:param technique: Technique ID. E.g. T111
:param status: int from ScanStatus Enum
:param machine: VictimHost obj from model/host.py
:param data: Other data relevant to the attack technique
"""
super(VictimHostTelem, self).__init__(technique, status, data)
victim_host = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr}
self.data.update({'machine': victim_host})

View File

@ -1,7 +1,9 @@
import os import os
import sys
import shutil import shutil
import struct import struct
import sys
import tempfile
from infection_monkey.config import WormConfiguration from infection_monkey.config import WormConfiguration
@ -16,10 +18,9 @@ def get_dropper_log_path():
def is_64bit_windows_os(): def is_64bit_windows_os():
''' """
Checks for 64 bit Windows OS using environment variables. Checks for 64 bit Windows OS using environment variables.
:return: """
'''
return 'PROGRAMFILES(X86)' in os.environ return 'PROGRAMFILES(X86)' in os.environ
@ -53,7 +54,4 @@ def remove_monkey_dir():
def get_monkey_dir_path(): def get_monkey_dir_path():
if is_windows_os(): return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name)
return WormConfiguration.monkey_dir_windows
else:
return WormConfiguration.monkey_dir_linux

View File

@ -33,7 +33,6 @@ from monkey_island.cc.services.database import Database
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
from monkey_island.cc.resources.pba_file_upload import FileUpload from monkey_island.cc.resources.pba_file_upload import FileUpload
from monkey_island.cc.resources.attack.attack_telem import AttackTelem
from monkey_island.cc.resources.attack.attack_config import AttackConfiguration from monkey_island.cc.resources.attack.attack_config import AttackConfiguration
from monkey_island.cc.resources.attack.attack_report import AttackReport from monkey_island.cc.resources.attack.attack_report import AttackReport
@ -133,7 +132,6 @@ def init_api_resources(api):
'/api/fileUpload/<string:file_type>?restore=<string:filename>') '/api/fileUpload/<string:file_type>?restore=<string:filename>')
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
api.add_resource(AttackConfiguration, '/api/attack') api.add_resource(AttackConfiguration, '/api/attack')
api.add_resource(AttackTelem, '/api/attack/<string:technique>')
api.add_resource(AttackReport, '/api/attack/report') api.add_resource(AttackReport, '/api/attack/report')
api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/') api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/')

View File

@ -25,3 +25,14 @@ def is_db_server_up(mongo_url):
return True return True
except ServerSelectionTimeoutError: except ServerSelectionTimeoutError:
return False return False
def get_db_version(mongo_url):
"""
Return the mongo db version
:param mongo_url: Which mongo to check.
:return: version as a tuple (e.g. `(u'4', u'0', u'8')`)
"""
client = MongoClient(mongo_url, serverSelectionTimeoutMS=100)
server_version = tuple(client.server_info()['version'].split('.'))
return server_version

View File

@ -6,6 +6,8 @@ import sys
import time import time
import logging import logging
MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0"
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_PATH not in sys.path: if BASE_PATH not in sys.path:
@ -22,7 +24,7 @@ from monkey_island.cc.app import init_app
from monkey_island.cc.exporter_init import populate_exporter_list from monkey_island.cc.exporter_init import populate_exporter_list
from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.environment.environment import env from monkey_island.cc.environment.environment import env
from monkey_island.cc.database import is_db_server_up from monkey_island.cc.database import is_db_server_up, get_db_version
def main(): def main():
@ -31,10 +33,8 @@ def main():
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
wait_for_mongo_db_server(mongo_url)
while not is_db_server_up(mongo_url): assert_mongo_db_version(mongo_url)
logger.info('Waiting for MongoDB server')
time.sleep(1)
populate_exporter_list() populate_exporter_list()
app = init_app(mongo_url) app = init_app(mongo_url)
@ -55,5 +55,27 @@ def main():
IOLoop.instance().start() IOLoop.instance().start()
def wait_for_mongo_db_server(mongo_url):
while not is_db_server_up(mongo_url):
logger.info('Waiting for MongoDB server on {0}'.format(mongo_url))
time.sleep(1)
def assert_mongo_db_version(mongo_url):
"""
Checks if the mongodb version is new enough for running the app.
If the DB is too old, quits.
:param mongo_url: URL to the mongo the Island will use
"""
required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split("."))
server_version = get_db_version(mongo_url)
if server_version < required_version:
logger.error(
'Mongo DB version too old. {0} is required, but got {1}'.format(str(required_version), str(server_version)))
sys.exit(-1)
else:
logger.info('Mongo DB version OK. Got {0}'.format(str(server_version)))
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -55,6 +55,14 @@ class Monkey(Document):
monkey_is_dead = True monkey_is_dead = True
return monkey_is_dead return monkey_is_dead
def get_os(self):
os = "unknown"
if self.description.lower().find("linux") != -1:
os = "linux"
elif self.description.lower().find("windows") != -1:
os = "windows"
return os
class MonkeyNotFoundError(Exception): class MonkeyNotFoundError(Exception):
pass pass

View File

@ -1,13 +1,13 @@
import uuid import uuid
from time import sleep from time import sleep
from unittest import TestCase
from monkey import Monkey from monkey import Monkey
from monkey_island.cc.models.monkey import MonkeyNotFoundError from monkey_island.cc.models.monkey import MonkeyNotFoundError
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
from monkey_ttl import MonkeyTtl from monkey_ttl import MonkeyTtl
class TestMonkey(TestCase): class TestMonkey(IslandTestCase):
""" """
Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and
won't work. won't work.
@ -15,7 +15,11 @@ class TestMonkey(TestCase):
Also, the working directory needs to be the working directory from which you usually run the island so the Also, the working directory needs to be the working directory from which you usually run the island so the
server.json file is found and loaded. server.json file is found and loaded.
""" """
def test_is_dead(self): def test_is_dead(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
# Arrange # Arrange
alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30)
alive_monkey_ttl.save() alive_monkey_ttl.save()
@ -43,6 +47,9 @@ class TestMonkey(TestCase):
self.assertFalse(alive_monkey.is_dead()) self.assertFalse(alive_monkey.is_dead())
def test_get_single_monkey_by_id(self): def test_get_single_monkey_by_id(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
# Arrange # Arrange
a_monkey = Monkey(guid=str(uuid.uuid4())) a_monkey = Monkey(guid=str(uuid.uuid4()))
a_monkey.save() a_monkey.save()
@ -52,3 +59,21 @@ class TestMonkey(TestCase):
self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id)) self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id))
# Raise on non-existent monkey # Raise on non-existent monkey
self.assertRaises(MonkeyNotFoundError, Monkey.get_single_monkey_by_id, "abcdefabcdefabcdefabcdef") self.assertRaises(MonkeyNotFoundError, Monkey.get_single_monkey_by_id, "abcdefabcdefabcdefabcdef")
def test_get_os(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
linux_monkey = Monkey(guid=str(uuid.uuid4()),
description="Linux shay-Virtual-Machine 4.15.0-50-generic #54-Ubuntu SMP Mon May 6 18:46:08 UTC 2019 x86_64 x86_64")
windows_monkey = Monkey(guid=str(uuid.uuid4()),
description="Windows bla bla bla")
unknown_monkey = Monkey(guid=str(uuid.uuid4()),
description="bla bla bla")
linux_monkey.save()
windows_monkey.save()
unknown_monkey.save()
self.assertEquals(1, len(filter(lambda m: m.get_os() == "windows", Monkey.objects())))
self.assertEquals(1, len(filter(lambda m: m.get_os() == "linux", Monkey.objects())))
self.assertEquals(1, len(filter(lambda m: m.get_os() == "unknown", Monkey.objects())))

View File

@ -1,7 +1,7 @@
import flask_restful import flask_restful
from flask import jsonify from flask import jsonify
from cc.auth import jwt_required from monkey_island.cc.auth import jwt_required
from cc.services.attack.attack_report import AttackReportService from monkey_island.cc.services.attack.attack_report import AttackReportService
__author__ = "VakarisZ" __author__ = "VakarisZ"

View File

@ -1,24 +0,0 @@
import flask_restful
from flask import request
import json
from monkey_island.cc.services.attack.attack_telem import AttackTelemService
import logging
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
class AttackTelem(flask_restful.Resource):
"""
ATT&CK endpoint used to retrieve matrix related info from monkey
"""
def post(self, technique):
"""
Gets ATT&CK telemetry data and stores it in the database
:param technique: Technique ID, e.g. T1111
"""
data = json.loads(request.data)
AttackTelemService.set_results(technique, data)
return {}

View File

@ -95,7 +95,7 @@ class Monkey(flask_restful.Resource):
parent_to_add = (monkey_json.get('guid'), None) # default values in case of manual run parent_to_add = (monkey_json.get('guid'), None) # default values in case of manual run
if parent and parent != monkey_json.get('guid'): # current parent is known if parent and parent != monkey_json.get('guid'): # current parent is known
exploit_telem = [x for x in exploit_telem = [x for x in
mongo.db.telemetry.find({'telem_type': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, mongo.db.telemetry.find({'telem_catagory': {'$eq': 'exploit'}, 'data.result': {'$eq': True},
'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}, 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']},
'monkey_guid': {'$eq': parent}})] 'monkey_guid': {'$eq': parent}})]
if 1 == len(exploit_telem): if 1 == len(exploit_telem):
@ -104,7 +104,7 @@ class Monkey(flask_restful.Resource):
parent_to_add = (parent, None) parent_to_add = (parent, None)
elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json: elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json:
exploit_telem = [x for x in exploit_telem = [x for x in
mongo.db.telemetry.find({'telem_type': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, mongo.db.telemetry.find({'telem_catagory': {'$eq': 'exploit'}, 'data.result': {'$eq': True},
'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}})] 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}})]
if 1 == len(exploit_telem): if 1 == len(exploit_telem):

View File

@ -8,7 +8,7 @@ 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
from monkey_island.cc.services.report import ReportService from monkey_island.cc.services.report import ReportService
from cc.services.attack.attack_report import AttackReportService from monkey_island.cc.services.attack.attack_report import AttackReportService
from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.services.database import Database from monkey_island.cc.services.database import Database

View File

@ -26,7 +26,7 @@ class Telemetry(flask_restful.Resource):
@jwt_required() @jwt_required()
def get(self, **kw): def get(self, **kw):
monkey_guid = request.args.get('monkey_guid') monkey_guid = request.args.get('monkey_guid')
telem_type = request.args.get('telem_type') telem_catagory = request.args.get('telem_catagory')
timestamp = request.args.get('timestamp') timestamp = request.args.get('timestamp')
if "null" == timestamp: # special case to avoid ugly JS code... if "null" == timestamp: # special case to avoid ugly JS code...
timestamp = None timestamp = None
@ -36,8 +36,8 @@ class Telemetry(flask_restful.Resource):
if monkey_guid: if monkey_guid:
find_filter["monkey_guid"] = {'$eq': monkey_guid} find_filter["monkey_guid"] = {'$eq': monkey_guid}
if telem_type: if telem_catagory:
find_filter["telem_type"] = {'$eq': telem_type} find_filter["telem_catagory"] = {'$eq': telem_catagory}
if timestamp: if timestamp:
find_filter['timestamp'] = {'$gt': dateutil.parser.parse(timestamp)} find_filter['timestamp'] = {'$gt': dateutil.parser.parse(timestamp)}
@ -53,11 +53,11 @@ class Telemetry(flask_restful.Resource):
try: try:
NodeService.update_monkey_modify_time(monkey["_id"]) NodeService.update_monkey_modify_time(monkey["_id"])
telem_type = telemetry_json.get('telem_type') telem_catagory = telemetry_json.get('telem_catagory')
if telem_type in TELEM_PROCESS_DICT: if telem_catagory in TELEM_PROCESS_DICT:
TELEM_PROCESS_DICT[telem_type](telemetry_json) TELEM_PROCESS_DICT[telem_catagory](telemetry_json)
else: else:
logger.info('Got unknown type of telemetry: %s' % telem_type) logger.info('Got unknown type of telemetry: %s' % telem_catagory)
except Exception as ex: except Exception as ex:
logger.error("Exception caught while processing telemetry", exc_info=True) logger.error("Exception caught while processing telemetry", exc_info=True)
@ -79,7 +79,7 @@ class Telemetry(flask_restful.Resource):
monkey_label = telem_monkey_guid monkey_label = telem_monkey_guid
x["monkey"] = monkey_label x["monkey"] = monkey_label
objects.append(x) objects.append(x)
if x['telem_type'] == 'system_info_collection' and 'credentials' in x['data']: if x['telem_catagory'] == 'system_info_collection' and 'credentials' in x['data']:
for user in x['data']['credentials']: for user in x['data']['credentials']:
if -1 != user.find(','): if -1 != user.find(','):
new_user = user.replace(',', '.') new_user = user.replace(',', '.')
@ -265,6 +265,12 @@ class Telemetry(flask_restful.Resource):
{'guid': telemetry_json['monkey_guid']}, {'guid': telemetry_json['monkey_guid']},
{'$push': {'pba_results': telemetry_json['data']}}) {'$push': {'pba_results': telemetry_json['data']}})
@staticmethod
def process_attack_telemetry(telemetry_json):
# No processing required
pass
TELEM_PROCESS_DICT = \ TELEM_PROCESS_DICT = \
{ {
'tunnel': Telemetry.process_tunnel_telemetry, 'tunnel': Telemetry.process_tunnel_telemetry,
@ -273,5 +279,6 @@ TELEM_PROCESS_DICT = \
'scan': Telemetry.process_scan_telemetry, 'scan': Telemetry.process_scan_telemetry,
'system_info_collection': Telemetry.process_system_info_telemetry, 'system_info_collection': Telemetry.process_system_info_telemetry,
'trace': Telemetry.process_trace_telemetry, 'trace': Telemetry.process_trace_telemetry,
'post_breach': Telemetry.process_post_breach_telemetry 'post_breach': Telemetry.process_post_breach_telemetry,
'attack': Telemetry.process_attack_telemetry
} }

View File

@ -38,7 +38,7 @@ class TelemetryFeed(flask_restful.Resource):
'id': telem['_id'], 'id': telem['_id'],
'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'), 'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'),
'hostname': monkey.get('hostname', default_hostname) if monkey else default_hostname, 'hostname': monkey.get('hostname', default_hostname) if monkey else default_hostname,
'brief': TELEM_PROCESS_DICT[telem['telem_type']](telem) 'brief': TELEM_PROCESS_DICT[telem['telem_catagory']](telem)
} }
@staticmethod @staticmethod
@ -86,6 +86,10 @@ class TelemetryFeed(flask_restful.Resource):
telem['data']['hostname'], telem['data']['hostname'],
telem['data']['ip']) telem['data']['ip'])
@staticmethod
def get_attack_telem_brief(telem):
return 'Monkey collected MITRE ATT&CK info.'
TELEM_PROCESS_DICT = \ TELEM_PROCESS_DICT = \
{ {
@ -95,5 +99,6 @@ TELEM_PROCESS_DICT = \
'scan': TelemetryFeed.get_scan_telem_brief, 'scan': TelemetryFeed.get_scan_telem_brief,
'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief, 'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief,
'trace': TelemetryFeed.get_trace_telem_brief, 'trace': TelemetryFeed.get_trace_telem_brief,
'post_breach': TelemetryFeed.get_post_breach_telem_brief 'post_breach': TelemetryFeed.get_post_breach_telem_brief,
'attack': TelemetryFeed.get_attack_telem_brief
} }

View File

@ -30,7 +30,7 @@ class AttackReportService:
Generates new report based on telemetries, replaces old report in db with new one. Generates new report based on telemetries, replaces old report in db with new one.
:return: Report object :return: Report object
""" """
report = {'techniques': {}, 'meta': AttackTelemService.get_latest_telem(), 'name': REPORT_NAME} report = {'techniques': {}, 'latest_telem_time': AttackReportService.get_latest_attack_telem_time(), 'name': REPORT_NAME}
for tech_id, value in AttackConfig.get_technique_values().items(): for tech_id, value in AttackConfig.get_technique_values().items():
if value: if value:
try: try:
@ -41,6 +41,14 @@ class AttackReportService:
mongo.db.attack_report.replace_one({'name': REPORT_NAME}, report, upsert=True) mongo.db.attack_report.replace_one({'name': REPORT_NAME}, report, upsert=True)
return report return report
@staticmethod
def get_latest_attack_telem_time():
"""
Gets timestamp of latest attack telem
:return: timestamp of latest attack telem
"""
return [x['timestamp'] for x in mongo.db.telemetry.find({'telem_catagory': 'attack'}).sort('timestamp', -1).limit(1)][0]
@staticmethod @staticmethod
def get_latest_report(): def get_latest_report():
""" """
@ -48,9 +56,9 @@ class AttackReportService:
:return: report dict. :return: report dict.
""" """
if AttackReportService.is_report_generated(): if AttackReportService.is_report_generated():
telem_time = AttackTelemService.get_latest_telem() telem_time = AttackReportService.get_latest_attack_telem_time()
latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME}) latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME})
if telem_time and latest_report['meta'] and telem_time['time'] == latest_report['meta']['time']: if telem_time and latest_report['latest_telem_time'] and telem_time == latest_report['latest_telem_time']:
return latest_report return latest_report
return AttackReportService.generate_new_report() return AttackReportService.generate_new_report()

View File

@ -1,30 +0,0 @@
"""
File that contains ATT&CK telemetry storing/retrieving logic
"""
import logging
from monkey_island.cc.database import mongo
__author__ = "VakarisZ"
logger = logging.getLogger(__name__)
class AttackTelemService(object):
def __init__(self):
pass
@staticmethod
def set_results(technique, data):
"""
Adds ATT&CK technique results(telemetry) to the database
:param technique: technique ID string e.g. T1110
:param data: Data, relevant to the technique
"""
data.update({'technique': technique})
mongo.db.attack_results.insert(data)
mongo.db.attack_results.update({'name': 'latest'}, {'name': 'latest', 'time': data['time']}, upsert=True)
@staticmethod
def get_latest_telem():
return mongo.db.attack_results.find_one({'name': 'latest'})

View File

@ -17,9 +17,10 @@ class T1003(AttackTechnique):
@staticmethod @staticmethod
def get_report_data(): def get_report_data():
data = {'title': T1003.technique_title(T1003.tech_id)} data = {'title': T1003.technique_title()}
if mongo.db.telemetry.count_documents(T1003.query): if mongo.db.telemetry.count_documents(T1003.query):
data.update({'message': T1003.used_msg, 'status': ScanStatus.USED.name}) status = ScanStatus.USED
else: else:
data.update({'message': T1003.unscanned_msg, 'status': ScanStatus.UNSCANNED.name}) status = ScanStatus.UNSCANNED
data.update(T1003.get_message_and_status(status))
return data return data

View File

@ -22,9 +22,10 @@ class T1059(AttackTechnique):
@staticmethod @staticmethod
def get_report_data(): def get_report_data():
cmd_data = list(mongo.db.telemetry.aggregate(T1059.query)) cmd_data = list(mongo.db.telemetry.aggregate(T1059.query))
data = {'title': T1059.technique_title(T1059.tech_id), 'cmds': cmd_data} data = {'title': T1059.technique_title(), 'cmds': cmd_data}
if cmd_data: if cmd_data:
data.update({'message': T1059.used_msg, 'status': ScanStatus.USED.name}) status = ScanStatus.USED
else: else:
data.update({'message': T1059.unscanned_msg, 'status': ScanStatus.UNSCANNED.name}) status = ScanStatus.UNSCANNED
data.update(T1059.get_message_and_status(status))
return data return data

View File

@ -16,7 +16,7 @@ class T1075(AttackTechnique):
{'lm_hash': {'$ne': ''}}]}}} {'lm_hash': {'$ne': ''}}]}}}
# Gets data about successful PTH logins # Gets data about successful PTH logins
query = [{'$match': {'telem_type': 'exploit', query = [{'$match': {'telem_category': 'exploit',
'data.attempts': {'$not': {'$size': 0}, 'data.attempts': {'$not': {'$size': 0},
'$elemMatch': {'$and': [{'$or': [{'ntlm_hash': {'$ne': ''}}, '$elemMatch': {'$and': [{'$or': [{'ntlm_hash': {'$ne': ''}},
{'lm_hash': {'$ne': ''}}]}, {'lm_hash': {'$ne': ''}}]},
@ -31,13 +31,14 @@ class T1075(AttackTechnique):
@staticmethod @staticmethod
def get_report_data(): def get_report_data():
data = {'title': T1075.technique_title(T1075.tech_id)} data = {'title': T1075.technique_title()}
successful_logins = list(mongo.db.telemetry.aggregate(T1075.query)) successful_logins = list(mongo.db.telemetry.aggregate(T1075.query))
data.update({'successful_logins': successful_logins}) data.update({'successful_logins': successful_logins})
if successful_logins: if successful_logins:
data.update({'message': T1075.used_msg, 'status': ScanStatus.USED.name}) status = ScanStatus.USED
elif mongo.db.telemetry.count_documents(T1075.login_attempt_query): elif mongo.db.telemetry.count_documents(T1075.login_attempt_query):
data.update({'message': T1075.scanned_msg, 'status': ScanStatus.SCANNED.name}) status = ScanStatus.SCANNED
else: else:
data.update({'message': T1075.unscanned_msg, 'status': ScanStatus.UNSCANNED.name}) status = ScanStatus.UNSCANNED
data.update(T1075.get_message_and_status(status))
return data return data

View File

@ -35,12 +35,16 @@ class T1110(AttackTechnique):
result['successful_creds'].append(T1110.parse_creds(attempt)) result['successful_creds'].append(T1110.parse_creds(attempt))
if succeeded: if succeeded:
data = {'message': T1110.used_msg, 'status': ScanStatus.USED.name} status = ScanStatus.USED
elif attempts: elif attempts:
data = {'message': T1110.scanned_msg, 'status': ScanStatus.SCANNED.name} status = ScanStatus.SCANNED
else: else:
data = {'message': T1110.unscanned_msg, 'status': ScanStatus.UNSCANNED.name} status = ScanStatus.UNSCANNED
data.update({'services': attempts, 'title': T1110.technique_title(T1110.tech_id)}) data = T1110.get_message_and_status(status)
# Remove data with no successful brute force attempts
attempts = [attempt for attempt in attempts if attempt['attempts']]
data.update({'services': attempts, 'title': T1110.technique_title()})
return data return data
@staticmethod @staticmethod
@ -51,21 +55,39 @@ class T1110(AttackTechnique):
:return: string with username and used password/hash :return: string with username and used password/hash
""" """
username = attempt['user'] username = attempt['user']
if attempt['lm_hash']: creds = {'lm_hash': {'type': 'LM hash', 'output': T1110.censor_hash(attempt['lm_hash'])},
return '%s ; LM hash %s ...' % (username, encryptor.dec(attempt['lm_hash'])[0:5]) 'ntlm_hash': {'type': 'NTLM hash', 'output': T1110.censor_hash(attempt['ntlm_hash'], 20)},
if attempt['ntlm_hash']: 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']},
return '%s ; NTLM hash %s ...' % (username, encryptor.dec(attempt['ntlm_hash'])[0:20]) 'password': {'type': 'Plaintext password', 'output': T1110.censor_password(attempt['password'])}}
if attempt['ssh_key']: for key, cred in creds.items():
return '%s ; SSH key %s ...' % (username, encryptor.dec(attempt['ssh_key'])[0:15]) if attempt[key]:
if attempt['password']: return '%s ; %s : %s' % (username,
return '%s : %s' % (username, T1110.obfuscate_password(encryptor.dec(attempt['password']))) cred['type'],
cred['output'])
@staticmethod @staticmethod
def obfuscate_password(password, plain_chars=3): def censor_password(password, plain_chars=3, secret_chars=5):
""" """
Obfuscates password by changing characters to * Decrypts and obfuscates password by changing characters to *
:param password: Password or string to obfuscate :param password: Password or string to obfuscate
:param plain_chars: How many plain-text characters should be kept at the start of the string :param plain_chars: How many plain-text characters should be kept at the start of the string
:param secret_chars: How many * symbols should be used to hide the remainder of the password
:return: Obfuscated string e.g. Pass**** :return: Obfuscated string e.g. Pass****
""" """
return password[0:plain_chars] + '*' * (len(password) - plain_chars) if not password:
return ""
password = encryptor.dec(password)
return password[0:plain_chars] + '*' * secret_chars
@staticmethod
def censor_hash(hash_, plain_chars=5):
"""
Decrypts and obfuscates hash by only showing a part of it
:param hash_: Hash to obfuscate
:param plain_chars: How many chars of hash should be shown
:return: Obfuscated string
"""
if not hash_:
return ""
hash_ = encryptor.dec(hash_)
return hash_[0: plain_chars] + ' ...'

View File

@ -12,13 +12,13 @@ class T1197(AttackTechnique):
@staticmethod @staticmethod
def get_report_data(): def get_report_data():
data = T1197.get_tech_base_data(T1197) data = T1197.get_tech_base_data()
bits_results = mongo.db.attack_results.aggregate([{'$match': {'technique': T1197.tech_id}}, bits_results = mongo.db.telemetry.aggregate([{'$match': {'telem_catagory': 'attack', 'data.technique': T1197.tech_id}},
{'$group': {'_id': {'ip_addr': '$machine.ip_addr', 'usage': '$usage'}, {'$group': {'_id': {'ip_addr': '$data.machine.ip_addr', 'usage': '$data.usage'},
'ip_addr': {'$first': '$machine.ip_addr'}, 'ip_addr': {'$first': '$data.machine.ip_addr'},
'domain_name': {'$first': '$machine.domain_name'}, 'domain_name': {'$first': '$data.machine.domain_name'},
'usage': {'$first': '$usage'}, 'usage': {'$first': '$data.usage'},
'time': {'$first': '$time'}} 'time': {'$first': '$timestamp'}}
}]) }])
bits_results = list(bits_results) bits_results = list(bits_results)
data.update({'bits_jobs': bits_results}) data.update({'bits_jobs': bits_results})

View File

@ -14,21 +14,22 @@ class T1210(AttackTechnique):
@staticmethod @staticmethod
def get_report_data(): def get_report_data():
data = {'title': T1210.technique_title(T1210.tech_id)} data = {'title': T1210.technique_title()}
scanned_services = T1210.get_scanned_services() scanned_services = T1210.get_scanned_services()
exploited_services = T1210.get_exploited_services() exploited_services = T1210.get_exploited_services()
if exploited_services: if exploited_services:
data.update({'status': ScanStatus.USED.name, 'message': T1210.used_msg}) status = ScanStatus.USED
elif scanned_services: elif scanned_services:
data.update({'status': ScanStatus.SCANNED.name, 'message': T1210.scanned_msg}) status = ScanStatus.SCANNED
else: else:
data.update({'status': ScanStatus.UNSCANNED.name, 'message': T1210.unscanned_msg}) status = ScanStatus.UNSCANNED.name
data.update(T1210.get_message_and_status(status))
data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services}) data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services})
return data return data
@staticmethod @staticmethod
def get_scanned_services(): def get_scanned_services():
results = mongo.db.telemetry.aggregate([{'$match': {'telem_type': 'scan'}}, results = mongo.db.telemetry.aggregate([{'$match': {'telem_catagory': 'scan'}},
{'$sort': {'data.service_count': -1}}, {'$sort': {'data.service_count': -1}},
{'$group': { {'$group': {
'_id': {'ip_addr': '$data.machine.ip_addr'}, '_id': {'ip_addr': '$data.machine.ip_addr'},
@ -38,7 +39,7 @@ class T1210(AttackTechnique):
@staticmethod @staticmethod
def get_exploited_services(): def get_exploited_services():
results = mongo.db.telemetry.aggregate([{'$match': {'telem_type': 'exploit', 'data.result': True}}, results = mongo.db.telemetry.aggregate([{'$match': {'telem_catagory': 'exploit', 'data.result': True}},
{'$group': { {'$group': {
'_id': {'ip_addr': '$data.machine.ip_addr'}, '_id': {'ip_addr': '$data.machine.ip_addr'},
'service': {'$first': '$data.info'}, 'service': {'$first': '$data.info'},

View File

@ -46,43 +46,63 @@ class AttackTechnique(object):
""" """
pass pass
@staticmethod @classmethod
def technique_status(technique): def technique_status(cls):
""" """
Gets the status of a certain attack technique. Gets the status of a certain attack technique.
:param technique: technique's id.
:return: ScanStatus Enum object :return: ScanStatus Enum object
""" """
if mongo.db.attack_results.find_one({'status': ScanStatus.USED.value, 'technique': technique}): if mongo.db.attack_results.find_one({'telem_catagory': 'attack',
'status': ScanStatus.USED.value,
'technique': cls.tech_id}):
return ScanStatus.USED return ScanStatus.USED
elif mongo.db.attack_results.find_one({'status': ScanStatus.SCANNED.value, 'technique': technique}): elif mongo.db.attack_results.find_one({'telem_catagory': 'attack',
'status': ScanStatus.SCANNED.value,
'technique': cls.tech_id}):
return ScanStatus.SCANNED return ScanStatus.SCANNED
else: else:
return ScanStatus.UNSCANNED return ScanStatus.UNSCANNED
@staticmethod @classmethod
def technique_title(technique): def get_message_and_status(cls, status):
"""
Returns a dict with attack technique's message and status.
:param status: Enum type value from common/attack_utils.py
:return: Dict with message and status
"""
return {'message': cls.get_message_by_status(status), 'status': status.name}
@classmethod
def get_message_by_status(cls, status):
"""
Picks a message to return based on status.
:param status: Enum type value from common/attack_utils.py
:return: message string
"""
if status == ScanStatus.UNSCANNED:
return cls.unscanned_msg
elif status == ScanStatus.SCANNED:
return cls.scanned_msg
else:
return cls.used_msg
@classmethod
def technique_title(cls):
""" """
:param technique: Technique's id. E.g. T1110
:return: techniques title. E.g. "T1110 Brute force" :return: techniques title. E.g. "T1110 Brute force"
""" """
return AttackConfig.get_technique(technique)['title'] return AttackConfig.get_technique(cls.tech_id)['title']
@staticmethod @classmethod
def get_tech_base_data(technique): def get_tech_base_data(cls):
""" """
Gathers basic attack technique data into a dict. Gathers basic attack technique data into a dict.
:param technique: Technique's id. E.g. T1110
:return: dict E.g. {'message': 'Brute force used', 'status': 'Used', 'title': 'T1110 Brute force'} :return: dict E.g. {'message': 'Brute force used', 'status': 'Used', 'title': 'T1110 Brute force'}
""" """
data = {} data = {}
status = AttackTechnique.technique_status(technique.tech_id) status = cls.technique_status()
title = AttackTechnique.technique_title(technique.tech_id) title = cls.technique_title()
data.update({'status': status.name, 'title': title}) data.update({'status': status.name,
if status == ScanStatus.UNSCANNED: 'title': title,
data.update({'message': technique.unscanned_msg}) 'message': cls.get_message_by_status(status)})
elif status == ScanStatus.SCANNED:
data.update({'message': technique.scanned_msg})
else:
data.update({'message': technique.used_msg})
return data return data

View File

@ -499,17 +499,11 @@ SCHEMA = {
"default": 60, "default": 60,
"description": "Time to keep tunnel open before going down after last exploit (in seconds)" "description": "Time to keep tunnel open before going down after last exploit (in seconds)"
}, },
"monkey_dir_windows": { "monkey_dir_name": {
"title": "Monkey's windows directory", "title": "Monkey's directory name",
"type": "string", "type": "string",
"default": r"C:\Windows\temp\monkey_dir", "default": r"monkey_dir",
"description": "Directory containing all monkey files on windows" "description": "Directory name for the directory which will contain all of the monkey files"
},
"monkey_dir_linux": {
"title": "Monkey's linux directory",
"type": "string",
"default": "/tmp/monkey_dir",
"description": "Directory containing all monkey files on linux"
}, },
} }
}, },

View File

@ -1,6 +1,7 @@
from itertools import product from itertools import product
from monkey_island.cc.database import mongo from monkey_island.cc.database import mongo
from monkey_island.cc.models import Monkey
from bson import ObjectId from bson import ObjectId
from monkey_island.cc.services.groups_and_users_consts import USERTYPE from monkey_island.cc.services.groups_and_users_consts import USERTYPE
@ -216,15 +217,15 @@ class PTHReportService(object):
@staticmethod @staticmethod
def generate_map_nodes(): def generate_map_nodes():
monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1}) monkeys = filter(lambda m: m.get_os() == "windows", Monkey.objects())
return [ return [
{ {
'id': monkey['_id'], 'id': monkey.guid,
'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]), 'label': '{0} : {1}'.format(monkey.hostname, monkey.ip_addresses[0]),
'group': 'critical' if monkey.get('critical_services', []) else 'normal', 'group': 'critical' if monkey.critical_services is not None else 'normal',
'services': monkey.get('critical_services', []), 'services': monkey.critical_services,
'hostname': monkey['hostname'] 'hostname': monkey.hostname
} for monkey in monkeys } for monkey in monkeys
] ]

View File

@ -171,7 +171,7 @@ class ReportService:
PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
creds = [] creds = []
for telem in mongo.db.telemetry.find( for telem in mongo.db.telemetry.find(
{'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}}, {'telem_catagory': 'system_info_collection', 'data.credentials': {'$exists': True}},
{'data.credentials': 1, 'monkey_guid': 1} {'data.credentials': 1, 'monkey_guid': 1}
): ):
monkey_creds = telem['data']['credentials'] monkey_creds = telem['data']['credentials']
@ -199,7 +199,7 @@ class ReportService:
""" """
creds = [] creds = []
for telem in mongo.db.telemetry.find( for telem in mongo.db.telemetry.find(
{'telem_type': 'system_info_collection', 'data.ssh_info': {'$exists': True}}, {'telem_catagory': 'system_info_collection', 'data.ssh_info': {'$exists': True}},
{'data.ssh_info': 1, 'monkey_guid': 1} {'data.ssh_info': 1, 'monkey_guid': 1}
): ):
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
@ -220,7 +220,7 @@ class ReportService:
""" """
creds = [] creds = []
for telem in mongo.db.telemetry.find( for telem in mongo.db.telemetry.find(
{'telem_type': 'system_info_collection', 'data.Azure': {'$exists': True}}, {'telem_catagory': 'system_info_collection', 'data.Azure': {'$exists': True}},
{'data.Azure': 1, 'monkey_guid': 1} {'data.Azure': 1, 'monkey_guid': 1}
): ):
azure_users = telem['data']['Azure']['usernames'] azure_users = telem['data']['Azure']['usernames']
@ -373,7 +373,7 @@ class ReportService:
@staticmethod @staticmethod
def get_exploits(): def get_exploits():
exploits = [] exploits = []
for exploit in mongo.db.telemetry.find({'telem_type': 'exploit', 'data.result': True}): for exploit in mongo.db.telemetry.find({'telem_catagory': 'exploit', 'data.result': True}):
new_exploit = ReportService.process_exploit(exploit) 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)
@ -382,7 +382,7 @@ class ReportService:
@staticmethod @staticmethod
def get_monkey_subnets(monkey_guid): def get_monkey_subnets(monkey_guid):
network_info = mongo.db.telemetry.find_one( network_info = mongo.db.telemetry.find_one(
{'telem_type': 'system_info_collection', 'monkey_guid': monkey_guid}, {'telem_catagory': 'system_info_collection', 'monkey_guid': monkey_guid},
{'data.network_info.networks': 1} {'data.network_info.networks': 1}
) )
if network_info is None: if network_info is None:
@ -540,7 +540,7 @@ class ReportService:
@staticmethod @staticmethod
def get_cross_segment_issues(): def get_cross_segment_issues():
scans = mongo.db.telemetry.find({'telem_type': 'scan'}, scans = mongo.db.telemetry.find({'telem_catagory': 'scan'},
{'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1}) {'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1})
cross_segment_issues = [] cross_segment_issues = []

View File

@ -0,0 +1,69 @@
import uuid
from monkey_island.cc.models import Monkey
from monkey_island.cc.services.pth_report import PTHReportService
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
class TestPTHReportServiceGenerateMapNodes(IslandTestCase):
def test_generate_map_nodes(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
self.assertEqual(PTHReportService.generate_map_nodes(), [])
windows_monkey_with_services = Monkey(
guid=str(uuid.uuid4()),
hostname="A_Windows_PC_1",
critical_services=["aCriticalService", "Domain Controller"],
ip_addresses=["1.1.1.1", "2.2.2.2"],
description="windows 10"
)
windows_monkey_with_services.save()
windows_monkey_with_no_services = Monkey(
guid=str(uuid.uuid4()),
hostname="A_Windows_PC_2",
critical_services=[],
ip_addresses=["3.3.3.3"],
description="windows 10"
)
windows_monkey_with_no_services.save()
linux_monkey = Monkey(
guid=str(uuid.uuid4()),
hostname="A_Linux_PC",
ip_addresses=["4.4.4.4"],
description="linux ubuntu"
)
linux_monkey.save()
map_nodes = PTHReportService.generate_map_nodes()
self.assertEquals(2, len(map_nodes))
def test_generate_map_nodes_parsing(self):
self.fail_if_not_testing_env()
self.clean_monkey_db()
monkey_id = str(uuid.uuid4())
hostname = "A_Windows_PC_1"
windows_monkey_with_services = Monkey(
guid=monkey_id,
hostname=hostname,
critical_services=["aCriticalService", "Domain Controller"],
ip_addresses=["1.1.1.1"],
description="windows 10"
)
windows_monkey_with_services.save()
map_nodes = PTHReportService.generate_map_nodes()
self.assertEquals(map_nodes[0]["id"], monkey_id)
self.assertEquals(map_nodes[0]["label"], "A_Windows_PC_1 : 1.1.1.1")
self.assertEquals(map_nodes[0]["group"], "critical")
self.assertEquals(len(map_nodes[0]["services"]), 2)
self.assertEquals(map_nodes[0]["hostname"], hostname)

View File

@ -0,0 +1,12 @@
import unittest
from monkey_island.cc.environment.environment import env
from monkey_island.cc.models import Monkey
class IslandTestCase(unittest.TestCase):
def fail_if_not_testing_env(self):
self.failIf(not env.testing, "Change server_config.json to testing environment.")
@staticmethod
def clean_monkey_db():
Monkey.objects().delete()

View File

@ -6,6 +6,7 @@
"clean": "rimraf dist/*", "clean": "rimraf dist/*",
"copy": "copyfiles -f ./src/index.html ./src/favicon.ico ./dist", "copy": "copyfiles -f ./src/index.html ./src/favicon.ico ./dist",
"dist": "webpack --mode production", "dist": "webpack --mode production",
"dev": "webpack --mode development",
"lint": "eslint ./src", "lint": "eslint ./src",
"posttest": "npm run lint", "posttest": "npm run lint",
"release:major": "npm version major && npm publish && git push --follow-tags", "release:major": "npm version major && npm publish && git push --follow-tags",
@ -100,6 +101,7 @@
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"sha3": "^2.0.0", "sha3": "^2.0.0",
"react-spinners": "^0.5.4", "react-spinners": "^0.5.4",
"@emotion/core": "^10.0.10" "@emotion/core": "^10.0.10",
"react-desktop-notification": "^1.0.9"
} }
} }

View File

@ -14,6 +14,8 @@ import ReportPage from 'components/pages/ReportPage';
import LicensePage from 'components/pages/LicensePage'; import LicensePage from 'components/pages/LicensePage';
import AuthComponent from 'components/AuthComponent'; import AuthComponent from 'components/AuthComponent';
import LoginPageComponent from 'components/pages/LoginPage'; import LoginPageComponent from 'components/pages/LoginPage';
import Notifier from "react-desktop-notification"
import 'normalize.css/normalize.css'; import 'normalize.css/normalize.css';
import 'react-data-components/css/table-twbs.css'; import 'react-data-components/css/table-twbs.css';
@ -25,6 +27,7 @@ import VersionComponent from "./side-menu/VersionComponent";
let logoImage = require('../images/monkey-icon.svg'); let logoImage = require('../images/monkey-icon.svg');
let infectionMonkeyImage = require('../images/infection-monkey.svg'); let infectionMonkeyImage = require('../images/infection-monkey.svg');
let guardicoreLogoImage = require('../images/guardicore-logo.png'); let guardicoreLogoImage = require('../images/guardicore-logo.png');
let notificationIcon = require('../images/notification-logo-512x512.png');
class AppComponent extends AuthComponent { class AppComponent extends AuthComponent {
updateStatus = () => { updateStatus = () => {
@ -50,6 +53,7 @@ class AppComponent extends AuthComponent {
} }
if (isChanged) { if (isChanged) {
this.setState({completedSteps: res['completed_steps']}); this.setState({completedSteps: res['completed_steps']});
this.showInfectionDoneNotification();
} }
}); });
} }
@ -194,6 +198,20 @@ class AppComponent extends AuthComponent {
</Router> </Router>
); );
} }
showInfectionDoneNotification() {
if (this.state.completedSteps.infection_done) {
let hostname = window.location.hostname;
let url = `https://${hostname}:5000/report`;
console.log("Trying to show notification. URL: " + url + " | icon: " + notificationIcon);
Notifier.start(
"Monkey Island",
"Infection is done! Click here to go to the report page.",
url,
notificationIcon);
}
}
} }
AppComponent.defaultProps = {}; AppComponent.defaultProps = {};

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
export function RenderMachine(val){ export function renderMachine(val){
return ( return (
<span>{val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}</span> <span>{val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}</span>
) )

View File

@ -10,7 +10,7 @@ class T1059 extends React.Component {
super(props); super(props);
} }
static getHashColumns() { static getCommandColumns() {
return ([{ return ([{
Header: 'Example commands used', Header: 'Example commands used',
columns: [ columns: [
@ -27,7 +27,7 @@ class T1059 extends React.Component {
<br/> <br/>
{this.props.data.status === 'USED' ? {this.props.data.status === 'USED' ?
<ReactTable <ReactTable
columns={T1059.getHashColumns()} columns={T1059.getCommandColumns()}
data={this.props.data.cmds} data={this.props.data.cmds}
showPagination={false} showPagination={false}
defaultPageSize={this.props.data.cmds.length} defaultPageSize={this.props.data.cmds.length}

View File

@ -1,26 +1,28 @@
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 } from "./Helpers" import { renderMachine } from "./Helpers"
class T1075 extends React.Component { class T1075 extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.props.data.successful_logins.forEach((login) => { this.props.data.successful_logins.forEach((login) => this.setLoginHashType(login))
if(login.attempts[0].ntlm_hash !== ""){ }
login.attempts[0].hashType = 'NTLM';
} else if(login.attempts[0].lm_hash !== ""){ setLoginHashType(login){
login.attempts[0].hashType = 'LM'; if(login.attempts[0].ntlm_hash !== ""){
} login.attempts[0].hashType = 'NTLM';
}) } else if(login.attempts[0].lm_hash !== ""){
login.attempts[0].hashType = 'LM';
}
} }
static getHashColumns() { static getHashColumns() {
return ([{ return ([{
columns: [ columns: [
{Header: 'Machine', id: 'machine', accessor: x => RenderMachine(x.machine), style: { 'whiteSpace': 'unset' }}, {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), style: { 'whiteSpace': 'unset' }},
{Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }}, {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }},
{Header: 'Username', id: 'username', accessor: x => x.attempts[0].user, style: { 'whiteSpace': 'unset' }}, {Header: 'Username', id: 'username', accessor: x => x.attempts[0].user, style: { 'whiteSpace': 'unset' }},
{Header: 'Hash type', id: 'hash', accessor: x => x.attempts[0].hashType, style: { 'whiteSpace': 'unset' }}, {Header: 'Hash type', id: 'hash', accessor: x => x.attempts[0].hashType, style: { 'whiteSpace': 'unset' }},

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 } from "./Helpers" import { renderMachine } from "./Helpers"
class T1110 extends React.Component { class T1110 extends React.Component {
@ -13,7 +13,7 @@ class T1110 extends React.Component {
static getServiceColumns() { static getServiceColumns() {
return ([{ return ([{
columns: [ columns: [
{Header: 'Machine', id: 'machine', accessor: x => RenderMachine(x.machine), {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 160}, style: { 'whiteSpace': 'unset' }, width: 160},
{Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100}, {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100},
{Header: 'Started', id: 'started', accessor: x => x.info.started, style: { 'whiteSpace': 'unset' }}, {Header: 'Started', id: 'started', accessor: x => x.info.started, style: { 'whiteSpace': 'unset' }},

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 } from "./Helpers" import { renderMachine } from "./Helpers"
class T1210 extends React.Component { class T1210 extends React.Component {
@ -9,7 +9,7 @@ class T1210 extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.columns = [ {Header: 'Machine', this.columns = [ {Header: 'Machine',
id: 'machine', accessor: x => RenderMachine(x), id: 'machine', accessor: x => renderMachine(x),
style: { 'whiteSpace': 'unset' }, style: { 'whiteSpace': 'unset' },
width: 200}, width: 200},
{Header: 'Time', {Header: 'Time',

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 } from "./Helpers" import { renderMachine } from "./Helpers"
class T1210 extends React.Component { class T1210 extends React.Component {
@ -13,7 +13,7 @@ class T1210 extends React.Component {
static getScanColumns() { static getScanColumns() {
return ([{ return ([{
columns: [ columns: [
{Header: 'Machine', id: 'machine', accessor: x => RenderMachine(x.machine), {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 200}, style: { 'whiteSpace': 'unset' }, width: 200},
{Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170}, {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170},
{Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }}, {Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }},
@ -24,7 +24,7 @@ class T1210 extends React.Component {
static getExploitColumns() { static getExploitColumns() {
return ([{ return ([{
columns: [ columns: [
{Header: 'Machine', id: 'machine', accessor: x => RenderMachine(x.machine), {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 200}, style: { 'whiteSpace': 'unset' }, width: 200},
{Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170}, {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170},
{Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }}, {Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }},

View File

@ -11,7 +11,7 @@ const renderTime = (val) => val.split('.')[0];
const columns = [ const columns = [
{ title: 'Time', prop: 'timestamp', render: renderTime}, { title: 'Time', prop: 'timestamp', render: renderTime},
{ title: 'Monkey', prop: 'monkey'}, { title: 'Monkey', prop: 'monkey'},
{ title: 'Type', prop: 'telem_type'}, { title: 'Type', prop: 'telem_catagory'},
{ title: 'Details', prop: 'data', render: renderJson, width: '40%' } { title: 'Details', prop: 'data', render: renderJson, width: '40%' }
]; ];

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB