Merge remote-tracking branch 'origin/develop' into feature/refactor-attack-telem

# Conflicts:
#	monkey/monkey_island/cc/services/attack/attack_report.py
This commit is contained in:
itay 2019-06-18 15:34:15 +03:00
commit 232a82334a
29 changed files with 396 additions and 71 deletions

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

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

@ -1,6 +1,7 @@
from abc import ABCMeta, abstractmethod, abstractproperty from abc import ABCMeta, abstractmethod, abstractproperty
import infection_monkey.config import infection_monkey.config
from common.utils.exploit_enum import ExploitType from common.utils.exploit_enum import ExploitType
from datetime import datetime
__author__ = 'itamar' __author__ = 'itamar'
@ -20,11 +21,19 @@ class HostExploiter(object):
def __init__(self, host): def __init__(self, host):
self._config = infection_monkey.config.WormConfiguration self._config = infection_monkey.config.WormConfiguration
self._exploit_info = {'display_name': self._EXPLOITED_SERVICE, self._exploit_info = {'display_name': self._EXPLOITED_SERVICE,
'started': '',
'finished': '',
'vulnerable_urls': [], 'vulnerable_urls': [],
'vulnerable_ports': []} 'vulnerable_ports': []}
self._exploit_attempts = [] self._exploit_attempts = []
self.host = host self.host = host
def set_start_time(self):
self._exploit_info['started'] = datetime.now().isoformat()
def set_finish_time(self):
self._exploit_info['finished'] = datetime.now().isoformat()
def is_os_supported(self): def is_os_supported(self):
return self.host.os.get('type') in self._TARGET_OS_TYPE return self.host.os.get('type') in self._TARGET_OS_TYPE

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
@ -124,10 +125,12 @@ class MSSQLExploiter(HostExploiter):
conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT) conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT)
LOG.info('Successfully connected to host: {0}, ' LOG.info('Successfully connected to host: {0}, '
'using user: {1}, password: {2}'.format(host, user, password)) 'using user: {1}, password: {2}'.format(host, user, password))
self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT)
self.report_login_attempt(True, user, password) self.report_login_attempt(True, user, password)
cursor = conn.cursor() cursor = conn.cursor()
return cursor return cursor
except pymssql.OperationalError: except pymssql.OperationalError:
self.report_login_attempt(False, user, password)
# Combo didn't work, hopping to the next one # Combo didn't work, hopping to the next one
pass pass

View File

@ -153,6 +153,7 @@ class SRVSVC_Exploit(object):
class Ms08_067_Exploiter(HostExploiter): class Ms08_067_Exploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows'] _TARGET_OS_TYPE = ['windows']
_EXPLOITED_SERVICE = 'Microsoft Server Service'
_windows_versions = {'Windows Server 2003 3790 Service Pack 2': WindowsVersion.Windows2003_SP2, _windows_versions = {'Windows Server 2003 3790 Service Pack 2': WindowsVersion.Windows2003_SP2,
'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2} 'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2}

View File

@ -285,7 +285,9 @@ class InfectionMonkey(object):
result = False result = False
try: try:
exploiter.set_start_time()
result = exploiter.exploit_host() result = exploiter.exploit_host()
exploiter.set_finish_time()
if result: if result:
self.successfully_exploited(machine, exploiter) self.successfully_exploited(machine, exploiter)
return True return True

View File

@ -16,4 +16,6 @@ WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add,
class BackdoorUser(PBA): class BackdoorUser(PBA):
def __init__(self): def __init__(self):
super(BackdoorUser, self).__init__("Backdoor user", linux_cmd=LINUX_COMMANDS, windows_cmd=WINDOWS_COMMANDS) super(BackdoorUser, self).__init__("Backdoor user",
linux_cmd=' '.join(LINUX_COMMANDS),
windows_cmd=WINDOWS_COMMANDS)

View File

@ -1,8 +1,9 @@
import os import os
import sys
import shutil import shutil
import struct import struct
import datetime import sys
import tempfile
from infection_monkey.config import WormConfiguration from infection_monkey.config import WormConfiguration
@ -17,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
@ -54,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

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

@ -119,6 +119,8 @@ class Telemetry(flask_restful.Resource):
def process_exploit_telemetry(telemetry_json): def process_exploit_telemetry(telemetry_json):
edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
Telemetry.encrypt_exploit_creds(telemetry_json) Telemetry.encrypt_exploit_creds(telemetry_json)
telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started'])
telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished'])
new_exploit = copy.deepcopy(telemetry_json['data']) new_exploit = copy.deepcopy(telemetry_json['data'])

View File

@ -1,5 +1,5 @@
import logging import logging
from monkey_island.cc.services.attack.technique_reports import T1210, T1197 from monkey_island.cc.services.attack.technique_reports import T1210, T1197, T1110
from monkey_island.cc.services.attack.attack_config import AttackConfig from monkey_island.cc.services.attack.attack_config import AttackConfig
from monkey_island.cc.database import mongo from monkey_island.cc.database import mongo
@ -9,7 +9,8 @@ __author__ = "VakarisZ"
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
TECHNIQUES = {'T1210': T1210.T1210, TECHNIQUES = {'T1210': T1210.T1210,
'T1197': T1197.T1197} 'T1197': T1197.T1197,
'T1110': T1110.T1110}
REPORT_NAME = 'new_report' REPORT_NAME = 'new_report'

View File

@ -0,0 +1,93 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.encryptor import encryptor
__author__ = "VakarisZ"
class T1110(AttackTechnique):
tech_id = "T1110"
unscanned_msg = "Monkey didn't try to brute force any services."
scanned_msg = "Monkey tried to brute force some services, but failed."
used_msg = "Monkey successfully used brute force in the network."
# Gets data about brute force attempts
query = [{'$match': {'telem_type': 'exploit',
'data.attempts': {'$not': {'$size': 0}}}},
{'$project': {'_id': 0,
'machine': '$data.machine',
'info': '$data.info',
'attempt_cnt': {'$size': '$data.attempts'},
'attempts': {'$filter': {'input': '$data.attempts',
'as': 'attempt',
'cond': {'$eq': ['$$attempt.result', True]}}}}}]
@staticmethod
def get_report_data():
attempts = list(mongo.db.telemetry.aggregate(T1110.query))
succeeded = False
for result in attempts:
result['successful_creds'] = []
for attempt in result['attempts']:
succeeded = True
result['successful_creds'].append(T1110.parse_creds(attempt))
if succeeded:
data = T1110.get_message_and_status(T1110, ScanStatus.USED)
elif attempts:
data = T1110.get_message_and_status(T1110, ScanStatus.SCANNED)
else:
data = T1110.get_message_and_status(T1110, ScanStatus.UNSCANNED)
# 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(T1110.tech_id)})
return data
@staticmethod
def parse_creds(attempt):
"""
Parses used credentials into a string
:param attempt: login attempt from database
:return: string with username and used password/hash
"""
username = attempt['user']
creds = {'lm_hash': {'type': 'LM hash', 'output': T1110.censor_hash(attempt['lm_hash'])},
'ntlm_hash': {'type': 'NTLM hash', 'output': T1110.censor_hash(attempt['ntlm_hash'], 20)},
'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']},
'password': {'type': 'Plaintext password', 'output': T1110.censor_password(attempt['password'])}}
for key, cred in creds.items():
if attempt[key]:
return '%s ; %s : %s' % (username,
cred['type'],
cred['output'])
@staticmethod
def censor_password(password, plain_chars=3, secret_chars=5):
"""
Decrypts and obfuscates password by changing characters to *
:param password: Password or string to obfuscate
:param plain_chars: How many plain-text characters should be kept at the start of the string
:param secret_chars: How many * symbols should be used to hide the remainder of the password
:return: Obfuscated string e.g. Pass****
"""
if not password:
return ""
password = encryptor.dec(password)
return password[0:plain_chars] + '*' * secret_chars
@staticmethod
def censor_hash(hash_, plain_chars=5):
"""
Decrypts and obfuscates hash by only showing a part of it
:param hash_: Hash to obfuscate
:param plain_chars: How many chars of hash should be shown
:return: Obfuscated string
"""
if not hash_:
return ""
hash_ = encryptor.dec(hash_)
return hash_[0: plain_chars] + ' ...'

View File

@ -4,13 +4,6 @@ from monkey_island.cc.database import mongo
__author__ = "VakarisZ" __author__ = "VakarisZ"
TECHNIQUE = "T1210"
MESSAGES = {
'unscanned': "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?",
'scanned': "Monkey scanned for remote services on the network, but couldn't exploit any of them.",
'used': "Monkey scanned for remote services and exploited some on the network."
}
class T1210(AttackTechnique): class T1210(AttackTechnique):
@ -21,7 +14,7 @@ class T1210(AttackTechnique):
@staticmethod @staticmethod
def get_report_data(): def get_report_data():
data = {'title': T1210.technique_title(TECHNIQUE)} data = {'title': T1210.technique_title(T1210.tech_id)}
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:

View File

@ -60,6 +60,19 @@ class AttackTechnique(object):
else: else:
return ScanStatus.UNSCANNED return ScanStatus.UNSCANNED
@staticmethod
def get_message_and_status(technique, status):
return {'message': technique.get_message_by_status(technique, status), 'status': status.name}
@staticmethod
def get_message_by_status(technique, status):
if status == ScanStatus.UNSCANNED:
return technique.unscanned_msg
elif status == ScanStatus.SCANNED:
return technique.scanned_msg
else:
return technique.used_msg
@staticmethod @staticmethod
def technique_title(technique): def technique_title(technique):
""" """
@ -78,11 +91,7 @@ class AttackTechnique(object):
data = {} data = {}
status = AttackTechnique.technique_status(technique.tech_id) status = AttackTechnique.technique_status(technique.tech_id)
title = AttackTechnique.technique_title(technique.tech_id) title = AttackTechnique.technique_title(technique.tech_id)
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': technique.get_message_by_status(technique, 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

@ -57,8 +57,8 @@ class ReportService:
STRUTS2 = 8 STRUTS2 = 8
WEBLOGIC = 9 WEBLOGIC = 9
HADOOP = 10 HADOOP = 10
PTH_CRIT_SERVICES_ACCESS = 11, PTH_CRIT_SERVICES_ACCESS = 11
MSSQL = 12, MSSQL = 12
VSFTPD = 13 VSFTPD = 13
class WARNINGS_DICT(Enum): class WARNINGS_DICT(Enum):

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

@ -0,0 +1,52 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
class T1110 extends React.Component {
constructor(props) {
super(props);
}
static getServiceColumns() {
return ([{
columns: [
{Header: 'Machine', id: 'machine', accessor: x => this.renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 160},
{Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100},
{Header: 'Started', id: 'started', accessor: x => x.info.started, style: { 'whiteSpace': 'unset' }},
{Header: 'Finished', id: 'finished', accessor: x => x.info.finished, style: { 'whiteSpace': 'unset' }},
{Header: 'Attempts', id: 'attempts', accessor: x => x.attempt_cnt, style: { 'whiteSpace': 'unset' }, width: 160},
{Header: 'Successful credentials', id: 'credentials', accessor: x => this.renderCreds(x.successful_creds), style: { 'whiteSpace': 'unset' }},
]
}])};
static renderCreds(creds) {
return <span>{creds.map(cred => <div>{cred}</div>)}</span>
};
static renderMachine(val){
return (
<span>{val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}</span>
)
};
render() {
return (
<div>
<div>{this.props.data.message}</div>
<br/>
{(this.props.data.status === 'SCANNED' || this.props.data.status === 'USED') ?
<ReactTable
columns={T1110.getServiceColumns()}
data={this.props.data.services}
showPagination={false}
defaultPageSize={this.props.data.services.length}
/> : ""}
</div>
);
}
}
export default T1110;

View File

@ -81,7 +81,7 @@ class ReportPageComponent extends AuthComponent {
} }
return ( return (
<Col xs={12} lg={8}> <Col xs={12} lg={10}>
<h1 className="page-title no-print">4. Security Report</h1> <h1 className="page-title no-print">4. Security Report</h1>
<div style={{'fontSize': '1.2em'}}> <div style={{'fontSize': '1.2em'}}>
{content} {content}

View File

@ -6,11 +6,13 @@ import AuthComponent from '../AuthComponent';
import Collapse from '@kunukn/react-collapse'; import Collapse from '@kunukn/react-collapse';
import T1210 from '../attack/techniques/T1210'; import T1210 from '../attack/techniques/T1210';
import T1197 from '../attack/techniques/T1197'; import T1197 from '../attack/techniques/T1197';
import T1110 from '../attack/techniques/T1110';
import '../../styles/Collapse.scss' import '../../styles/Collapse.scss'
const tech_components = { const tech_components = {
'T1210': T1210, 'T1210': T1210,
'T1197': T1197 'T1197': T1197,
'T1110': T1110
}; };
const classNames = require('classnames'); const classNames = require('classnames');

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB