diff --git a/chaos_monkey/exploit/shellshock.py b/chaos_monkey/exploit/shellshock.py index 97c950a18..bca03b6ea 100644 --- a/chaos_monkey/exploit/shellshock.py +++ b/chaos_monkey/exploit/shellshock.py @@ -38,8 +38,10 @@ class ShellShockExploiter(HostExploiter): def exploit_host(self): # start by picking ports - candidate_services = {service: self.host.services[service] for service in self.host.services if - self.host.services[service]['name'] == 'http'} + candidate_services = { + service: self.host.services[service] for service in self.host.services if + ('name' in self.host.services[service]) and (self.host.services[service]['name'] == 'http') + } valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in self.HTTP if 'tcp-' + str(port) in candidate_services] diff --git a/chaos_monkey/main.py b/chaos_monkey/main.py index 231734d56..c53232b2c 100644 --- a/chaos_monkey/main.py +++ b/chaos_monkey/main.py @@ -1,14 +1,17 @@ -import os -import sys -import logging -import traceback -import logging.config -from config import WormConfiguration, EXTERNAL_CONFIG_FILE -from model import MONKEY_ARG, DROPPER_ARG -from dropper import MonkeyDrops -from monkey import ChaosMonkey +from __future__ import print_function + import argparse import json +import logging +import logging.config +import os +import sys +import traceback + +from config import WormConfiguration, EXTERNAL_CONFIG_FILE +from dropper import MonkeyDrops +from model import MONKEY_ARG, DROPPER_ARG +from monkey import ChaosMonkey if __name__ == "__main__": sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -55,22 +58,22 @@ def main(): config_file = opts.config if os.path.isfile(config_file): # using print because config can also change log locations - print "Loading config from %s." % config_file + print("Loading config from %s." % config_file) try: with open(config_file) as config_fo: json_dict = json.load(config_fo) WormConfiguration.from_dict(json_dict) except ValueError as e: - print "Error loading config: %s, using default" % (e,) + print("Error loading config: %s, using default" % (e,)) else: print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,)) - print "Loaded Configuration: %r" % WormConfiguration.as_dict() + print("Loaded Configuration: %r" % WormConfiguration.as_dict()) # Make sure we're not in a machine that has the kill file kill_path = os.path.expandvars(WormConfiguration.kill_file_path_windows) if sys.platform == "win32" else WormConfiguration.kill_file_path_linux if os.path.exists(kill_path): - print "Kill path found, finished run" + print("Kill path found, finished run") return True try: diff --git a/chaos_monkey/system_info/__init__.py b/chaos_monkey/system_info/__init__.py index 0a5bf8e31..126854b8e 100644 --- a/chaos_monkey/system_info/__init__.py +++ b/chaos_monkey/system_info/__init__.py @@ -1,3 +1,4 @@ +import logging import socket import sys @@ -6,6 +7,8 @@ from enum import IntEnum from network.info import get_host_subnets +LOG = logging.getLogger(__name__) + # Linux doesn't have WindowsError try: WindowsError @@ -56,8 +59,9 @@ class InfoCollector(object): def get_hostname(self): """ Adds the fully qualified computer hostname to the system information. - :return: Nothing + :return: None. Updates class information """ + LOG.debug("Reading hostname") self.info['hostname'] = socket.getfqdn() def get_process_list(self): @@ -65,8 +69,9 @@ class InfoCollector(object): Adds process information from the host to the system information. Currently lists process name, ID, parent ID, command line and the full image path of each process. - :return: Nothing + :return: None. Updates class information """ + LOG.debug("Reading process list") processes = {} for process in psutil.process_iter(): try: @@ -95,6 +100,7 @@ class InfoCollector(object): Adds network information from the host to the system information. Currently updates with a list of networks accessible from host, containing host ip and the subnet range. - :return: None + :return: None. Updates class information """ + LOG.debug("Reading subnets") self.info['network_info'] = {'networks': get_host_subnets()} diff --git a/chaos_monkey/system_info/linux_info_collector.py b/chaos_monkey/system_info/linux_info_collector.py index 6c7570fc0..906173421 100644 --- a/chaos_monkey/system_info/linux_info_collector.py +++ b/chaos_monkey/system_info/linux_info_collector.py @@ -1,7 +1,11 @@ +import logging + from . import InfoCollector __author__ = 'uri' +LOG = logging.getLogger(__name__) + class LinuxInfoCollector(InfoCollector): """ @@ -12,6 +16,12 @@ class LinuxInfoCollector(InfoCollector): super(LinuxInfoCollector, self).__init__() def get_info(self): + """ + Collect Linux system information + Hostname, process list and network subnets + :return: Dict of system information + """ + LOG.debug("Running Linux collector") self.get_hostname() self.get_process_list() self.get_network_info() diff --git a/chaos_monkey/system_info/mimikatz_collector.py b/chaos_monkey/system_info/mimikatz_collector.py index 53f42ad4c..e69bcd73e 100644 --- a/chaos_monkey/system_info/mimikatz_collector.py +++ b/chaos_monkey/system_info/mimikatz_collector.py @@ -1,5 +1,5 @@ -import ctypes import binascii +import ctypes import logging import socket @@ -8,13 +8,14 @@ __author__ = 'itay.mizeretz' LOG = logging.getLogger(__name__) -class MimikatzCollector: +class MimikatzCollector(object): """ Password collection module for Windows using Mimikatz. """ def __init__(self): try: + self._isInit = False self._config = __import__('config').WormConfiguration self._dll = ctypes.WinDLL(self._config.mimikatz_dll_name) @@ -31,9 +32,9 @@ class MimikatzCollector: Gets the logon info from mimikatz. Returns a dictionary of users with their known credentials. """ - if not self._isInit: return {} + LOG.debug("Running mimikatz collector") try: entry_count = self._collect() diff --git a/chaos_monkey/system_info/windows_info_collector.py b/chaos_monkey/system_info/windows_info_collector.py index 2ba26fd34..72e189f81 100644 --- a/chaos_monkey/system_info/windows_info_collector.py +++ b/chaos_monkey/system_info/windows_info_collector.py @@ -1,5 +1,10 @@ -from . import InfoCollector +import logging + from mimikatz_collector import MimikatzCollector +from . import InfoCollector + +LOG = logging.getLogger(__name__) + __author__ = 'uri' @@ -12,6 +17,13 @@ class WindowsInfoCollector(InfoCollector): super(WindowsInfoCollector, self).__init__() def get_info(self): + """ + Collect Windows system information + Hostname, process list and network subnets + Tries to read credential secrets using mimikatz + :return: Dict of system information + """ + LOG.debug("Running Windows collector") self.get_hostname() self.get_process_list() self.get_network_info() diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 6cfea1502..9c85f6230 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -15,8 +15,9 @@ from cc.resources.monkey_download import MonkeyDownload from cc.resources.netmap import NetMap from cc.resources.edge import Edge from cc.resources.node import Node - +from cc.resources.report import Report from cc.resources.root import Root +from cc.resources.telemetry_feed import TelemetryFeed from cc.services.config import ConfigService __author__ = 'Barak' @@ -88,5 +89,7 @@ def init_app(mongo_url): api.add_resource(NetMap, '/api/netmap', '/api/netmap/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') + api.add_resource(Report, '/api/report', '/api/report/') + api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') return app diff --git a/monkey_island/cc/island_config.py b/monkey_island/cc/island_config.py index c53d27004..0a8f33bac 100644 --- a/monkey_island/cc/island_config.py +++ b/monkey_island/cc/island_config.py @@ -1,4 +1,5 @@ __author__ = 'itay.mizeretz' ISLAND_PORT = 5000 -DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" \ No newline at end of file +DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland" +DEBUG_SERVER = False diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index 20885f47b..bb1ed9eaf 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -11,7 +11,7 @@ if BASE_PATH not in sys.path: from cc.app import init_app from cc.utils import local_ip_addresses -from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT +from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT, DEBUG_SERVER from cc.database import is_db_server_up if __name__ == '__main__': @@ -26,11 +26,13 @@ if __name__ == '__main__': time.sleep(1) app = init_app(mongo_url) + if DEBUG_SERVER: + app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key')) + else: + http_server = HTTPServer(WSGIContainer(app), + ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'), + 'keyfile': os.environ.get('SERVER_KEY', 'server.key')}) + http_server.listen(ISLAND_PORT) + print('Monkey Island C&C Server is running on https://{}:{}'.format(local_ip_addresses()[0], ISLAND_PORT)) + IOLoop.instance().start() - http_server = HTTPServer(WSGIContainer(app), - ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'), - 'keyfile': os.environ.get('SERVER_KEY', 'server.key')}) - http_server.listen(ISLAND_PORT) - print('Monkey Island C&C Server is running on https://{}:{}'.format(local_ip_addresses()[0], ISLAND_PORT)) - IOLoop.instance().start() - # app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key')) diff --git a/monkey_island/cc/resources/local_run.py b/monkey_island/cc/resources/local_run.py index 2fe7be3d1..3d18b49e6 100644 --- a/monkey_island/cc/resources/local_run.py +++ b/monkey_island/cc/resources/local_run.py @@ -36,7 +36,7 @@ def run_local_monkey(): # run the monkey try: - args = ["%s m0nk3y -s %s:%s" % (target_path, local_ip_addresses()[0], ISLAND_PORT)] + args = ['"%s" m0nk3y -s %s:%s' % (target_path, local_ip_addresses()[0], ISLAND_PORT)] if sys.platform == "win32": args = "".join(args) pid = subprocess.Popen(args, shell=True).pid diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index 2e2da8a5d..37722262c 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -53,6 +53,8 @@ class Monkey(flask_restful.Resource): def post(self, **kw): monkey_json = json.loads(request.data) + monkey_json['creds'] = [] + monkey_json['dead'] = False if 'keepalive' in monkey_json: monkey_json['keepalive'] = dateutil.parser.parse(monkey_json['keepalive']) else: @@ -60,6 +62,8 @@ class Monkey(flask_restful.Resource): monkey_json['modifytime'] = datetime.now() + ConfigService.save_initial_config_if_needed() + # if new monkey telem, change config according to "new monkeys" config. db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]}) if not db_monkey: @@ -119,6 +123,8 @@ class Monkey(flask_restful.Resource): node_id = existing_node["_id"] for edge in mongo.db.edge.find({"to": node_id}): mongo.db.edge.update({"_id": edge["_id"]}, {"$set": {"to": new_monkey_id}}) + for creds in existing_node['creds']: + NodeService.add_credentials_to_monkey(new_monkey_id, creds) mongo.db.node.remove({"_id": node_id}) return {"id": new_monkey_id} diff --git a/monkey_island/cc/resources/report.py b/monkey_island/cc/resources/report.py new file mode 100644 index 000000000..e967b207f --- /dev/null +++ b/monkey_island/cc/resources/report.py @@ -0,0 +1,10 @@ +import flask_restful + +from cc.services.report import ReportService + +__author__ = "itay.mizeretz" + + +class Report(flask_restful.Resource): + def get(self): + return ReportService.get_report() diff --git a/monkey_island/cc/resources/root.py b/monkey_island/cc/resources/root.py index 3f5ee9dd4..25d7dfed7 100644 --- a/monkey_island/cc/resources/root.py +++ b/monkey_island/cc/resources/root.py @@ -1,12 +1,12 @@ from datetime import datetime -from flask import request, make_response, jsonify import flask_restful +from flask import request, make_response, jsonify from cc.database import mongo from cc.services.config import ConfigService from cc.services.node import NodeService - +from cc.services.report import ReportService from cc.utils import local_ip_addresses __author__ = 'Barak' @@ -18,24 +18,35 @@ class Root(flask_restful.Resource): action = request.args.get('action') if not action: - return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db), completed_steps=self.get_completed_steps()) - + return Root.get_server_info() elif action == "reset": - mongo.db.config.drop() - mongo.db.monkey.drop() - mongo.db.telemetry.drop() - mongo.db.node.drop() - mongo.db.edge.drop() - ConfigService.init_config() - return jsonify(status='OK') + return Root.reset_db() elif action == "killall": - mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}}, upsert=False, - multi=True) - return jsonify(status='OK') + return Root.kill_all() else: return make_response(400, {'error': 'unknown action'}) - def get_completed_steps(self): + @staticmethod + def get_server_info(): + return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db), + completed_steps=Root.get_completed_steps()) + + @staticmethod + def reset_db(): + [mongo.db[x].drop() for x in ['config', 'monkey', 'telemetry', 'node', 'edge', 'report']] + ConfigService.init_config() + return jsonify(status='OK') + + @staticmethod + def kill_all(): + mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}}, + upsert=False, + multi=True) + return jsonify(status='OK') + + @staticmethod + def get_completed_steps(): is_any_exists = NodeService.is_any_monkey_exists() - is_any_alive = NodeService.is_any_monkey_alive() - return dict(run_server=True, run_monkey=is_any_exists, infection_done=(is_any_exists and not is_any_alive)) + infection_done = NodeService.is_monkey_finished_running() + report_done = ReportService.is_report_generated() + return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done) diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 88b144333..94c4046b5 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -1,5 +1,6 @@ import json import traceback +import copy from datetime import datetime import dateutil @@ -39,7 +40,6 @@ class Telemetry(flask_restful.Resource): telemetry_json = json.loads(request.data) telemetry_json['timestamp'] = datetime.now() - telem_id = mongo.db.telemetry.insert(telemetry_json) monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) try: @@ -53,6 +53,7 @@ class Telemetry(flask_restful.Resource): print("Exception caught while processing telemetry: %s" % str(ex)) traceback.print_exc() + telem_id = mongo.db.telemetry.insert(telemetry_json) return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) @staticmethod @@ -70,6 +71,11 @@ class Telemetry(flask_restful.Resource): monkey_label = telem_monkey_guid x["monkey"] = monkey_label objects.append(x) + if x['telem_type'] == 'system_info_collection' and 'credentials' in x['data']: + for user in x['data']['credentials']: + if -1 != user.find(','): + new_user = user.replace(',', '.') + x['data']['credentials'][new_user] = x['data']['credentials'].pop(user) return objects @@ -103,7 +109,7 @@ class Telemetry(flask_restful.Resource): @staticmethod def process_exploit_telemetry(telemetry_json): edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) - new_exploit = telemetry_json['data'] + new_exploit = copy.deepcopy(telemetry_json['data']) new_exploit.pop('machine') new_exploit['timestamp'] = telemetry_json['timestamp'] @@ -115,10 +121,18 @@ class Telemetry(flask_restful.Resource): if new_exploit['result']: EdgeService.set_edge_exploited(edge) + for attempt in telemetry_json['data']['attempts']: + if attempt['result']: + found_creds = {'user': attempt['user']} + for field in ['password', 'lm_hash', 'ntlm_hash']: + if len(attempt[field]) != 0: + found_creds[field] = attempt[field] + NodeService.add_credentials_to_node(edge['to'], found_creds) + @staticmethod def process_scan_telemetry(telemetry_json): edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) - data = telemetry_json['data']['machine'] + data = copy.deepcopy(telemetry_json['data']['machine']) ip_address = data.pop("ip_addr") new_scan = \ { @@ -158,11 +172,17 @@ class Telemetry(flask_restful.Resource): if 'ntlm_hash' in creds[user]: ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) + for user in creds: + if -1 != user.find('.'): + new_user = user.replace('.', ',') + creds[new_user] = creds.pop(user) + @staticmethod def process_trace_telemetry(telemetry_json): # Nothing to do return + TELEM_PROCESS_DICT = \ { 'tunnel': Telemetry.process_tunnel_telemetry, diff --git a/monkey_island/cc/resources/telemetry_feed.py b/monkey_island/cc/resources/telemetry_feed.py new file mode 100644 index 000000000..9a7e507ef --- /dev/null +++ b/monkey_island/cc/resources/telemetry_feed.py @@ -0,0 +1,88 @@ +from datetime import datetime + +import dateutil +import flask_restful +from flask import request +import flask_pymongo + +from cc.database import mongo +from cc.services.node import NodeService + +__author__ = 'itay.mizeretz' + + +class TelemetryFeed(flask_restful.Resource): + def get(self, **kw): + timestamp = request.args.get('timestamp') + if "null" == timestamp or timestamp is None: # special case to avoid ugly JS code... + telemetries = mongo.db.telemetry.find({}) + else: + telemetries = mongo.db.telemetry.find({'timestamp': {'$gt': dateutil.parser.parse(timestamp)}})\ + + telemetries = telemetries.sort([('timestamp', flask_pymongo.ASCENDING)]) + + return \ + { + 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries], + 'timestamp': datetime.now().isoformat() + } + + @staticmethod + def get_displayed_telemetry(telem): + return \ + { + 'id': telem['_id'], + 'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'), + 'hostname': NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'], + 'brief': TELEM_PROCESS_DICT[telem['telem_type']](telem) + } + + @staticmethod + def get_tunnel_telem_brief(telem): + tunnel = telem['data']['proxy'] + if tunnel is None: + return 'No tunnel is used.' + else: + tunnel_host_ip = tunnel.split(":")[-2].replace("//", "") + tunnel_host = NodeService.get_monkey_by_ip(tunnel_host_ip)['hostname'] + return 'Tunnel set up to machine: %s.' % tunnel_host + + @staticmethod + def get_state_telem_brief(telem): + if telem['data']['done']: + return 'Monkey died.' + else: + return 'Monkey started.' + + @staticmethod + def get_exploit_telem_brief(telem): + target = telem['data']['machine']['ip_addr'] + exploiter = telem['data']['exploiter'] + result = telem['data']['result'] + if result: + return 'Monkey successfully exploited %s using the %s exploiter.' % (target, exploiter) + else: + return 'Monkey failed exploiting %s using the %s exploiter.' % (target, exploiter) + + @staticmethod + def get_scan_telem_brief(telem): + return 'Monkey discovered machine %s.' % telem['data']['machine']['ip_addr'] + + @staticmethod + def get_systeminfo_telem_brief(telem): + return 'Monkey collected system information.' + + @staticmethod + def get_trace_telem_brief(telem): + return 'Monkey reached max depth.' + + +TELEM_PROCESS_DICT = \ + { + 'tunnel': TelemetryFeed.get_tunnel_telem_brief, + 'state': TelemetryFeed.get_state_telem_brief, + 'exploit': TelemetryFeed.get_exploit_telem_brief, + 'scan': TelemetryFeed.get_scan_telem_brief, + 'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief, + 'trace': TelemetryFeed.get_trace_telem_brief + } diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 200e24029..ea755312f 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -800,15 +800,23 @@ class ConfigService: pass @staticmethod - def get_config(): - config = mongo.db.config.find_one({'name': 'newconfig'}) or {} + def get_config(is_initial_config=False): + config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}) or {} for field in ('name', '_id'): config.pop(field, None) return config @staticmethod - def get_flat_config(): - config_json = ConfigService.get_config() + def get_config_value(config_key_as_arr, is_initial_config=False): + config_key = reduce(lambda x, y: x+'.'+y, config_key_as_arr) + config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}, {config_key: 1}) + for config_key_part in config_key_as_arr: + config = config[config_key_part] + return config + + @staticmethod + def get_flat_config(is_initial_config=False): + config_json = ConfigService.get_config(is_initial_config) flat_config_json = {} for i in config_json: for j in config_json[i]: @@ -880,6 +888,16 @@ class ConfigService: config["cnc"]["servers"]["command_servers"] = ["%s:%d" % (ip, ISLAND_PORT) for ip in ips] config["cnc"]["servers"]["current_server"] = "%s:%d" % (ips[0], ISLAND_PORT) + @staticmethod + def save_initial_config_if_needed(): + if mongo.db.config.find_one({'name': 'initial'}) is not None: + return + + initial_config = mongo.db.config.find_one({'name': 'newconfig'}) + initial_config['name'] = 'initial' + initial_config.pop('_id') + mongo.db.config.insert(initial_config) + @staticmethod def _extend_config_with_default(validator_class): validate_properties = validator_class.VALIDATORS["properties"] diff --git a/monkey_island/cc/services/edge.py b/monkey_island/cc/services/edge.py index 308a57e55..520808be8 100644 --- a/monkey_island/cc/services/edge.py +++ b/monkey_island/cc/services/edge.py @@ -11,22 +11,22 @@ class EdgeService: pass @staticmethod - def get_displayed_edge_by_id(edge_id): + def get_displayed_edge_by_id(edge_id, for_report=False): edge = mongo.db.edge.find({"_id": ObjectId(edge_id)})[0] - return EdgeService.edge_to_displayed_edge(edge) + return EdgeService.edge_to_displayed_edge(edge, for_report) @staticmethod - def get_displayed_edges_by_to(to): + def get_displayed_edges_by_to(to, for_report=False): edges = mongo.db.edge.find({"to": ObjectId(to)}) - return [EdgeService.edge_to_displayed_edge(edge) for edge in edges] + return [EdgeService.edge_to_displayed_edge(edge, for_report) for edge in edges] @staticmethod - def edge_to_displayed_edge(edge): + def edge_to_displayed_edge(edge, for_report=False): services = [] os = {} if len(edge["scans"]) > 0: - services = EdgeService.services_to_displayed_services(edge["scans"][-1]["data"]["services"]) + services = EdgeService.services_to_displayed_services(edge["scans"][-1]["data"]["services"], for_report) os = edge["scans"][-1]["data"]["os"] displayed_edge = EdgeService.edge_to_net_edge(edge) @@ -104,8 +104,11 @@ class EdgeService: return edges @staticmethod - def services_to_displayed_services(services): - return [x + ": " + (services[x]['name'] if services[x].has_key('name') else 'unknown') for x in services] + def services_to_displayed_services(services, for_report=False): + if for_report: + return [x for x in services] + else: + return [x + ": " + (services[x]['name'] if 'name' in services[x] else 'unknown') for x in services] @staticmethod def edge_to_net_edge(edge): diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index 3acd66b75..47cfba8d9 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -12,11 +12,11 @@ class NodeService: pass @staticmethod - def get_displayed_node_by_id(node_id): + def get_displayed_node_by_id(node_id, for_report=False): if ObjectId(node_id) == NodeService.get_monkey_island_pseudo_id(): return NodeService.get_monkey_island_node() - edges = EdgeService.get_displayed_edges_by_to(node_id) + edges = EdgeService.get_displayed_edges_by_to(node_id, for_report) accessible_from_nodes = [] exploits = [] @@ -29,14 +29,14 @@ class NodeService: return new_node # node is infected - new_node = NodeService.monkey_to_net_node(monkey) + new_node = NodeService.monkey_to_net_node(monkey, for_report) for key in monkey: if key not in ['_id', 'modifytime', 'parent', 'dead', 'description']: new_node[key] = monkey[key] else: # node is uninfected - new_node = NodeService.node_to_net_node(node) + new_node = NodeService.node_to_net_node(node, for_report) new_node["ip_addresses"] = node["ip_addresses"] for edge in edges: @@ -89,6 +89,10 @@ class NodeService: return True + @staticmethod + def get_monkey_label_by_id(monkey_id): + return NodeService.get_monkey_label(NodeService.get_monkey_by_id(monkey_id)) + @staticmethod def get_monkey_label(monkey): label = monkey["hostname"] + " : " + monkey["ip_addresses"][0] @@ -115,22 +119,24 @@ class NodeService: return "%s_%s" % (node_type, node_os) @staticmethod - def monkey_to_net_node(monkey): + def monkey_to_net_node(monkey, for_report=False): + label = monkey['hostname'] if for_report else NodeService.get_monkey_label(monkey) return \ { "id": monkey["_id"], - "label": NodeService.get_monkey_label(monkey), + "label": label, "group": NodeService.get_monkey_group(monkey), "os": NodeService.get_monkey_os(monkey), "dead": monkey["dead"], } @staticmethod - def node_to_net_node(node): + def node_to_net_node(node, for_report=False): + label = node['os']['version'] if for_report else NodeService.get_node_label(node) return \ { "id": node["_id"], - "label": NodeService.get_node_label(node), + "label": label, "group": NodeService.get_node_group(node), "os": NodeService.get_node_os(node) } @@ -166,6 +172,7 @@ class NodeService: { "ip_addresses": [ip_address], "exploited": False, + "creds": [], "os": { "type": "unknown", @@ -273,3 +280,39 @@ class NodeService: @staticmethod def is_any_monkey_exists(): return mongo.db.monkey.find_one({}) is not None + + @staticmethod + def is_monkey_finished_running(): + return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive() + + @staticmethod + def add_credentials_to_monkey(monkey_id, creds): + mongo.db.monkey.update( + {'_id': monkey_id}, + {'$push': {'creds': creds}} + ) + + @staticmethod + def add_credentials_to_node(node_id, creds): + mongo.db.node.update( + {'_id': node_id}, + {'$push': {'creds': creds}} + ) + + @staticmethod + def get_node_or_monkey_by_ip(ip_address): + node = NodeService.get_node_by_ip(ip_address) + if node is not None: + return node + return NodeService.get_monkey_by_ip(ip_address) + + @staticmethod + def get_node_or_monkey_by_id(node_id): + node = NodeService.get_node_by_id(node_id) + if node is not None: + return node + return NodeService.get_monkey_by_id(node_id) + + @staticmethod + def get_node_hostname(node): + return node['hostname'] if 'hostname' in node else node['os']['version'] diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py new file mode 100644 index 000000000..c197c55f3 --- /dev/null +++ b/monkey_island/cc/services/report.py @@ -0,0 +1,418 @@ +import ipaddress +from enum import Enum + +from cc.database import mongo +from cc.services.config import ConfigService +from cc.services.edge import EdgeService +from cc.services.node import NodeService +from cc.utils import local_ip_addresses, get_subnets + +__author__ = "itay.mizeretz" + + +class ReportService: + def __init__(self): + pass + + EXPLOIT_DISPLAY_DICT = \ + { + 'SmbExploiter': 'SMB Exploiter', + 'WmiExploiter': 'WMI Exploiter', + 'SSHExploiter': 'SSH Exploiter', + 'RdpExploiter': 'RDP Exploiter', + 'SambaCryExploiter': 'SambaCry Exploiter', + 'ElasticGroovyExploiter': 'Elastic Groovy Exploiter', + 'Ms08_067_Exploiter': 'Conficker Exploiter', + 'ShellShockExploiter': 'ShellShock Exploiter', + } + + class ISSUES_DICT(Enum): + WEAK_PASSWORD = 0 + STOLEN_CREDS = 1 + ELASTIC = 2 + SAMBACRY = 3 + SHELLSHOCK = 4 + CONFICKER = 5 + + class WARNINGS_DICT(Enum): + CROSS_SEGMENT = 0 + TUNNEL = 1 + + @staticmethod + def get_first_monkey_time(): + return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', 1)]).limit(1)[0]['timestamp'] + + @staticmethod + def get_last_monkey_dead_time(): + return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', -1)]).limit(1)[0]['timestamp'] + + @staticmethod + def get_monkey_duration(): + delta = ReportService.get_last_monkey_dead_time() - ReportService.get_first_monkey_time() + st = "" + hours, rem = divmod(delta.seconds, 60 * 60) + minutes, seconds = divmod(rem, 60) + + if delta.days > 0: + st += "%d days, " % delta.days + if hours > 0: + st += "%d hours, " % hours + st += "%d minutes and %d seconds" % (minutes, seconds) + + return st + + @staticmethod + def get_tunnels(): + return [ + { + 'type': 'tunnel', + 'machine': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['_id'])), + 'dest': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['tunnel'])) + } + for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})] + + @staticmethod + def get_scanned(): + nodes = \ + [NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \ + + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in + mongo.db.monkey.find({}, {'_id': 1})] + nodes = [ + { + 'label': node['label'], + 'ip_addresses': node['ip_addresses'], + 'accessible_from_nodes': + (x['hostname'] for x in + (NodeService.get_displayed_node_by_id(edge['from'], True) + for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))), + 'services': node['services'] + } + for node in nodes] + + return nodes + + @staticmethod + def get_exploited(): + exploited = \ + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in + mongo.db.monkey.find({}, {'_id': 1}) + if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))] \ + + [NodeService.get_displayed_node_by_id(node['_id'], True) + for node in mongo.db.node.find({'exploited': True}, {'_id': 1})] + + exploited = [ + { + 'label': monkey['label'], + 'ip_addresses': monkey['ip_addresses'], + 'exploits': list(set( + [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in monkey['exploits'] if + exploit['result']])) + } + for monkey in exploited] + + return exploited + + @staticmethod + def get_stolen_creds(): + PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} + creds = [] + for telem in mongo.db.telemetry.find( + {'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}}, + {'data.credentials': 1, 'monkey_guid': 1} + ): + monkey_creds = telem['data']['credentials'] + if len(monkey_creds) == 0: + continue + origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] + for user in monkey_creds: + for pass_type in monkey_creds[user]: + creds.append( + { + 'username': user.replace(',', '.'), + 'type': PASS_TYPE_DICT[pass_type], + 'origin': origin + } + ) + return creds + + @staticmethod + def process_general_exploit(exploit): + ip_addr = exploit['data']['machine']['ip_addr'] + return {'machine': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_ip(ip_addr)), + 'ip_address': ip_addr} + + @staticmethod + def process_general_creds_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + + for attempt in exploit['data']['attempts']: + if attempt['result']: + processed_exploit['username'] = attempt['user'] + if len(attempt['password']) > 0: + processed_exploit['type'] = 'password' + processed_exploit['password'] = attempt['password'] + else: + processed_exploit['type'] = 'hash' + return processed_exploit + + @staticmethod + def process_smb_exploit(exploit): + processed_exploit = ReportService.process_general_creds_exploit(exploit) + if processed_exploit['type'] == 'password': + processed_exploit['type'] = 'smb_password' + else: + processed_exploit['type'] = 'smb_pth' + return processed_exploit + + @staticmethod + def process_wmi_exploit(exploit): + processed_exploit = ReportService.process_general_creds_exploit(exploit) + if processed_exploit['type'] == 'password': + processed_exploit['type'] = 'wmi_password' + else: + processed_exploit['type'] = 'wmi_pth' + return processed_exploit + + @staticmethod + def process_ssh_exploit(exploit): + processed_exploit = ReportService.process_general_creds_exploit(exploit) + processed_exploit['type'] = 'ssh' + return processed_exploit + + @staticmethod + def process_rdp_exploit(exploit): + processed_exploit = ReportService.process_general_creds_exploit(exploit) + processed_exploit['type'] = 'rdp' + return processed_exploit + + @staticmethod + def process_sambacry_exploit(exploit): + processed_exploit = ReportService.process_general_creds_exploit(exploit) + processed_exploit['type'] = 'sambacry' + return processed_exploit + + @staticmethod + def process_elastic_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'elastic' + return processed_exploit + + @staticmethod + def process_conficker_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'conficker' + return processed_exploit + + @staticmethod + def process_shellshock_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'shellshock' + urls = exploit['data']['info']['vulnerable_urls'] + processed_exploit['port'] = urls[0].split(':')[2].split('/')[0] + processed_exploit['paths'] = ['/' + url.split(':')[2].split('/')[1] for url in urls] + return processed_exploit + + @staticmethod + def process_exploit(exploit): + exploiter_type = exploit['data']['exploiter'] + EXPLOIT_PROCESS_FUNCTION_DICT = { + 'SmbExploiter': ReportService.process_smb_exploit, + 'WmiExploiter': ReportService.process_wmi_exploit, + 'SSHExploiter': ReportService.process_ssh_exploit, + 'RdpExploiter': ReportService.process_rdp_exploit, + 'SambaCryExploiter': ReportService.process_sambacry_exploit, + 'ElasticGroovyExploiter': ReportService.process_elastic_exploit, + 'Ms08_067_Exploiter': ReportService.process_conficker_exploit, + 'ShellShockExploiter': ReportService.process_shellshock_exploit, + } + + return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) + + @staticmethod + def get_exploits(): + exploits = [] + for exploit in mongo.db.telemetry.find({'telem_type': 'exploit', 'data.result': True}): + new_exploit = ReportService.process_exploit(exploit) + if new_exploit not in exploits: + exploits.append(new_exploit) + return exploits + + @staticmethod + def get_monkey_subnets(monkey_guid): + network_info = mongo.db.telemetry.find_one( + {'telem_type': 'system_info_collection', 'monkey_guid': monkey_guid}, + {'data.network_info.networks': 1} + ) + if network_info is None: + return [] + + return \ + [ + ipaddress.ip_interface(unicode(network['addr'] + '/' + network['netmask'])).network + for network in network_info['data']['network_info']['networks'] + ] + + @staticmethod + def get_cross_segment_issues(): + issues = [] + island_ips = local_ip_addresses() + for monkey in mongo.db.monkey.find({'tunnel': {'$exists': False}}, {'tunnel': 1, 'guid': 1, 'hostname': 1}): + found_good_ip = False + monkey_subnets = ReportService.get_monkey_subnets(monkey['guid']) + for subnet in monkey_subnets: + for ip in island_ips: + if ipaddress.ip_address(unicode(ip)) in subnet: + found_good_ip = True + break + if found_good_ip: + break + if not found_good_ip: + issues.append( + {'type': 'cross_segment', 'machine': monkey['hostname'], + 'networks': [str(subnet) for subnet in monkey_subnets], + 'server_networks': [str(subnet) for subnet in get_subnets()]} + ) + + return issues + + @staticmethod + def get_issues(): + issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + issues_dict = {} + for issue in issues: + machine = issue['machine'] + if machine not in issues_dict: + issues_dict[machine] = [] + issues_dict[machine].append(issue) + return issues_dict + + @staticmethod + def get_manual_monkeys(): + return [monkey['hostname'] for monkey in mongo.db.monkey.find({}, {'hostname': 1, 'parent': 1, 'guid': 1}) if + NodeService.get_monkey_manual_run(monkey)] + + @staticmethod + def get_config_users(): + return ConfigService.get_config_value(['basic', 'credentials', 'exploit_user_list'], True) + + @staticmethod + def get_config_passwords(): + return ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list'], True) + + @staticmethod + def get_config_exploits(): + exploits_config_value = ['exploits', 'general', 'exploiter_classes'] + default_exploits = ConfigService.get_default_config() + for namespace in exploits_config_value: + default_exploits = default_exploits[namespace] + exploits = ConfigService.get_config_value(exploits_config_value, True) + + if exploits == default_exploits: + return ['default'] + + return [ReportService.EXPLOIT_DISPLAY_DICT[exploit] for exploit in + exploits] + + @staticmethod + def get_config_ips(): + if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True) != 'FixedRange': + return [] + return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True) + + @staticmethod + def get_config_scan(): + return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True) + + @staticmethod + def get_issues_overview(issues, config_users, config_passwords): + issues_byte_array = [False] * 6 + + for machine in issues: + for issue in issues[machine]: + if issue['type'] == 'elastic': + issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True + elif issue['type'] == 'sambacry': + issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True + elif issue['type'] == 'shellshock': + issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True + elif issue['type'] == 'conficker': + issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True + elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ + issue['username'] in config_users: + issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True + elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'): + issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True + + return issues_byte_array + + @staticmethod + def get_warnings_overview(issues): + warnings_byte_array = [False] * 2 + + for machine in issues: + for issue in issues[machine]: + if issue['type'] == 'cross_segment': + warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True + elif issue['type'] == 'tunnel': + warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True + + return warnings_byte_array + + @staticmethod + def is_report_generated(): + generated_report = mongo.db.report.find_one({'name': 'generated_report'}) + if generated_report is None: + return False + return generated_report['value'] + + @staticmethod + def set_report_generated(): + mongo.db.report.update( + {'name': 'generated_report'}, + {'$set': {'value': True}}, + upsert=True) + + @staticmethod + def get_report(): + issues = ReportService.get_issues() + config_users = ReportService.get_config_users() + config_passwords = ReportService.get_config_passwords() + + report = \ + { + 'overview': + { + 'manual_monkeys': ReportService.get_manual_monkeys(), + 'config_users': config_users, + 'config_passwords': config_passwords, + 'config_exploits': ReportService.get_config_exploits(), + 'config_ips': ReportService.get_config_ips(), + 'config_scan': ReportService.get_config_scan(), + 'monkey_start_time': ReportService.get_first_monkey_time().strftime("%d/%m/%Y %H:%M:%S"), + 'monkey_duration': ReportService.get_monkey_duration(), + 'issues': ReportService.get_issues_overview(issues, config_users, config_passwords), + 'warnings': ReportService.get_warnings_overview(issues) + }, + 'glance': + { + 'scanned': ReportService.get_scanned(), + 'exploited': ReportService.get_exploited(), + 'stolen_creds': ReportService.get_stolen_creds() + }, + 'recommendations': + { + 'issues': issues + } + } + + finished_run = NodeService.is_monkey_finished_running() + if finished_run: + ReportService.set_report_generated() + + return report + + @staticmethod + def did_exploit_type_succeed(exploit_type): + return mongo.db.edge.count( + {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, + limit=1) > 0 diff --git a/monkey_island/cc/ui/package.json b/monkey_island/cc/ui/package.json index 681a98bb3..5ee2e5389 100644 --- a/monkey_island/cc/ui/package.json +++ b/monkey_island/cc/ui/package.json @@ -67,6 +67,7 @@ "js-file-download": "^0.4.1", "normalize.css": "^4.0.0", "prop-types": "^15.5.10", + "rc-progress": "^2.2.5", "react": "^15.6.1", "react-bootstrap": "^0.31.2", "react-copy-to-clipboard": "^5.0.0", @@ -80,6 +81,7 @@ "react-modal-dialog": "^4.0.7", "react-redux": "^5.0.6", "react-router-dom": "^4.2.2", + "react-table": "^6.7.4", "react-toggle": "^4.0.1", "redux": "^3.7.2" } diff --git a/monkey_island/cc/ui/src/components/Main.js b/monkey_island/cc/ui/src/components/Main.js index a4d41f2af..881c3a2ec 100644 --- a/monkey_island/cc/ui/src/components/Main.js +++ b/monkey_island/cc/ui/src/components/Main.js @@ -16,8 +16,10 @@ require('normalize.css/normalize.css'); require('react-data-components/css/table-twbs.css'); require('styles/App.css'); require('react-toggle/style.css'); +require('react-table/react-table.css'); -let logoImage = require('../images/monkey-logo.png'); +let logoImage = require('../images/monkey-icon.svg'); +let infectionMonkeyImage = require('../images/infection-monkey.svg'); let guardicoreLogoImage = require('../images/guardicore-logo.png'); class AppComponent extends React.Component { @@ -27,7 +29,8 @@ class AppComponent extends React.Component { completedSteps: { run_server: true, run_monkey: false, - infection_done: false + infection_done: false, + report_done: false } }; } @@ -66,7 +69,8 @@ class AppComponent extends React.Component {
- Infection Monkey + + Infection Monkey