From 22105eabe3d76096c5b6f65f23dd53e7a4976683 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 15 Oct 2017 20:06:26 +0300 Subject: [PATCH 01/92] Add basic report logic --- monkey_island/cc/app.py | 3 +- monkey_island/cc/resources/report.py | 10 +++++++ monkey_island/cc/services/report.py | 41 ++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 monkey_island/cc/resources/report.py create mode 100644 monkey_island/cc/services/report.py diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 6cfea1502..70001521a 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -15,7 +15,7 @@ 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.services.config import ConfigService @@ -88,5 +88,6 @@ 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/') return app 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/services/report.py b/monkey_island/cc/services/report.py new file mode 100644 index 000000000..4356c84fd --- /dev/null +++ b/monkey_island/cc/services/report.py @@ -0,0 +1,41 @@ +from cc.database import mongo + +__author__ = "itay.mizeretz" + + +class ReportService: + def __init__(self): + pass + + @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_breach_count(): + return mongo.db.edge.count({'exploits.result': True}) + + @staticmethod + def get_successful_exploit_types(): + exploit_types = mongo.db.command({'distinct': 'edge', 'key': 'exploits.exploiter'})['values'] + return [exploit for exploit in exploit_types if ReportService.did_exploit_type_succeed(exploit)] + + @staticmethod + def get_report(): + return \ + { + 'first_monkey_time': ReportService.get_first_monkey_time(), + 'last_monkey_dead_time': ReportService.get_last_monkey_dead_time(), + 'breach_count': ReportService.get_breach_count(), + 'successful_exploit_types': ReportService.get_successful_exploit_types(), + } + + @staticmethod + def did_exploit_type_succeed(exploit_type): + return mongo.db.edge.count( + {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, + limit=1) > 0 From 739edeff2a95defdafed49c3bdd07dd4407d4e77 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 16 Oct 2017 10:40:07 +0300 Subject: [PATCH 02/92] Add option to debug server --- monkey_island/cc/island_config.py | 3 ++- monkey_island/cc/main.py | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) 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 dd133cfd1..68fae4e72 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -9,7 +9,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 if __name__ == '__main__': from tornado.wsgi import WSGIContainer @@ -17,10 +17,14 @@ if __name__ == '__main__': from tornado.ioloop import IOLoop app = init_app(os.environ.get('MONGO_URL', DEFAULT_MONGO_URL)) - 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')) + + 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() + From 04c79d1b3ca3762dfe4b90296a02a591ea704ba2 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 7 Nov 2017 10:30:28 +0200 Subject: [PATCH 03/92] Fix shellshock bug where service name wasn't available --- chaos_monkey/exploit/shellshock.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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] From e9b6b39a2177761756d96f828884aa962a459695 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 7 Nov 2017 13:17:02 +0200 Subject: [PATCH 04/92] Add tunnel info to report --- monkey_island/cc/services/node.py | 4 ++++ monkey_island/cc/services/report.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index 3acd66b75..af92eaa42 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -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] diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 4356c84fd..fd8bc3bdb 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,4 +1,5 @@ from cc.database import mongo +from cc.services.node import NodeService __author__ = "itay.mizeretz" @@ -24,6 +25,12 @@ class ReportService: exploit_types = mongo.db.command({'distinct': 'edge', 'key': 'exploits.exploiter'})['values'] return [exploit for exploit in exploit_types if ReportService.did_exploit_type_succeed(exploit)] + @staticmethod + def get_tunnels(): + return [ + (NodeService.get_monkey_label_by_id(tunnel['_id']), NodeService.get_monkey_label_by_id(tunnel['tunnel'])) + for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})] + @staticmethod def get_report(): return \ @@ -32,6 +39,7 @@ class ReportService: 'last_monkey_dead_time': ReportService.get_last_monkey_dead_time(), 'breach_count': ReportService.get_breach_count(), 'successful_exploit_types': ReportService.get_successful_exploit_types(), + 'tunnels': ReportService.get_tunnels() } @staticmethod From 8d9068fe40025f597c710e892bb69e0c0d994584 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 7 Nov 2017 14:52:13 +0200 Subject: [PATCH 05/92] Add known credentials to monkey documents --- monkey_island/cc/resources/monkey.py | 3 +++ monkey_island/cc/resources/telemetry.py | 11 +++++++++++ monkey_island/cc/services/node.py | 15 +++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index 2e2da8a5d..0c6a8ddf1 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -53,6 +53,7 @@ class Monkey(flask_restful.Resource): def post(self, **kw): monkey_json = json.loads(request.data) + monkey_json['creds'] = {} if 'keepalive' in monkey_json: monkey_json['keepalive'] = dateutil.parser.parse(monkey_json['keepalive']) else: @@ -119,6 +120,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 user in existing_node['creds']: + NodeService.add_credentials_to_monkey(new_monkey_id, user, existing_node['creds'][user]) mongo.db.node.remove({"_id": node_id}) return {"id": new_monkey_id} diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 88b144333..6df6649fa 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -115,6 +115,15 @@ class Telemetry(flask_restful.Resource): if new_exploit['result']: EdgeService.set_edge_exploited(edge) + for attempt in telemetry_json['data']['attempts']: + if attempt['result']: + attempt.pop('result') + user = attempt.pop('user') + for field in ['password', 'lm_hash', 'ntlm_hash']: + if len(attempt[field]) == 0: + attempt.pop(field) + NodeService.add_credentials_to_node(edge['to'], user, attempt) + @staticmethod def process_scan_telemetry(telemetry_json): edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) @@ -151,6 +160,8 @@ class Telemetry(flask_restful.Resource): creds = telemetry_json['data']['credentials'] for user in creds: ConfigService.creds_add_username(user) + NodeService.add_credentials_to_monkey( + NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])['_id'], user, creds[user]) if 'password' in creds[user]: ConfigService.creds_add_password(creds[user]['password']) if 'lm_hash' in creds[user]: diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index af92eaa42..5777bcc36 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -170,6 +170,7 @@ class NodeService: { "ip_addresses": [ip_address], "exploited": False, + "creds": {}, "os": { "type": "unknown", @@ -277,3 +278,17 @@ class NodeService: @staticmethod def is_any_monkey_exists(): return mongo.db.monkey.find_one({}) is not None + + @staticmethod + def add_credentials_to_monkey(monkey_id, user, creds): + mongo.db.monkey.update( + {'_id': monkey_id}, + {'$set': {'creds.' + user: creds}} + ) + + @staticmethod + def add_credentials_to_node(node_id, user, creds): + mongo.db.node.update( + {'_id': node_id}, + {'$set': {'creds.' + user: creds}} + ) From 1ad37b1dade83b622ddeb862ee8f9a2fdc6692ff Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 7 Nov 2017 14:54:11 +0200 Subject: [PATCH 06/92] Fix bug where 'dead' property of monkey wasn't defined --- monkey_island/cc/resources/monkey.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index 2e2da8a5d..c87adc245 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -53,6 +53,7 @@ class Monkey(flask_restful.Resource): def post(self, **kw): monkey_json = json.loads(request.data) + monkey_json['dead'] = False if 'keepalive' in monkey_json: monkey_json['keepalive'] = dateutil.parser.parse(monkey_json['keepalive']) else: From b284467fbcadeeec4bfe3d50936d98cd6ed42ce7 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 7 Nov 2017 16:33:26 +0200 Subject: [PATCH 07/92] Add scanned and exploited to report --- monkey_island/cc/services/report.py | 38 ++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index fd8bc3bdb..ca7a55234 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -31,6 +31,40 @@ class ReportService: (NodeService.get_monkey_label_by_id(tunnel['_id']), NodeService.get_monkey_label_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']) for node in mongo.db.node.find({}, {'_id': 1})]\ + + [NodeService.get_displayed_node_by_id(monkey['_id']) for monkey in mongo.db.monkey.find({}, {'_id': 1})] + nodes = [ + { + 'label': node['label'], + 'ip_addresses': node['ip_addresses'], + 'accessible_from_nodes': node['accessible_from_nodes'], + 'services': node['services'] + } + for node in nodes] + + return nodes + + @staticmethod + def get_exploited(): + exploited =\ + [NodeService.get_displayed_node_by_id(monkey['_id']) 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']) + for node in mongo.db.node.find({'exploited': True}, {'_id': 1})] + + exploited = [ + { + 'label': monkey['label'], + 'ip_addresses': monkey['ip_addresses'], + 'exploits': [exploit['exploiter'] for exploit in monkey['exploits'] if exploit['result']] + } + for monkey in exploited] + + return exploited + @staticmethod def get_report(): return \ @@ -39,7 +73,9 @@ class ReportService: 'last_monkey_dead_time': ReportService.get_last_monkey_dead_time(), 'breach_count': ReportService.get_breach_count(), 'successful_exploit_types': ReportService.get_successful_exploit_types(), - 'tunnels': ReportService.get_tunnels() + 'tunnels': ReportService.get_tunnels(), + 'scanned': ReportService.get_scanned(), + 'exploited': ReportService.get_exploited() } @staticmethod From be8d20b2f5559f3bf7cb12ba8b02f664d2cfdd0a Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 7 Nov 2017 17:02:45 +0200 Subject: [PATCH 08/92] Change creds format in monkey document --- monkey_island/cc/resources/monkey.py | 6 +++--- monkey_island/cc/resources/telemetry.py | 6 +++--- monkey_island/cc/services/node.py | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index 0c6a8ddf1..93fb0a0be 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -53,7 +53,7 @@ class Monkey(flask_restful.Resource): def post(self, **kw): monkey_json = json.loads(request.data) - monkey_json['creds'] = {} + monkey_json['creds'] = [] if 'keepalive' in monkey_json: monkey_json['keepalive'] = dateutil.parser.parse(monkey_json['keepalive']) else: @@ -120,8 +120,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 user in existing_node['creds']: - NodeService.add_credentials_to_monkey(new_monkey_id, user, existing_node['creds'][user]) + 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/telemetry.py b/monkey_island/cc/resources/telemetry.py index 6df6649fa..8698cd45f 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -118,11 +118,10 @@ class Telemetry(flask_restful.Resource): for attempt in telemetry_json['data']['attempts']: if attempt['result']: attempt.pop('result') - user = attempt.pop('user') for field in ['password', 'lm_hash', 'ntlm_hash']: if len(attempt[field]) == 0: attempt.pop(field) - NodeService.add_credentials_to_node(edge['to'], user, attempt) + NodeService.add_credentials_to_node(edge['to'], attempt) @staticmethod def process_scan_telemetry(telemetry_json): @@ -160,8 +159,9 @@ class Telemetry(flask_restful.Resource): creds = telemetry_json['data']['credentials'] for user in creds: ConfigService.creds_add_username(user) + creds[user]['user'] = user NodeService.add_credentials_to_monkey( - NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])['_id'], user, creds[user]) + NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])['_id'], creds[user]) if 'password' in creds[user]: ConfigService.creds_add_password(creds[user]['password']) if 'lm_hash' in creds[user]: diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index 5777bcc36..dc30d60d5 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -170,7 +170,7 @@ class NodeService: { "ip_addresses": [ip_address], "exploited": False, - "creds": {}, + "creds": [], "os": { "type": "unknown", @@ -280,15 +280,15 @@ class NodeService: return mongo.db.monkey.find_one({}) is not None @staticmethod - def add_credentials_to_monkey(monkey_id, user, creds): + def add_credentials_to_monkey(monkey_id, creds): mongo.db.monkey.update( {'_id': monkey_id}, - {'$set': {'creds.' + user: creds}} + {'$push': {'creds': creds}} ) @staticmethod - def add_credentials_to_node(node_id, user, creds): + def add_credentials_to_node(node_id, creds): mongo.db.node.update( {'_id': node_id}, - {'$set': {'creds.' + user: creds}} + {'$push': {'creds': creds}} ) From 0bc666824288e4cdfcaa859f54df479268e3fb75 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sat, 11 Nov 2017 20:32:12 +0200 Subject: [PATCH 09/92] 0001-Rename-to-check tcp-udp port-and-refactor --- chaos_monkey/exploit/rdpgrinder.py | 6 +++--- chaos_monkey/exploit/smbexec.py | 6 +++--- chaos_monkey/exploit/sshexec.py | 4 ++-- chaos_monkey/exploit/win_ms08_067.py | 4 ++-- chaos_monkey/network/sshfinger.py | 7 ++++--- chaos_monkey/network/tcp_scanner.py | 7 ++++--- chaos_monkey/network/tools.py | 20 ++++++++++---------- chaos_monkey/tunnel.py | 4 ++-- 8 files changed, 30 insertions(+), 28 deletions(-) diff --git a/chaos_monkey/exploit/rdpgrinder.py b/chaos_monkey/exploit/rdpgrinder.py index 207564778..606f44f90 100644 --- a/chaos_monkey/exploit/rdpgrinder.py +++ b/chaos_monkey/exploit/rdpgrinder.py @@ -13,7 +13,7 @@ from exploit import HostExploiter from exploit.tools import HTTPTools, get_monkey_depth from exploit.tools import get_target_monkey from model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS -from network.tools import check_port_tcp +from network.tools import check_tcp_port from tools import build_monkey_commandline __author__ = 'hoffer' @@ -245,7 +245,7 @@ class RdpExploiter(HostExploiter): return True if not self.host.os.get('type'): - is_open, _ = check_port_tcp(self.host.ip_addr, RDP_PORT) + is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT) if is_open: self.host.os['type'] = 'windows' return True @@ -254,7 +254,7 @@ class RdpExploiter(HostExploiter): def exploit_host(self): global g_reactor - is_open, _ = check_port_tcp(self.host.ip_addr, RDP_PORT) + is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT) if not is_open: LOG.info("RDP port is closed on %r, skipping", self.host) return False diff --git a/chaos_monkey/exploit/smbexec.py b/chaos_monkey/exploit/smbexec.py index f5fa2b26b..b76a7bce6 100644 --- a/chaos_monkey/exploit/smbexec.py +++ b/chaos_monkey/exploit/smbexec.py @@ -7,7 +7,7 @@ from exploit import HostExploiter from exploit.tools import SmbTools, get_target_monkey, get_monkey_depth from model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS from network import SMBFinger -from network.tools import check_port_tcp +from network.tools import check_tcp_port from tools import build_monkey_commandline LOG = getLogger(__name__) @@ -31,12 +31,12 @@ class SmbExploiter(HostExploiter): return True if not self.host.os.get('type'): - is_smb_open, _ = check_port_tcp(self.host.ip_addr, 445) + is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445) if is_smb_open: smb_finger = SMBFinger() smb_finger.get_host_fingerprint(self.host) else: - is_nb_open, _ = check_port_tcp(self.host.ip_addr, 139) + is_nb_open, _ = check_tcp_port(self.host.ip_addr, 139) if is_nb_open: self.host.os['type'] = 'windows' return self.host.os.get('type') in self._TARGET_OS_TYPE diff --git a/chaos_monkey/exploit/sshexec.py b/chaos_monkey/exploit/sshexec.py index f58e5677b..b93970ca9 100644 --- a/chaos_monkey/exploit/sshexec.py +++ b/chaos_monkey/exploit/sshexec.py @@ -7,7 +7,7 @@ import monkeyfs from exploit import HostExploiter from exploit.tools import get_target_monkey, get_monkey_depth from model import MONKEY_ARG -from network.tools import check_port_tcp +from network.tools import check_tcp_port from tools import build_monkey_commandline __author__ = 'hoffer' @@ -41,7 +41,7 @@ class SSHExploiter(HostExploiter): if servdata.get('name') == 'ssh' and servkey.startswith('tcp-'): port = int(servkey.replace('tcp-', '')) - is_open, _ = check_port_tcp(self.host.ip_addr, port) + is_open, _ = check_tcp_port(self.host.ip_addr, port) if not is_open: LOG.info("SSH port is closed on %r, skipping", self.host) return False diff --git a/chaos_monkey/exploit/win_ms08_067.py b/chaos_monkey/exploit/win_ms08_067.py index 3ed553931..51393ea69 100644 --- a/chaos_monkey/exploit/win_ms08_067.py +++ b/chaos_monkey/exploit/win_ms08_067.py @@ -17,7 +17,7 @@ from impacket.dcerpc.v5 import transport from exploit.tools import SmbTools, get_target_monkey, get_monkey_depth from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from network import SMBFinger -from network.tools import check_port_tcp +from network.tools import check_tcp_port from tools import build_monkey_commandline from . import HostExploiter @@ -168,7 +168,7 @@ class Ms08_067_Exploiter(HostExploiter): if not self.host.os.get('type') or ( self.host.os.get('type') in self._TARGET_OS_TYPE and not self.host.os.get('version')): - is_smb_open, _ = check_port_tcp(self.host.ip_addr, 445) + is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445) if is_smb_open: smb_finger = SMBFinger() if smb_finger.get_host_fingerprint(self.host): diff --git a/chaos_monkey/network/sshfinger.py b/chaos_monkey/network/sshfinger.py index 75a3380ca..89c3092d7 100644 --- a/chaos_monkey/network/sshfinger.py +++ b/chaos_monkey/network/sshfinger.py @@ -1,7 +1,8 @@ import re -from network import HostFinger -from network.tools import check_port_tcp + from model.host import VictimHost +from network import HostFinger +from network.tools import check_tcp_port SSH_PORT = 22 SSH_SERVICE_DEFAULT = 'tcp-22' @@ -38,7 +39,7 @@ class SSHFinger(HostFinger): self._banner_match(name, host, banner) return - is_open, banner = check_port_tcp(host.ip_addr, SSH_PORT, TIMEOUT, True) + is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True) if is_open: host.services[SSH_SERVICE_DEFAULT] = {} diff --git a/chaos_monkey/network/tcp_scanner.py b/chaos_monkey/network/tcp_scanner.py index 8ce715f7f..4a6b8c40e 100644 --- a/chaos_monkey/network/tcp_scanner.py +++ b/chaos_monkey/network/tcp_scanner.py @@ -1,8 +1,9 @@ import time from random import shuffle -from network import HostScanner, HostFinger + from model.host import VictimHost -from network.tools import check_port_tcp +from network import HostScanner, HostFinger +from network.tools import check_tcp_port __author__ = 'itamar' @@ -26,7 +27,7 @@ class TcpScanner(HostScanner, HostFinger): for target_port in target_ports: - is_open, banner = check_port_tcp(host.ip_addr, + is_open, banner = check_tcp_port(host.ip_addr, target_port, self._config.tcp_scan_timeout / 1000.0, self._config.tcp_scan_get_banner) diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py index 66f4eef57..b89002802 100644 --- a/chaos_monkey/network/tools.py +++ b/chaos_monkey/network/tools.py @@ -1,6 +1,6 @@ -import socket -import select import logging +import select +import socket import struct DEFAULT_TIMEOUT = 10 @@ -32,10 +32,10 @@ def struct_unpack_tracker_string(data, index): """ ascii_len = data[index:].find('\0') fmt = "%ds" % ascii_len - return struct_unpack_tracker(data,index,fmt) + return struct_unpack_tracker(data, index, fmt) -def check_port_tcp(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): +def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) @@ -43,7 +43,7 @@ def check_port_tcp(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): sock.connect((ip, port)) except socket.timeout: return False, None - except socket.error, exc: + except socket.error as exc: LOG.debug("Check port: %s:%s, Exception: %s", ip, port, exc) return False, None @@ -56,23 +56,23 @@ def check_port_tcp(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): banner = sock.recv(BANNER_READ) except: pass - + sock.close() return True, banner -def check_port_udp(ip, port, timeout=DEFAULT_TIMEOUT): +def check_udp_port(ip, port, timeout=DEFAULT_TIMEOUT): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(timeout) - + data = None is_open = False - + try: sock.sendto("-", (ip, port)) data, _ = sock.recvfrom(BANNER_READ) is_open = True - except: + except socket.error: pass sock.close() diff --git a/chaos_monkey/tunnel.py b/chaos_monkey/tunnel.py index 7f7edec03..9a50679ff 100644 --- a/chaos_monkey/tunnel.py +++ b/chaos_monkey/tunnel.py @@ -8,7 +8,7 @@ from threading import Thread from model import VictimHost from network.firewall import app as firewall from network.info import local_ips, get_free_tcp_port -from network.tools import check_port_tcp +from network.tools import check_tcp_port from transport.base import get_last_serve_time __author__ = 'hoffer' @@ -40,7 +40,7 @@ def _check_tunnel(address, port, existing_sock=None): sock = existing_sock LOG.debug("Checking tunnel %s:%s", address, port) - is_open, _ = check_port_tcp(address, int(port)) + is_open, _ = check_tcp_port(address, int(port)) if not is_open: LOG.debug("Could not connect to %s:%s", address, port) if not existing_sock: From b9809f1e1f4b484c5d75d345c404eec1baa2e031 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sat, 11 Nov 2017 20:36:56 +0200 Subject: [PATCH 10/92] Move tcp scanner to use new check_tcp_pors --- chaos_monkey/network/tcp_scanner.py | 28 +++---- chaos_monkey/network/tools.py | 118 +++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 18 deletions(-) diff --git a/chaos_monkey/network/tcp_scanner.py b/chaos_monkey/network/tcp_scanner.py index 4a6b8c40e..e149da8e7 100644 --- a/chaos_monkey/network/tcp_scanner.py +++ b/chaos_monkey/network/tcp_scanner.py @@ -1,9 +1,7 @@ -import time from random import shuffle -from model.host import VictimHost from network import HostScanner, HostFinger -from network.tools import check_tcp_port +from network.tools import check_tcp_ports __author__ = 'itamar' @@ -18,29 +16,25 @@ class TcpScanner(HostScanner, HostFinger): return self.get_host_fingerprint(host, True) def get_host_fingerprint(self, host, only_one_port=False): - assert isinstance(host, VictimHost) + """ + Scans a target host to see if it's alive using the tcp_target_ports specified in the configuration. + :param host: VictimHost structure + :param only_one_port: Currently unused. + :return: T/F if there is at least one open port. In addition, the host object is updated to mark those services as alive. + """ - count = 0 # maybe hide under really bad detection systems target_ports = self._config.tcp_target_ports[:] shuffle(target_ports) - for target_port in target_ports: - - is_open, banner = check_tcp_port(host.ip_addr, - target_port, - self._config.tcp_scan_timeout / 1000.0, - self._config.tcp_scan_get_banner) - - if is_open: - count += 1 + ports, banners = check_tcp_ports(host.ip_addr, target_ports, self._config.tcp_scan_timeout / 1000.0) + if len(ports) != 0: + for target_port, banner in zip(ports, banners): service = 'tcp-' + str(target_port) host.services[service] = {} if banner: host.services[service]['banner'] = banner if only_one_port: break - else: - time.sleep(self._config.tcp_scan_interval / 1000.0) - return count != 0 + return len(ports) != 0 diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py index b89002802..6aea850b5 100644 --- a/chaos_monkey/network/tools.py +++ b/chaos_monkey/network/tools.py @@ -2,6 +2,7 @@ import logging import select import socket import struct +import time DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 @@ -36,6 +37,14 @@ def struct_unpack_tracker_string(data, index): def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): + """ + Checks if a given TCP port is open + :param ip: Target IP + :param port: Target Port + :param timeout: Timeout for socket connection + :param get_banner: if true, pulls first BANNER_READ bytes from the socket. + :return: Tuple, T/F + banner if requested. + """ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) @@ -54,7 +63,7 @@ def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): read_ready, _, _ = select.select([sock], [], [], timeout) if len(read_ready) > 0: banner = sock.recv(BANNER_READ) - except: + except socket.error: pass sock.close() @@ -62,6 +71,13 @@ def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): def check_udp_port(ip, port, timeout=DEFAULT_TIMEOUT): + """ + Checks if a given UDP port is open by checking if it replies to an empty message + :param ip: Target IP + :param port: Target port + :param timeout: Timeout to wait + :return: Tuple, T/F + banner + """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(timeout) @@ -77,3 +93,103 @@ def check_udp_port(ip, port, timeout=DEFAULT_TIMEOUT): sock.close() return is_open, data + + +def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): + """ + Checks whether any of the given ports are open on a target IP. + :param ip: IP of host to attack + :param ports: List of ports to attack. Must not be empty. + :param timeout: Amount of time to wait for connection + :param get_banner: T/F if to get first packets from server + :return: list of open ports. If get_banner=True, then a matching list of banners. + """ + sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] + [s.setblocking(0) for s in sockets] + good_ports = [] + try: + for sock, port in zip(sockets, ports): + LOG.debug("Connecting to port %d" % port) + err = sock.connect_ex((ip, port)) + if err == 0: + good_ports.append((port, sock)) + if err == 10035: # WSAEWOULDBLOCK is valid, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + good_ports.append((port, sock)) + + if len(good_ports) != 0: + time.sleep(timeout) + read_sockets, write_sockets, errored_sockets = \ + select.select( + [s[1] for s in good_ports], + [s[1] for s in good_ports], + [s[1] for s in good_ports], + 0) # no timeout because we've already slept + connected_ports_sockets = [x for x in good_ports if x[1] in write_sockets] + LOG.debug( + "On host %s discovered the following ports %s" % + (str(ip), ",".join([str(x) for x in connected_ports_sockets]))) + banners = [] + if get_banner: + # read first X bytes + banners = [sock.recv(BANNER_READ) if sock in read_sockets else "" + for port, sock in connected_ports_sockets] + pass + # try to cleanup + [s[1].close() for s in good_ports] + return [port for port, sock in connected_ports_sockets], banners + else: + return [], [] + + except socket.error as exc: + LOG.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) + return [], [] + + +def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): + """ + Checks whether any of the given ports are open on a target IP. + :param ip: IP of host to attack + :param ports: List of ports to attack. Must not be empty. + :param timeout: Amount of time to wait for connection + :param get_banner: T/F if to get first packets from server + :return: list of open ports. If get_banner=True, then a matching list of banners. + """ + sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] + [s.setblocking(0) for s in sockets] + good_ports = [] + try: + for sock, port in zip(sockets, ports): + LOG.debug("Connecting to port %d" % port) + err = sock.connect_ex((ip, port)) + if err == 0: + good_ports.append((port, sock)) + if err == 10035: # WSAEWOULDBLOCK is valid, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + good_ports.append((port, sock)) + + if len(good_ports) != 0: + time.sleep(timeout) + read_sockets, write_sockets, errored_sockets = \ + select.select( + [s[1] for s in good_ports], + [s[1] for s in good_ports], + [s[1] for s in good_ports], + 0) # no timeout because we've already slept + connected_ports_sockets = [x for x in good_ports if x[1] in write_sockets] + LOG.debug( + "On host %s discovered the following ports %s" % + (str(ip), ",".join([str(x) for x in connected_ports_sockets]))) + banners = [] + if get_banner: + # read first X bytes + banners = [sock.recv(BANNER_READ) if sock in read_sockets else "" + for port, sock in connected_ports_sockets] + pass + # try to cleanup + [s[1].close() for s in good_ports] + return [port for port, sock in connected_ports_sockets], banners + else: + return [], [] + + except socket.error as exc: + LOG.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) + return [], [] From 2b17eca6149449a0e8357bdf89d9bc9589818185 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sat, 11 Nov 2017 20:36:56 +0200 Subject: [PATCH 11/92] Move tcp scanner to use new check_tcp_pors --- chaos_monkey/network/tcp_scanner.py | 28 +++++------- chaos_monkey/network/tools.py | 68 ++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/chaos_monkey/network/tcp_scanner.py b/chaos_monkey/network/tcp_scanner.py index 4a6b8c40e..e149da8e7 100644 --- a/chaos_monkey/network/tcp_scanner.py +++ b/chaos_monkey/network/tcp_scanner.py @@ -1,9 +1,7 @@ -import time from random import shuffle -from model.host import VictimHost from network import HostScanner, HostFinger -from network.tools import check_tcp_port +from network.tools import check_tcp_ports __author__ = 'itamar' @@ -18,29 +16,25 @@ class TcpScanner(HostScanner, HostFinger): return self.get_host_fingerprint(host, True) def get_host_fingerprint(self, host, only_one_port=False): - assert isinstance(host, VictimHost) + """ + Scans a target host to see if it's alive using the tcp_target_ports specified in the configuration. + :param host: VictimHost structure + :param only_one_port: Currently unused. + :return: T/F if there is at least one open port. In addition, the host object is updated to mark those services as alive. + """ - count = 0 # maybe hide under really bad detection systems target_ports = self._config.tcp_target_ports[:] shuffle(target_ports) - for target_port in target_ports: - - is_open, banner = check_tcp_port(host.ip_addr, - target_port, - self._config.tcp_scan_timeout / 1000.0, - self._config.tcp_scan_get_banner) - - if is_open: - count += 1 + ports, banners = check_tcp_ports(host.ip_addr, target_ports, self._config.tcp_scan_timeout / 1000.0) + if len(ports) != 0: + for target_port, banner in zip(ports, banners): service = 'tcp-' + str(target_port) host.services[service] = {} if banner: host.services[service]['banner'] = banner if only_one_port: break - else: - time.sleep(self._config.tcp_scan_interval / 1000.0) - return count != 0 + return len(ports) != 0 diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py index b89002802..ed0c1d4ca 100644 --- a/chaos_monkey/network/tools.py +++ b/chaos_monkey/network/tools.py @@ -2,6 +2,7 @@ import logging import select import socket import struct +import time DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 @@ -36,6 +37,14 @@ def struct_unpack_tracker_string(data, index): def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): + """ + Checks if a given TCP port is open + :param ip: Target IP + :param port: Target Port + :param timeout: Timeout for socket connection + :param get_banner: if true, pulls first BANNER_READ bytes from the socket. + :return: Tuple, T/F + banner if requested. + """ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) @@ -54,7 +63,7 @@ def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): read_ready, _, _ = select.select([sock], [], [], timeout) if len(read_ready) > 0: banner = sock.recv(BANNER_READ) - except: + except socket.error: pass sock.close() @@ -62,6 +71,13 @@ def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False): def check_udp_port(ip, port, timeout=DEFAULT_TIMEOUT): + """ + Checks if a given UDP port is open by checking if it replies to an empty message + :param ip: Target IP + :param port: Target port + :param timeout: Timeout to wait + :return: Tuple, T/F + banner + """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(timeout) @@ -77,3 +93,53 @@ def check_udp_port(ip, port, timeout=DEFAULT_TIMEOUT): sock.close() return is_open, data + + +def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): + """ + Checks whether any of the given ports are open on a target IP. + :param ip: IP of host to attack + :param ports: List of ports to attack. Must not be empty. + :param timeout: Amount of time to wait for connection + :param get_banner: T/F if to get first packets from server + :return: list of open ports. If get_banner=True, then a matching list of banners. + """ + sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] + [s.setblocking(0) for s in sockets] + good_ports = [] + try: + for sock, port in zip(sockets, ports): + LOG.debug("Connecting to port %d" % port) + err = sock.connect_ex((ip, port)) + if err == 0: + good_ports.append((port, sock)) + if err == 10035: # WSAEWOULDBLOCK is valid, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + good_ports.append((port, sock)) + + if len(good_ports) != 0: + time.sleep(timeout) + read_sockets, write_sockets, _ = \ + select.select( + [s[1] for s in good_ports], + [s[1] for s in good_ports], + [s[1] for s in good_ports], + 0) # no timeout because we've already slept + connected_ports_sockets = [x for x in good_ports if x[1] in write_sockets] + LOG.debug( + "On host %s discovered the following ports %s" % + (str(ip), ",".join([str(x) for x in connected_ports_sockets]))) + banners = [] + if get_banner: + # read first X bytes + banners = [sock.recv(BANNER_READ) if sock in read_sockets else "" + for port, sock in connected_ports_sockets] + pass + # try to cleanup + [s[1].close() for s in good_ports] + return [port for port, sock in connected_ports_sockets], banners + else: + return [], [] + + except socket.error as exc: + LOG.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc) + return [], [] From be8feeb3ee124083895b2bb57c4b54711c2ca9b4 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 12 Nov 2017 16:11:12 +0200 Subject: [PATCH 12/92] Add get config value function --- monkey_island/cc/services/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 200e24029..2145011c3 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -806,6 +806,14 @@ class ConfigService: config.pop(field, None) return config + @staticmethod + def get_config_value(config_key_as_arr): + config_key = reduce(lambda x, y: x+'.'+y, config_key_as_arr) + config = mongo.db.config.find_one({'name': '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(): config_json = ConfigService.get_config() From 80b709b2ac8805a8fbc640725f118b6a9d7747d6 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 12 Nov 2017 16:13:40 +0200 Subject: [PATCH 13/92] Add reused passwords --- monkey_island/cc/services/report.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index ca7a55234..0e0a2779f 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,4 +1,5 @@ from cc.database import mongo +from cc.services.config import ConfigService from cc.services.node import NodeService __author__ = "itay.mizeretz" @@ -47,6 +48,17 @@ class ReportService: return nodes + @staticmethod + def get_reused_passwords(): + password_dict = {} + password_list = ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list']) + for password in password_list: + machines_with_password = [NodeService.get_monkey_label_by_id(node['_id']) for node in mongo.db.monkey.find({'creds.password': password}, {'_id': 1})] + if len(machines_with_password) >= 2: + password_dict[password] = machines_with_password + + return password_dict + @staticmethod def get_exploited(): exploited =\ @@ -75,7 +87,8 @@ class ReportService: 'successful_exploit_types': ReportService.get_successful_exploit_types(), 'tunnels': ReportService.get_tunnels(), 'scanned': ReportService.get_scanned(), - 'exploited': ReportService.get_exploited() + 'exploited': ReportService.get_exploited(), + 'reused_passwords': ReportService.get_reused_passwords() } @staticmethod From 545b49919d483cec0f4444dcca8d62eb855ce1b5 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 12 Nov 2017 16:20:15 +0200 Subject: [PATCH 14/92] Remove mimikatz's stolen credentials from machine's list of stolen creds --- monkey_island/cc/resources/telemetry.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 8698cd45f..666bfc16c 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -160,8 +160,6 @@ class Telemetry(flask_restful.Resource): for user in creds: ConfigService.creds_add_username(user) creds[user]['user'] = user - NodeService.add_credentials_to_monkey( - NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])['_id'], creds[user]) if 'password' in creds[user]: ConfigService.creds_add_password(creds[user]['password']) if 'lm_hash' in creds[user]: From 7a523bdd75b942b0449028c767681b045029f343 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 12 Nov 2017 18:06:13 +0200 Subject: [PATCH 15/92] Oppertunistic wait --- chaos_monkey/network/tools.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py index ed0c1d4ca..706dcc48d 100644 --- a/chaos_monkey/network/tools.py +++ b/chaos_monkey/network/tools.py @@ -117,13 +117,21 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): good_ports.append((port, sock)) if len(good_ports) != 0: - time.sleep(timeout) read_sockets, write_sockets, _ = \ select.select( [s[1] for s in good_ports], [s[1] for s in good_ports], [s[1] for s in good_ports], - 0) # no timeout because we've already slept + timeout) # wait max timeout + if len(write_sockets) != len(good_ports): + time.sleep(timeout) + read_sockets, write_sockets, _ = \ + select.select( + [s[1] for s in good_ports], + [s[1] for s in good_ports], + [s[1] for s in good_ports], + 0) # no timeout because we've already slept + connected_ports_sockets = [x for x in good_ports if x[1] in write_sockets] LOG.debug( "On host %s discovered the following ports %s" % From fddda34dcd7800eaf2b18d317408d5d49c2fecc0 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 12 Nov 2017 19:04:54 +0200 Subject: [PATCH 16/92] Worst case is now timeout rather than every case --- chaos_monkey/network/tools.py | 48 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py index 706dcc48d..4ce8fed2e 100644 --- a/chaos_monkey/network/tools.py +++ b/chaos_monkey/network/tools.py @@ -5,6 +5,7 @@ import struct import time DEFAULT_TIMEOUT = 10 +SLEEP_BETWEEN_POLL = 0.5 BANNER_READ = 1024 LOG = logging.getLogger(__name__) @@ -100,39 +101,44 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): Checks whether any of the given ports are open on a target IP. :param ip: IP of host to attack :param ports: List of ports to attack. Must not be empty. - :param timeout: Amount of time to wait for connection + :param timeout: Amount of time to wait for connection. :param get_banner: T/F if to get first packets from server :return: list of open ports. If get_banner=True, then a matching list of banners. """ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))] [s.setblocking(0) for s in sockets] - good_ports = [] + port_attempts = [] try: for sock, port in zip(sockets, ports): LOG.debug("Connecting to port %d" % port) err = sock.connect_ex((ip, port)) if err == 0: - good_ports.append((port, sock)) + port_attempts.append((port, sock)) if err == 10035: # WSAEWOULDBLOCK is valid, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 - good_ports.append((port, sock)) - - if len(good_ports) != 0: - read_sockets, write_sockets, _ = \ - select.select( - [s[1] for s in good_ports], - [s[1] for s in good_ports], - [s[1] for s in good_ports], - timeout) # wait max timeout - if len(write_sockets) != len(good_ports): - time.sleep(timeout) - read_sockets, write_sockets, _ = \ + port_attempts.append((port, sock)) + if len(port_attempts) != 0: + num_replies = 0 + timeout = int(round(timeout)) # clamp to integer, to avoid checking input + time_left = timeout + write_sockets = [] + read_sockets = [] + while num_replies != len(port_attempts): + # bad ports show up as err_sockets + read_sockets, write_sockets, err_sockets = \ select.select( - [s[1] for s in good_ports], - [s[1] for s in good_ports], - [s[1] for s in good_ports], - 0) # no timeout because we've already slept + [s[1] for s in port_attempts], + [s[1] for s in port_attempts], + [s[1] for s in port_attempts], + time_left) + # any read_socket is automatically a writesocket + num_replies = len(write_sockets) + len(err_sockets) + if num_replies == len(port_attempts) or time_left == 0: + break + else: + time_left -= SLEEP_BETWEEN_POLL + time.sleep(SLEEP_BETWEEN_POLL) - connected_ports_sockets = [x for x in good_ports if x[1] in write_sockets] + connected_ports_sockets = [x for x in port_attempts if x[1] in write_sockets] LOG.debug( "On host %s discovered the following ports %s" % (str(ip), ",".join([str(x) for x in connected_ports_sockets]))) @@ -143,7 +149,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): for port, sock in connected_ports_sockets] pass # try to cleanup - [s[1].close() for s in good_ports] + [s[1].close() for s in port_attempts] return [port for port, sock in connected_ports_sockets], banners else: return [], [] From 0f2c58b0aa22f7be8e1579c99728256565792482 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 12 Nov 2017 20:52:01 +0200 Subject: [PATCH 17/92] Add skeleton and more of report --- monkey_island/cc/ui/package.json | 1 + monkey_island/cc/ui/src/components/Main.js | 1 + .../cc/ui/src/components/pages/ReportPage.js | 106 +++++++++++++++++- .../report-components/BreachedServers.js | 40 +++++++ .../report-components/ScannedServers.js | 41 +++++++ monkey_island/cc/ui/src/styles/App.css | 11 +- 6 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 monkey_island/cc/ui/src/components/report-components/BreachedServers.js create mode 100644 monkey_island/cc/ui/src/components/report-components/ScannedServers.js diff --git a/monkey_island/cc/ui/package.json b/monkey_island/cc/ui/package.json index 681a98bb3..71f2decd4 100644 --- a/monkey_island/cc/ui/package.json +++ b/monkey_island/cc/ui/package.json @@ -80,6 +80,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..dd143ea3a 100644 --- a/monkey_island/cc/ui/src/components/Main.js +++ b/monkey_island/cc/ui/src/components/Main.js @@ -16,6 +16,7 @@ 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 guardicoreLogoImage = require('../images/guardicore-logo.png'); diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 633fadc12..c568aa13b 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -1,19 +1,119 @@ import React from 'react'; import {Col} from 'react-bootstrap'; +import BreachedServers from 'components/report-components/BreachedServers'; +import ScannedServers from 'components/report-components/ScannedServers'; + +const list_item = { + label: 'machine 1', + ip_addresses: ['1.2.3.4', '5.6.7.8'], + accessible_from_nodes: ['machine 2', 'machine 3'], + services: ['tcp-80', 'tcp-443'] +}; class ReportPageComponent extends React.Component { constructor(props) { super(props); + + this.state = { + report: {} + }; + } + + componentDidMount() { + fetch('/api/report') + .then(res => res.json()) + .then(res => { + this.setState({ + report: res + }); + }); } render() { + if (Object.keys(this.state.report).length === 0) { + return (
); + } return (

4. Security Report

-

- Under construction -

+
+

+ Overview +

+

+ {/* TODO: Replace 01/02/2017 21:45, 23:12 with data */} + The monkey run was started on 01/02/2017 21:45. After 23:12 minutes, all monkeys finished propagation attempts. +

+

+ From the attacker's point of view, the network looks like this: + {/* TODO: Add map */} +

+
+

* Imagine Map here :) *

+
+
+ {/* TODO: Replace 3 with data */} + During this simulated attack the Monkey uncovered 3 issues, detailed below. The security issues uncovered included: +
    + {/* TODO: Replace lis with data */} +
  • Weak user/passwords combinations
  • +
  • Machines not patched for the ‘Shellshock’ bug
  • +
+
+
+ In addition, the monkey uncovered the following possible set of issues: +
    + {/* TODO: Replace lis with data */} +
  • Machines from another segment accessed the Monkey Island
  • +
  • Network tunnels were created successfully
  • +
+
+

+ A full report of the Monkeys activities follows. +

+
+
+

+ Network Overview +

+

+ {/* TODO: Replace 6,2 with data */} + During the current run, the Monkey discovered 6 machines and successfully breached 2 of them. + In addition, it attempted to exploit the rest, any security software installed in the network should have picked up the attack attempts and logged them. +

+
+ Detailed recommendations in the next part of the report. +

Breached Servers

+ +
+
+

Scanned Servers

+ + {/* TODO: Add table of scanned servers */} +
+
+
+

+ Recommendations +

+
+
+

Issue #1

+

+ The machine Monkey-SMB with the following IP addresses 192.168.0.1 10.0.0.18 was vulnerable to a SmbExploiter attack. + The attack succeeded because weak/stolen password was used over SMB protocol. +

+
+
+

Issue #2

+

+ The network can probably be segmented. A monkey instance on Monkey-SMB in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network. +

+
+
+ {/* TODO: Entire part */} +
); diff --git a/monkey_island/cc/ui/src/components/report-components/BreachedServers.js b/monkey_island/cc/ui/src/components/report-components/BreachedServers.js new file mode 100644 index 000000000..0a7d3ed93 --- /dev/null +++ b/monkey_island/cc/ui/src/components/report-components/BreachedServers.js @@ -0,0 +1,40 @@ +import React from 'react'; +import ReactTable from 'react-table' + +let renderArray = function(val) { + if (val.length === 0) { + return ''; + } + return val.reduce((total, new_str) => total + ', ' + new_str); +}; + +const columns = [ + { Header: 'Machine', accessor: 'label'}, + { Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)}, + { Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)} +]; + +const pageSize = 10; + +class BreachedServersComponent extends React.Component { + constructor(props) { + super(props); + } + + render() { + let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; + let showPagination = this.props.data.length > pageSize; + return ( +
+ +
+ ); + } +} + +export default BreachedServersComponent; diff --git a/monkey_island/cc/ui/src/components/report-components/ScannedServers.js b/monkey_island/cc/ui/src/components/report-components/ScannedServers.js new file mode 100644 index 000000000..9ae1b5135 --- /dev/null +++ b/monkey_island/cc/ui/src/components/report-components/ScannedServers.js @@ -0,0 +1,41 @@ +import React from 'react'; +import ReactTable from 'react-table' + +let renderArray = function(val) { + if (val.length === 0) { + return ''; + } + return val.reduce((total, new_str) => total + ', ' + new_str); +}; + +const columns = [ + { Header: 'Machine', accessor: 'label'}, + { Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)}, + { Header: 'Accessible From', id: 'accessible_from_nodes', accessor: x => renderArray(x.accessible_from_nodes)}, + { Header: 'Services', id: 'services', accessor: x => renderArray(x.services)} +]; + +const pageSize = 10; + +class ScannedServersComponent extends React.Component { + constructor(props) { + super(props); + } + + render() { + let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; + let showPagination = this.props.data.length > pageSize; + return ( +
+ +
+ ); + } +} + +export default ScannedServersComponent; diff --git a/monkey_island/cc/ui/src/styles/App.css b/monkey_island/cc/ui/src/styles/App.css index 9ecf08cbb..fd8fbd22c 100644 --- a/monkey_island/cc/ui/src/styles/App.css +++ b/monkey_island/cc/ui/src/styles/App.css @@ -46,13 +46,22 @@ body { ul { list-style: none; - padding-left: 0; + padding-left: 0px; + } + + ul.report { + list-style: disc; + padding-left: 40px; } li { overflow: auto; } + li.report { + overflow: visible; + } + li .number { color: #666; display: inline-block; From c9e6d890e746e4c62ef7598a4e1d66653c36195a Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 14 Nov 2017 10:59:18 +0200 Subject: [PATCH 18/92] Add map to report --- .../cc/ui/src/components/map/MapOptions.js | 52 ++++++++++++++++ .../{ => map}/preview-pane/PreviewPane.js | 0 .../cc/ui/src/components/pages/MapPage.js | 60 ++----------------- .../cc/ui/src/components/pages/ReportPage.js | 45 +++++++++----- 4 files changed, 87 insertions(+), 70 deletions(-) create mode 100644 monkey_island/cc/ui/src/components/map/MapOptions.js rename monkey_island/cc/ui/src/components/{ => map}/preview-pane/PreviewPane.js (100%) diff --git a/monkey_island/cc/ui/src/components/map/MapOptions.js b/monkey_island/cc/ui/src/components/map/MapOptions.js new file mode 100644 index 000000000..701adcf29 --- /dev/null +++ b/monkey_island/cc/ui/src/components/map/MapOptions.js @@ -0,0 +1,52 @@ +let groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island', + 'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_windows', 'island_monkey_windows_running', + 'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux', + 'monkey_linux_running', 'monkey_windows', 'monkey_windows_running']; + +let getGroupsOptions = () => { + let groupOptions = {}; + for (let groupName of groupNames) { + groupOptions[groupName] = + { + shape: 'image', + size: 50, + image: require('../../images/nodes/' + groupName + '.png') + }; + } + return groupOptions; +}; + +export const options = { + autoResize: true, + layout: { + improvedLayout: false + }, + edges: { + width: 2, + smooth: { + type: 'curvedCW' + } + }, + physics: { + barnesHut: { + gravitationalConstant: -120000, + avoidOverlap: 0.5 + }, + minVelocity: 0.75 + }, + groups: getGroupsOptions() +}; + +export function edgeGroupToColor(group) { + switch (group) { + case 'exploited': + return '#c00'; + case 'tunnel': + return '#0058aa'; + case 'scan': + return '#f90'; + case 'island': + return '#aaa'; + } + return 'black'; +} diff --git a/monkey_island/cc/ui/src/components/preview-pane/PreviewPane.js b/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js similarity index 100% rename from monkey_island/cc/ui/src/components/preview-pane/PreviewPane.js rename to monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js diff --git a/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey_island/cc/ui/src/components/pages/MapPage.js index 81f3baf8a..aa5468380 100644 --- a/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -2,48 +2,10 @@ import React from 'react'; import {Col} from 'react-bootstrap'; import {Link} from 'react-router-dom'; import {Icon} from 'react-fa'; -import PreviewPane from 'components/preview-pane/PreviewPane'; -import {ReactiveGraph} from '../reactive-graph/ReactiveGraph'; +import PreviewPane from 'components/map/preview-pane/PreviewPane'; +import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {ModalContainer, ModalDialog} from 'react-modal-dialog'; - -let groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island', - 'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_windows', 'island_monkey_windows_running', - 'manual_linux', 'manual_linux_running', 'manual_windows', 'manual_windows_running', 'monkey_linux', - 'monkey_linux_running', 'monkey_windows', 'monkey_windows_running']; - -let getGroupsOptions = () => { - let groupOptions = {}; - for (let groupName of groupNames) { - groupOptions[groupName] = - { - shape: 'image', - size: 50, - image: require('../../images/nodes/' + groupName + '.png') - }; - } - return groupOptions; -}; - -let options = { - autoResize: true, - layout: { - improvedLayout: false - }, - edges: { - width: 2, - smooth: { - type: 'curvedCW' - } - }, - physics: { - barnesHut: { - gravitationalConstant: -120000, - avoidOverlap: 0.5 - }, - minVelocity: 0.75 - }, - groups: getGroupsOptions() -}; +import {options, edgeGroupToColor} from 'components/map/MapOptions'; class MapPageComponent extends React.Component { constructor(props) { @@ -61,20 +23,6 @@ class MapPageComponent extends React.Component { select: event => this.selectionChanged(event) }; - static edgeGroupToColor(group) { - switch (group) { - case 'exploited': - return '#c00'; - case 'tunnel': - return '#0058aa'; - case 'scan': - return '#f90'; - case 'island': - return '#aaa'; - } - return 'black'; - } - componentDidMount() { this.updateMapFromServer(); this.interval = setInterval(this.updateMapFromServer, 1000); @@ -89,7 +37,7 @@ class MapPageComponent extends React.Component { .then(res => res.json()) .then(res => { res.edges.forEach(edge => { - edge.color = MapPageComponent.edgeGroupToColor(edge.group); + edge.color = edgeGroupToColor(edge.group); }); this.setState({graph: res}); this.props.onStatusChange(); diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index c568aa13b..81aceeaac 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -2,24 +2,42 @@ import React from 'react'; import {Col} from 'react-bootstrap'; import BreachedServers from 'components/report-components/BreachedServers'; import ScannedServers from 'components/report-components/ScannedServers'; - -const list_item = { - label: 'machine 1', - ip_addresses: ['1.2.3.4', '5.6.7.8'], - accessible_from_nodes: ['machine 2', 'machine 3'], - services: ['tcp-80', 'tcp-443'] -}; +import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; +import {options, edgeGroupToColor} from 'components/map/MapOptions'; class ReportPageComponent extends React.Component { constructor(props) { super(props); this.state = { - report: {} + report: {}, + graph: {nodes: [], edges: []} }; } componentDidMount() { + this.getReportFromServer(); + this.updateMapFromServer(); + this.interval = setInterval(this.updateMapFromServer, 1000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + updateMapFromServer = () => { + fetch('/api/netmap') + .then(res => res.json()) + .then(res => { + res.edges.forEach(edge => { + edge.color = edgeGroupToColor(edge.group); + }); + this.setState({graph: res}); + this.props.onStatusChange(); + }); + }; + + getReportFromServer() { fetch('/api/report') .then(res => res.json()) .then(res => { @@ -31,7 +49,7 @@ class ReportPageComponent extends React.Component { render() { if (Object.keys(this.state.report).length === 0) { - return (
); + return (
); } return ( @@ -47,10 +65,9 @@ class ReportPageComponent extends React.Component {

From the attacker's point of view, the network looks like this: - {/* TODO: Add map */}

-
-

* Imagine Map here :) *

+
+
{/* TODO: Replace 3 with data */} @@ -85,11 +102,11 @@ class ReportPageComponent extends React.Component {
Detailed recommendations in the next part of the report.

Breached Servers

- +

Scanned Servers

- + {/* TODO: Add table of scanned servers */}
From f2e6600d8854a763bae04513601c28b6eabf1f65 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 14 Nov 2017 14:48:36 +0200 Subject: [PATCH 19/92] Add Stolen Passwords section Add example of every security issue (both overview and recommendation sections) Add 'Generating Report' waiting text --- .../cc/ui/src/components/pages/ReportPage.js | 266 +++++++++++++----- .../report-components/StolenPasswords.js | 34 +++ 2 files changed, 222 insertions(+), 78 deletions(-) create mode 100644 monkey_island/cc/ui/src/components/report-components/StolenPasswords.js diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 81aceeaac..b5e808e0e 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -4,11 +4,18 @@ import BreachedServers from 'components/report-components/BreachedServers'; import ScannedServers from 'components/report-components/ScannedServers'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {options, edgeGroupToColor} from 'components/map/MapOptions'; +import StolenPasswords from 'components/report-components/StolenPasswords'; class ReportPageComponent extends React.Component { constructor(props) { super(props); - + this.stolen_passwords = + [ + {username: 'admin', password: 'secretpassword', type: 'password', origin: 'Monkey-SMB'}, + {username: 'user', password: 'my_password', type: 'password', origin: 'Monkey-SMB2'}, + {username: 'dan', password: '066DDFD4EF0E9CD7C256FE77191EF43C', type: 'NTLM', origin: 'Monkey-RDP'}, + {username: 'joe', password: 'FDA95FBECA288D44AAD3B435B51404EE', type: 'LM', origin: 'Monkey-RDP'} + ]; this.state = { report: {}, graph: {nodes: [], edges: []} @@ -48,89 +55,192 @@ class ReportPageComponent extends React.Component { } render() { + let content; + if (Object.keys(this.state.report).length === 0) { - return (
); + content = (

Generating Report...

); + } else { + content = + ( +
+
+

+ Overview +

+

+ {/* TODO: Replace 01/02/2017 21:45, 23:12 with data */} + The monkey run was started on 01/02/2017 21:45. After 23:12 minutes, all monkeys finished propagation attempts. +

+

+ From the attacker's point of view, the network looks like this: +

+
+ +
+
+ {/* TODO: Replace 3 with data */} + During this simulated attack the Monkey uncovered 6 issues, detailed below. The security issues uncovered included: +
    + {/* TODO: Replace lis with data */} +
  • Weak user/passwords combinations.
  • +
  • Stolen passwords/hashes used to exploit other machines.
  • +
  • Elastic Search servers not patched for CVE-2015-1427 bug.
  • +
  • Samba servers not patched for ‘SambaCry’ bug.
  • +
  • Machines not patched for the ‘Shellshock’ bug.
  • +
  • Machines not patched for the ‘Conficker’ bug.
  • +
+
+
+ In addition, the monkey uncovered the following possible set of issues: +
    + {/* TODO: Replace lis with data */} +
  • Machines freely accessed the Monkey Island despite being on different networks.
  • +
  • Machines are not locked down at port level, tunnels between network segments were setup successfully.
  • +
+
+

+ A full report of the Monkeys activities follows. +

+
+
+

+ Network Overview +

+

+ {/* TODO: Replace 6,2 with data */} + During the current run, the Monkey discovered 6 machines and successfully breached 2 of them. + In addition, it attempted to exploit the rest, any security software installed in the network should have picked up the attack attempts and logged them. +

+
+ Detailed recommendations in the next part of the report. +

Breached Servers

+ +
+
+

Scanned Servers

+ + {/* TODO: Add table of scanned servers */} +
+
+
+

+ Stolen Credentials +

+ +
+
+

+ Recommendations +

+
+
+

Issue #1

+

+ The machine Monkey-SMB with the following IP addresses 192.168.0.1 10.0.0.18 was vulnerable to a SMB attack. +
+ The attack succeeded by authenticating over SMB protocol with user Administrator and its password. +

+
+
+

Issue #2

+

+ The machine Monkey-SMB2 with the following IP address 192.168.0.2 was vulnerable to a SMB attack. +
+ The attack succeeded by using a pass-the-hash attack over SMB protocol with user temp. +

+
+
+

Issue #3

+

+ The machine Monkey-WMI with the following IP address 192.168.0.3 was vulnerable to a WMI attack. +
+ The attack succeeded by authenticating over WMI protocol with user Administrator and its password. +

+
+
+

Issue #4

+

+ The machine Monkey-WMI2 with the following IP address 192.168.0.4 was vulnerable to a WMI attack. +
+ The attack succeeded by using a pass-the-hash attack over WMI protocol with user Administrator. +

+
+
+

Issue #5

+

+ The machine Monkey-SSH with the following IP address 192.168.0.5 was vulnerable to a SSH attack. +
+ The attack succeeded by authenticating over SSH protocol with user user and its password. +

+
+
+

Issue #6

+

+ The machine Monkey-RDP with the following IP address 192.168.0.6 was vulnerable to a RDP attack. +
+ The attack succeeded by authenticating over RDP protocol with user Administrator and its password. +

+
+
+

Issue #7

+

+ The machine Monkey-SambaCry with the following IP address 192.168.0.7 was vulnerable to a SambaCry attack. +
+ The attack succeeded by authenticating over SMB protocol with user user and its password, and by using the SambaCry vulnerability. +

+
+
+

Issue #8

+

+ The machine Monkey-Elastic with the following IP address 192.168.0.8 was vulnerable to an Elastic Groovy attack. +
+ The attack succeeded because the Elastic Search server was not parched against the CVE-2015-1427 bug. +

+
+
+

Issue #9

+

+ The machine Monkey-Shellshock with the following IP address 192.168.0.9 was vulnerable to a ShellShock attack. +
+ The attack succeeded because the HTTP server running on port 8080 was vulnerable to a shell injection attack on the paths: /cgi/backserver.cgi /cgi/login.cgi. +

+
+
+

Issue #10

+

+ The machine Monkey-Conficker with the following IP address 192.168.0.10 was vulnerable to a Conficker attack. +
+ The attack succeeded because the target machine uses an outdated and unpatched operating system. +

+
+
+

Issue #11

+

+ The network can probably be segmented. A monkey instance on Monkey-SMB in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network. +

+
+
+

Issue #12

+

+ The network can probably be segmented. A monkey instance on Monkey-SSH in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network. +

+
+
+

Issue #13

+

+ Machines are not locked down at port level. Network tunnel was set up from Monkey-SSH to Monkey-SambaCry. +

+
+
+
+
+ ); } return (

4. Security Report

-
-

- Overview -

-

- {/* TODO: Replace 01/02/2017 21:45, 23:12 with data */} - The monkey run was started on 01/02/2017 21:45. After 23:12 minutes, all monkeys finished propagation attempts. -

-

- From the attacker's point of view, the network looks like this: -

-
- -
-
- {/* TODO: Replace 3 with data */} - During this simulated attack the Monkey uncovered 3 issues, detailed below. The security issues uncovered included: -
    - {/* TODO: Replace lis with data */} -
  • Weak user/passwords combinations
  • -
  • Machines not patched for the ‘Shellshock’ bug
  • -
-
-
- In addition, the monkey uncovered the following possible set of issues: -
    - {/* TODO: Replace lis with data */} -
  • Machines from another segment accessed the Monkey Island
  • -
  • Network tunnels were created successfully
  • -
-
-

- A full report of the Monkeys activities follows. -

-
-
-

- Network Overview -

-

- {/* TODO: Replace 6,2 with data */} - During the current run, the Monkey discovered 6 machines and successfully breached 2 of them. - In addition, it attempted to exploit the rest, any security software installed in the network should have picked up the attack attempts and logged them. -

-
- Detailed recommendations in the next part of the report. -

Breached Servers

- -
-
-

Scanned Servers

- - {/* TODO: Add table of scanned servers */} -
-
-
-

- Recommendations -

-
-
-

Issue #1

-

- The machine Monkey-SMB with the following IP addresses 192.168.0.1 10.0.0.18 was vulnerable to a SmbExploiter attack. - The attack succeeded because weak/stolen password was used over SMB protocol. -

-
-
-

Issue #2

-

- The network can probably be segmented. A monkey instance on Monkey-SMB in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network. -

-
-
- {/* TODO: Entire part */} -
+ {content}
); diff --git a/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js new file mode 100644 index 000000000..c34d51bed --- /dev/null +++ b/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js @@ -0,0 +1,34 @@ +import React from 'react'; +import ReactTable from 'react-table' + +const columns = [ + { Header: 'Username', accessor: 'username'}, + { Header: 'Password/Hash', accessor: 'password'}, + { Header: 'Type', accessor: 'type'}, + { Header: 'Origin', accessor: 'origin'} +]; + +const pageSize = 10; + +class StolenPasswordsComponent extends React.Component { + constructor(props) { + super(props); + } + + render() { + let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; + let showPagination = this.props.data.length > pageSize; + return ( +
+ +
+ ); + } +} + +export default StolenPasswordsComponent; From 13d8d4cfc15244d62fb1a1a5246538f79df42dbd Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 14 Nov 2017 15:49:14 +0200 Subject: [PATCH 20/92] Add scanned-exploited pie chart Merged stolen passwords section Styled tables' header --- monkey_island/cc/ui/package.json | 1 + .../cc/ui/src/components/pages/ReportPage.js | 41 +++++++++------- .../report-components/BreachedServers.js | 11 +++-- .../report-components/ScannedBreachedChart.js | 49 +++++++++++++++++++ .../report-components/ScannedServers.js | 13 +++-- .../report-components/StolenPasswords.js | 13 +++-- 6 files changed, 100 insertions(+), 28 deletions(-) create mode 100644 monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js diff --git a/monkey_island/cc/ui/package.json b/monkey_island/cc/ui/package.json index 71f2decd4..537982386 100644 --- a/monkey_island/cc/ui/package.json +++ b/monkey_island/cc/ui/package.json @@ -80,6 +80,7 @@ "react-modal-dialog": "^4.0.7", "react-redux": "^5.0.6", "react-router-dom": "^4.2.2", + "react-svg-piechart": "^1.4.0", "react-table": "^6.7.4", "react-toggle": "^4.0.1", "redux": "^3.7.2" diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index b5e808e0e..1bf41a2e8 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -5,6 +5,7 @@ import ScannedServers from 'components/report-components/ScannedServers'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {options, edgeGroupToColor} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/StolenPasswords'; +import ScannedBreachedChart from 'components/report-components/ScannedBreachedChart'; class ReportPageComponent extends React.Component { constructor(props) { @@ -102,31 +103,37 @@ class ReportPageComponent extends React.Component { A full report of the Monkeys activities follows.

-
+

- Network Overview + At a Glance

-

- {/* TODO: Replace 6,2 with data */} - During the current run, the Monkey discovered 6 machines and successfully breached 2 of them. - In addition, it attempted to exploit the rest, any security software installed in the network should have picked up the attack attempts and logged them. -

- Detailed recommendations in the next part of the report. -

Breached Servers

+ +

+ {/* TODO: Replace 6,2 with data */} + During the current run, the Monkey discovered 6 machines and successfully breached 2 of them. +
+ In addition, it attempted to exploit the rest, any security software installed in the network should have picked up the attack attempts and logged them. +
+ Detailed recommendations in the next part of the report. +

+ + +
+ +
+ +
+
-
-

Scanned Servers

+
{/* TODO: Add table of scanned servers */}
-
-
-

- Stolen Credentials -

- +
+ +

diff --git a/monkey_island/cc/ui/src/components/report-components/BreachedServers.js b/monkey_island/cc/ui/src/components/report-components/BreachedServers.js index 0a7d3ed93..d8c91f5ca 100644 --- a/monkey_island/cc/ui/src/components/report-components/BreachedServers.js +++ b/monkey_island/cc/ui/src/components/report-components/BreachedServers.js @@ -9,9 +9,14 @@ let renderArray = function(val) { }; const columns = [ - { Header: 'Machine', accessor: 'label'}, - { Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)}, - { Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)} + { + Header: 'Breached Servers', + columns: [ + {Header: 'Machine', accessor: 'label'}, + {Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)}, + {Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)} + ] + } ]; const pageSize = 10; diff --git a/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js b/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js new file mode 100644 index 000000000..fcfd8fcf2 --- /dev/null +++ b/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js @@ -0,0 +1,49 @@ +import React from 'react' +import PieChart from 'react-svg-piechart' + +class ScannedBreachedChartComponent extends React.Component { + constructor() { + super(); + + this.state = { + expandedSector: null, + }; + + this.handleMouseEnterOnSector = this.handleMouseEnterOnSector.bind(this); + } + + handleMouseEnterOnSector(sector) { + this.setState({expandedSector: sector}); + } + + render() { + const data = [ + {label: 'Scanned', value: 4, color: '#f0ad4e'}, + {label: 'Exploited', value: 2, color: '#d9534f'} + ]; + + return ( +
+ +
+ { + data.map((element, i) => ( +
+ + {element.label} : {element.value} + +
+ )) + } +
+
+ ) + } +} + +export default ScannedBreachedChartComponent; diff --git a/monkey_island/cc/ui/src/components/report-components/ScannedServers.js b/monkey_island/cc/ui/src/components/report-components/ScannedServers.js index 9ae1b5135..b598ab537 100644 --- a/monkey_island/cc/ui/src/components/report-components/ScannedServers.js +++ b/monkey_island/cc/ui/src/components/report-components/ScannedServers.js @@ -9,10 +9,15 @@ let renderArray = function(val) { }; const columns = [ - { Header: 'Machine', accessor: 'label'}, - { Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)}, - { Header: 'Accessible From', id: 'accessible_from_nodes', accessor: x => renderArray(x.accessible_from_nodes)}, - { Header: 'Services', id: 'services', accessor: x => renderArray(x.services)} + { + Header: 'Scanned Servers', + columns: [ + { Header: 'Machine', accessor: 'label'}, + { Header: 'IP Addresses', id: 'ip_addresses', accessor: x => renderArray(x.ip_addresses)}, + { Header: 'Accessible From', id: 'accessible_from_nodes', accessor: x => renderArray(x.accessible_from_nodes)}, + { Header: 'Services', id: 'services', accessor: x => renderArray(x.services)} + ] + } ]; const pageSize = 10; diff --git a/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js index c34d51bed..754b51f92 100644 --- a/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js +++ b/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js @@ -2,10 +2,15 @@ import React from 'react'; import ReactTable from 'react-table' const columns = [ - { Header: 'Username', accessor: 'username'}, - { Header: 'Password/Hash', accessor: 'password'}, - { Header: 'Type', accessor: 'type'}, - { Header: 'Origin', accessor: 'origin'} + { + Header: 'Stolen Credentials', + columns: [ + { Header: 'Username', accessor: 'username'}, + { Header: 'Password/Hash', accessor: 'password'}, + { Header: 'Type', accessor: 'type'}, + { Header: 'Origin', accessor: 'origin'} + ] + } ]; const pageSize = 10; From f787801ab7b922157bb7455f66a512e6f6c94c8d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 14 Nov 2017 16:10:22 +0200 Subject: [PATCH 21/92] Add recommendations to security issues --- .../cc/ui/src/components/pages/ReportPage.js | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 1bf41a2e8..6bdb62fbc 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -146,6 +146,11 @@ class ReportPageComponent extends React.Component { The machine Monkey-SMB with the following IP addresses 192.168.0.1 10.0.0.18 was vulnerable to a SMB attack.
The attack succeeded by authenticating over SMB protocol with user Administrator and its password. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Use a complex one-use password that is not shared with other computers on the network.
  • +

@@ -154,6 +159,11 @@ class ReportPageComponent extends React.Component { The machine Monkey-SMB2 with the following IP address 192.168.0.2 was vulnerable to a SMB attack.
The attack succeeded by using a pass-the-hash attack over SMB protocol with user temp. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Use a complex one-use password that is not shared with other computers on the network.
  • +

@@ -162,6 +172,11 @@ class ReportPageComponent extends React.Component { The machine Monkey-WMI with the following IP address 192.168.0.3 was vulnerable to a WMI attack.
The attack succeeded by authenticating over WMI protocol with user Administrator and its password. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Use a complex one-use password that is not shared with other computers on the network.
  • +

@@ -170,6 +185,11 @@ class ReportPageComponent extends React.Component { The machine Monkey-WMI2 with the following IP address 192.168.0.4 was vulnerable to a WMI attack.
The attack succeeded by using a pass-the-hash attack over WMI protocol with user Administrator. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Use a complex one-use password that is not shared with other computers on the network.
  • +

@@ -178,6 +198,11 @@ class ReportPageComponent extends React.Component { The machine Monkey-SSH with the following IP address 192.168.0.5 was vulnerable to a SSH attack.
The attack succeeded by authenticating over SSH protocol with user user and its password. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Use a complex one-use password that is not shared with other computers on the network.
  • +

@@ -186,6 +211,11 @@ class ReportPageComponent extends React.Component { The machine Monkey-RDP with the following IP address 192.168.0.6 was vulnerable to a RDP attack.
The attack succeeded by authenticating over RDP protocol with user Administrator and its password. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Use a complex one-use password that is not shared with other computers on the network.
  • +

@@ -194,6 +224,12 @@ class ReportPageComponent extends React.Component { The machine Monkey-SambaCry with the following IP address 192.168.0.7 was vulnerable to a SambaCry attack.
The attack succeeded by authenticating over SMB protocol with user user and its password, and by using the SambaCry vulnerability. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up.
  • +
  • Use a complex one-use password that is not shared with other computers on the network.
  • +

@@ -202,6 +238,11 @@ class ReportPageComponent extends React.Component { The machine Monkey-Elastic with the following IP address 192.168.0.8 was vulnerable to an Elastic Groovy attack.
The attack succeeded because the Elastic Search server was not parched against the CVE-2015-1427 bug. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Update your Elastic Search server to version 1.4.3 and up.
  • +

@@ -210,6 +251,11 @@ class ReportPageComponent extends React.Component { The machine Monkey-Shellshock with the following IP address 192.168.0.9 was vulnerable to a ShellShock attack.
The attack succeeded because the HTTP server running on port 8080 was vulnerable to a shell injection attack on the paths: /cgi/backserver.cgi /cgi/login.cgi. +
+ In order to protect the machine, the following steps should be performed: +
    +
  • Update your Bash to a ShellShock-patched version.
  • +

@@ -217,25 +263,45 @@ class ReportPageComponent extends React.Component {

The machine Monkey-Conficker with the following IP address 192.168.0.10 was vulnerable to a Conficker attack.
- The attack succeeded because the target machine uses an outdated and unpatched operating system. + The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to Conficker. +
+ In order to protect the machine, the following steps should be performed: +

    +
  • Install the latest Windows updates or upgrade to a newer operating system.
  • +

Issue #11

The network can probably be segmented. A monkey instance on Monkey-SMB in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network. +
+ In order to protect the network, the following steps should be performed: +

    +
  • Segment your network. Make sure machines can't access machines from other segments.
  • +

Issue #12

The network can probably be segmented. A monkey instance on Monkey-SSH in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network. +
+ In order to protect the network, the following steps should be performed: +

    +
  • Segment your network. Make sure machines can't access machines from other segments.
  • +

Issue #13

Machines are not locked down at port level. Network tunnel was set up from Monkey-SSH to Monkey-SambaCry. +
+ In order to protect the machine, the following steps should be performed: +

    +
  • Use micro-segmentation policies to disable communication other than the required.
  • +

From ebeeabee719799f4de538c745bc7c9ae811744d1 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 14 Nov 2017 16:12:50 +0200 Subject: [PATCH 22/92] remove , --- .../ui/src/components/report-components/ScannedBreachedChart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js b/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js index fcfd8fcf2..4e7570e9f 100644 --- a/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js +++ b/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js @@ -6,7 +6,7 @@ class ScannedBreachedChartComponent extends React.Component { super(); this.state = { - expandedSector: null, + expandedSector: null }; this.handleMouseEnterOnSector = this.handleMouseEnterOnSector.bind(this); From 4a96c46f3ed822d5f86e3b6c8b6b1f6092826550 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 21 Nov 2017 11:42:15 +0200 Subject: [PATCH 23/92] Some content and cosmetic changes --- .../cc/ui/src/components/pages/ReportPage.js | 76 +++++++++---------- monkey_island/cc/ui/src/styles/App.css | 6 ++ 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 6bdb62fbc..3853ecacc 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -80,23 +80,23 @@ class ReportPageComponent extends React.Component {
{/* TODO: Replace 3 with data */} - During this simulated attack the Monkey uncovered 6 issues, detailed below. The security issues uncovered included: + During this simulated attack the Monkey uncovered 6 issues, detailed below. The security issues uncovered include:
    {/* TODO: Replace lis with data */} -
  • Weak user/passwords combinations.
  • -
  • Stolen passwords/hashes used to exploit other machines.
  • -
  • Elastic Search servers not patched for CVE-2015-1427 bug.
  • -
  • Samba servers not patched for ‘SambaCry’ bug.
  • -
  • Machines not patched for the ‘Shellshock’ bug.
  • -
  • Machines not patched for the ‘Conficker’ bug.
  • +
  • Users with weak passwords.
  • +
  • Stolen passwords/hashes were used to exploit other machines.
  • +
  • Elastic Search servers not patched for CVE-2015-1427.
  • +
  • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
  • +
  • Machines not patched for the ‘Shellshock’ (CVE-2014-6271).
  • +
  • Machines not patched for the ‘Conficker’ (MS08-067).
In addition, the monkey uncovered the following possible set of issues:
    {/* TODO: Replace lis with data */} -
  • Machines freely accessed the Monkey Island despite being on different networks.
  • -
  • Machines are not locked down at port level, tunnels between network segments were setup successfully.
  • +
  • Possible cross segment traffic. Infected machines could communicate with the Monkey Island despite crossing segment boundaries using unused ports.
  • +
  • Lack of port level segmentation, machines successfully tunneled monkey activity using unused ports.

@@ -111,9 +111,9 @@ class ReportPageComponent extends React.Component {

{/* TODO: Replace 6,2 with data */} - During the current run, the Monkey discovered 6 machines and successfully breached 2 of them. + The Monkey discovered 6 machines and successfully breached 2 of them.
- In addition, it attempted to exploit the rest, any security software installed in the network should have picked up the attack attempts and logged them. + In addition, while attempting to exploit additional hosts , security software installed in the network should have picked up the attack attempts and logged them.
Detailed recommendations in the next part of the report.

@@ -142,7 +142,7 @@ class ReportPageComponent extends React.Component {

Issue #1

-

+

The machine Monkey-SMB with the following IP addresses 192.168.0.1 10.0.0.18 was vulnerable to a SMB attack.
The attack succeeded by authenticating over SMB protocol with user Administrator and its password. @@ -151,11 +151,11 @@ class ReportPageComponent extends React.Component {
  • Use a complex one-use password that is not shared with other computers on the network.
-

+

Issue #2

-

+

The machine Monkey-SMB2 with the following IP address 192.168.0.2 was vulnerable to a SMB attack.
The attack succeeded by using a pass-the-hash attack over SMB protocol with user temp. @@ -164,11 +164,11 @@ class ReportPageComponent extends React.Component {
  • Use a complex one-use password that is not shared with other computers on the network.
-

+

Issue #3

-

+

The machine Monkey-WMI with the following IP address 192.168.0.3 was vulnerable to a WMI attack.
The attack succeeded by authenticating over WMI protocol with user Administrator and its password. @@ -177,11 +177,11 @@ class ReportPageComponent extends React.Component {
  • Use a complex one-use password that is not shared with other computers on the network.
-

+

Issue #4

-

+

The machine Monkey-WMI2 with the following IP address 192.168.0.4 was vulnerable to a WMI attack.
The attack succeeded by using a pass-the-hash attack over WMI protocol with user Administrator. @@ -190,11 +190,11 @@ class ReportPageComponent extends React.Component {
  • Use a complex one-use password that is not shared with other computers on the network.
-

+

Issue #5

-

+

The machine Monkey-SSH with the following IP address 192.168.0.5 was vulnerable to a SSH attack.
The attack succeeded by authenticating over SSH protocol with user user and its password. @@ -203,11 +203,11 @@ class ReportPageComponent extends React.Component {
  • Use a complex one-use password that is not shared with other computers on the network.
-

+

Issue #6

-

+

The machine Monkey-RDP with the following IP address 192.168.0.6 was vulnerable to a RDP attack.
The attack succeeded by authenticating over RDP protocol with user Administrator and its password. @@ -216,11 +216,11 @@ class ReportPageComponent extends React.Component {
  • Use a complex one-use password that is not shared with other computers on the network.
-

+

Issue #7

-

+

The machine Monkey-SambaCry with the following IP address 192.168.0.7 was vulnerable to a SambaCry attack.
The attack succeeded by authenticating over SMB protocol with user user and its password, and by using the SambaCry vulnerability. @@ -230,24 +230,24 @@ class ReportPageComponent extends React.Component {
  • Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up.
  • Use a complex one-use password that is not shared with other computers on the network.
  • -

    +

    Issue #8

    -

    +

    The machine Monkey-Elastic with the following IP address 192.168.0.8 was vulnerable to an Elastic Groovy attack.
    - The attack succeeded because the Elastic Search server was not parched against the CVE-2015-1427 bug. + The attack succeeded because the Elastic Search server was not parched against CVE-2015-1427.
    In order to protect the machine, the following steps should be performed:
    • Update your Elastic Search server to version 1.4.3 and up.
    -

    +

    Issue #9

    -

    +

    The machine Monkey-Shellshock with the following IP address 192.168.0.9 was vulnerable to a ShellShock attack.
    The attack succeeded because the HTTP server running on port 8080 was vulnerable to a shell injection attack on the paths: /cgi/backserver.cgi /cgi/login.cgi. @@ -256,11 +256,11 @@ class ReportPageComponent extends React.Component {
    • Update your Bash to a ShellShock-patched version.
    -

    +

    Issue #10

    -

    +

    The machine Monkey-Conficker with the following IP address 192.168.0.10 was vulnerable to a Conficker attack.
    The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to Conficker. @@ -269,40 +269,40 @@ class ReportPageComponent extends React.Component {
    • Install the latest Windows updates or upgrade to a newer operating system.
    -

    +

    Issue #11

    -

    +

    The network can probably be segmented. A monkey instance on Monkey-SMB in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network.
    In order to protect the network, the following steps should be performed:
    • Segment your network. Make sure machines can't access machines from other segments.
    -

    +

    Issue #12

    -

    +

    The network can probably be segmented. A monkey instance on Monkey-SSH in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network.
    In order to protect the network, the following steps should be performed:
    • Segment your network. Make sure machines can't access machines from other segments.
    -

    +

    Issue #13

    -

    +

    Machines are not locked down at port level. Network tunnel was set up from Monkey-SSH to Monkey-SambaCry.
    In order to protect the machine, the following steps should be performed:
    • Use micro-segmentation policies to disable communication other than the required.
    -

    +
    diff --git a/monkey_island/cc/ui/src/styles/App.css b/monkey_island/cc/ui/src/styles/App.css index fd8fbd22c..30ea8faa4 100644 --- a/monkey_island/cc/ui/src/styles/App.css +++ b/monkey_island/cc/ui/src/styles/App.css @@ -75,6 +75,12 @@ body { padding: 0.5em 1em; margin: 0.1em 0; } + + li a.report { + display: inline; + padding: 0em; + } + li a:hover { color: #000; background: #e9e9e9; From 8632f4d5ca9ad3d1ee11fc717f953d131d7c81b8 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 21 Nov 2017 13:50:29 +0200 Subject: [PATCH 24/92] Change machine name to be hostname when possible, and os['version'] otherwise --- monkey_island/cc/services/report.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 0e0a2779f..959be1ac5 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,5 +1,6 @@ from cc.database import mongo from cc.services.config import ConfigService +from cc.services.edge import EdgeService from cc.services.node import NodeService __author__ = "itay.mizeretz" @@ -39,9 +40,13 @@ class ReportService: + [NodeService.get_displayed_node_by_id(monkey['_id']) for monkey in mongo.db.monkey.find({}, {'_id': 1})] nodes = [ { - 'label': node['label'], + 'label': + node['hostname'] if 'hostname' in node else NodeService.get_node_by_id(node['id'])['os']['version'], 'ip_addresses': node['ip_addresses'], - 'accessible_from_nodes': node['accessible_from_nodes'], + 'accessible_from_nodes': + (x['hostname'] for x in + (NodeService.get_displayed_node_by_id(edge['from']) + for edge in EdgeService.get_displayed_edges_by_to(node['id']))), 'services': node['services'] } for node in nodes] @@ -53,7 +58,11 @@ class ReportService: password_dict = {} password_list = ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list']) for password in password_list: - machines_with_password = [NodeService.get_monkey_label_by_id(node['_id']) for node in mongo.db.monkey.find({'creds.password': password}, {'_id': 1})] + machines_with_password =\ + [ + NodeService.get_monkey_label_by_id(node['_id']) + for node in mongo.db.monkey.find({'creds.password': password}, {'_id': 1}) + ] if len(machines_with_password) >= 2: password_dict[password] = machines_with_password @@ -69,7 +78,7 @@ class ReportService: exploited = [ { - 'label': monkey['label'], + 'label': monkey['hostname'] if 'hostname' in monkey else monkey['os']['version'], 'ip_addresses': monkey['ip_addresses'], 'exploits': [exploit['exploiter'] for exploit in monkey['exploits'] if exploit['result']] } From 83c7c3d13cf484cd0a0ae4317c77cccc6ddc38da Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 21 Nov 2017 16:25:39 +0200 Subject: [PATCH 25/92] Report now uses dynamic data --- .../cc/ui/src/components/pages/ReportPage.js | 494 +++++++++++------- .../report-components/ScannedBreachedChart.js | 4 +- 2 files changed, 307 insertions(+), 191 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 3853ecacc..dee3c8cb2 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -8,6 +8,23 @@ import StolenPasswords from 'components/report-components/StolenPasswords'; import ScannedBreachedChart from 'components/report-components/ScannedBreachedChart'; class ReportPageComponent extends React.Component { + + Issue = + { + WEAK_PASSWORD: 0, + STOLEN_CREDS: 1, + ELASTIC: 2, + SAMBACRY: 3, + SHELLSHOCK: 4, + CONFICKER: 5 + }; + + Warning = + { + CROSS_SEGMENT: 0, + TUNNEL: 1 + }; + constructor(props) { super(props); this.stolen_passwords = @@ -18,13 +35,48 @@ class ReportPageComponent extends React.Component { {username: 'joe', password: 'FDA95FBECA288D44AAD3B435B51404EE', type: 'LM', origin: 'Monkey-RDP'} ]; this.state = { - report: {}, + report: { + overview: + { + monkey_start_time: '01/02/2017 21:45', + monkey_duration: '23:12 minutes', + issues: [false, true, true, true, false, true], + warnings: [true, true] + }, + glance: + { + scanned: + [{"services": ["tcp-22: ssh", "elastic-search-9200: Lorelei Travis"], "ip_addresses": ["11.0.0.13"], "accessible_from_nodes": ["webServer-shellshock0"], "label": "Ubuntu-4ubuntu2.1"}, {"services": [], "ip_addresses": ["10.0.3.23"], "accessible_from_nodes": [], "label": "ubuntu"}, {"services": ["tcp-22: ssh", "tcp-80: http"], "ip_addresses": ["10.0.3.68", "11.0.0.41"], "accessible_from_nodes": ["Monkey-MSSQL1", "ubuntu"], "label": "webServer-shellshock0"}, {"services": ["tcp-445: Windows Server 2012 R2 Standard 6.3"], "ip_addresses": ["12.0.0.90", "11.0.0.90"], "accessible_from_nodes": ["webServer-shellshock0"], "label": "Monkey-MSSQL1"}], + exploited: + [{"ip_addresses": ["10.0.3.68", "11.0.0.41"], "exploits": ["ShellShockExploiter", "ShellShockExploiter"], "label": "webServer-shellshock0"}, {"ip_addresses": ["12.0.0.90", "11.0.0.90"], "exploits": ["SmbExploiter", "SmbExploiter"], "label": "Monkey-MSSQL1"}], + stolen_creds: this.stolen_passwords + }, + recommendations: + { + issues: + [ + {type: 'smb_password', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, + {type: 'smb_pth', machine: 'Monkey-SMB2', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, + {type: 'wmi_password', machine: 'Monkey-WMI', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, + {type: 'wmi_pth', machine: 'Monkey-WMI2', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, + {type: 'ssh', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, + {type: 'rdp', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, + {type: 'sambacry', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, + {type: 'elastic', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18']}, + {type: 'shellshock', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], port: 8080, paths: ['/cgi/backserver.cgi', '/cgi/login.cgi']}, + {type: 'conficker', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18']}, + {type: 'cross_segment', machine: 'Monkey-SMB', network: '192.168.0.0/24', server_network: '172.168.0.0/24'}, + {type: 'tunnel', origin: 'Monkey-SSH', dest: 'Monkey-SambaCry'} + ] + } + }, graph: {nodes: [], edges: []} }; } componentDidMount() { - this.getReportFromServer(); + // TODO: uncomment + //this.getReportFromServer(); this.updateMapFromServer(); this.interval = setInterval(this.updateMapFromServer, 1000); } @@ -55,10 +107,243 @@ class ReportPageComponent extends React.Component { }); } + generateIpListBadges(ip_addresses) { + return ip_addresses.map(ip_address => {ip_address}); + } + + generateShellshockPathListBadges(paths) { + return paths.map(path => {path}); + } + + generateSmbPasswordIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a SMB attack. +
    + The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Use a complex one-use password that is not shared with other computers on the network.
    • +
    +
    + ); + } + + generateSmbPthIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a SMB attack. +
    + The attack succeeded by using a pass-the-hash attack over SMB protocol with user {issue.username}. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Use a complex one-use password that is not shared with other computers on the network.
    • +
    +
    + ); + } + + generateWmiPasswordIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a WMI attack. +
    + The attack succeeded by authenticating over WMI protocol with user {issue.username} and its password. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Use a complex one-use password that is not shared with other computers on the network.
    • +
    +
    + ); + } + + generateWmiPthIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a WMI attack. +
    + The attack succeeded by using a pass-the-hash attack over WMI protocol with user {issue.username}. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Use a complex one-use password that is not shared with other computers on the network.
    • +
    +
    + ); + } + + generateSshIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a SSH attack. +
    + The attack succeeded by authenticating over SSH protocol with user {issue.username} and its password. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Use a complex one-use password that is not shared with other computers on the network.
    • +
    +
    + ); + } + + generateRdpIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a RDP attack. +
    + The attack succeeded by authenticating over RDP protocol with user {issue.username} and its password. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Use a complex one-use password that is not shared with other computers on the network.
    • +
    +
    + ); + } + + generateSambaCryIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a SambaCry attack. +
    + The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password, and by using the SambaCry vulnerability. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up.
    • +
    • Use a complex one-use password that is not shared with other computers on the network.
    • +
    +
    + ); + } + + generateElasticIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to an Elastic Groovy attack. +
    + The attack succeeded because the Elastic Search server was not parched against CVE-2015-1427. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Update your Elastic Search server to version 1.4.3 and up.
    • +
    +
    + ); + } + + generateShellshockIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a ShellShock attack. +
    + The attack succeeded because the HTTP server running on port {issue.port} was vulnerable to a shell injection attack on the paths: {this.generateShellshockPathListBadges(issue.paths)}. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Update your Bash to a ShellShock-patched version.
    • +
    +
    + ); + } + + generateConfickerIssue(issue) { + return ( +
    + The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a Conficker attack. +
    + The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to Conficker. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Install the latest Windows updates or upgrade to a newer operating system.
    • +
    +
    + ); + } + + generateCrossSegmentIssue(issue) { + return ( +
    + The network can probably be segmented. A monkey instance on {issue.machine} in the {issue.network} network could directly access the Monkey Island C&C server in the {issue.server_network} network. +
    + In order to protect the network, the following steps should be performed: +
      +
    • Segment your network. Make sure machines can't access machines from other segments.
    • +
    +
    + ); + } + + generateTunnelIssue(issue) { + return ( +
    + Machines are not locked down at port level. Network tunnel was set up from {issue.origin} to {issue.dest}. +
    + In order to protect the machine, the following steps should be performed: +
      +
    • Use micro-segmentation policies to disable communication other than the required.
    • +
    +
    + ); + } + + generateIssue = (issue, index) => { + let data; + switch (issue.type) { + case 'smb_password': + data = this.generateSmbPasswordIssue(issue); + break; + case 'smb_pth': + data = this.generateSmbPthIssue(issue); + break; + case 'wmi_password': + data = this.generateWmiPasswordIssue(issue); + break; + case 'wmi_pth': + data = this.generateWmiPthIssue(issue); + break; + case 'ssh': + data = this.generateSshIssue(issue); + break; + case 'rdp': + data = this.generateRdpIssue(issue); + break; + case 'sambacry': + data = this.generateSambaCryIssue(issue); + break; + case 'elastic': + data = this.generateElasticIssue(issue); + break; + case 'shellshock': + data = this.generateShellshockIssue(issue); + break; + case 'conficker': + data = this.generateConfickerIssue(issue); + break; + case 'cross_segment': + data = this.generateCrossSegmentIssue(issue); + break; + case 'tunnel': + data = this.generateTunnelIssue(issue); + break; + } + return ( +
    +

    Issue #{index+1}

    + {data} +
    + ); + }; + render() { let content; - - if (Object.keys(this.state.report).length === 0) { + // TODO: remove 0==1 + if (0==1 || Object.keys(this.state.report).length === 0) { content = (

    Generating Report...

    ); } else { content = @@ -69,8 +354,7 @@ class ReportPageComponent extends React.Component { Overview

    - {/* TODO: Replace 01/02/2017 21:45, 23:12 with data */} - The monkey run was started on 01/02/2017 21:45. After 23:12 minutes, all monkeys finished propagation attempts. + The monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished propagation attempts.

    From the attacker's point of view, the network looks like this: @@ -79,24 +363,21 @@ class ReportPageComponent extends React.Component {

    - {/* TODO: Replace 3 with data */} - During this simulated attack the Monkey uncovered 6 issues, detailed below. The security issues uncovered include: + During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function(x){return x===true;}).length}, detailed below. The security issues uncovered include:
      - {/* TODO: Replace lis with data */} -
    • Users with weak passwords.
    • -
    • Stolen passwords/hashes were used to exploit other machines.
    • -
    • Elastic Search servers not patched for CVE-2015-1427.
    • -
    • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
    • -
    • Machines not patched for the ‘Shellshock’ (CVE-2014-6271).
    • -
    • Machines not patched for the ‘Conficker’ (MS08-067).
    • + {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
    • Users with weak passwords.
    • : null} + {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
    • Stolen passwords/hashes were used to exploit other machines.
    • : null} + {this.state.report.overview.issues[this.Issue.ELASTIC] ?
    • Elastic Search servers not patched for CVE-2015-1427.
    • : null} + {this.state.report.overview.issues[this.Issue.SAMBACRY] ?
    • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
    • : null} + {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ?
    • Machines not patched for the ‘Shellshock’ (CVE-2014-6271).
    • : null} + {this.state.report.overview.issues[this.Issue.CONFICKER] ?
    • Machines not patched for the ‘Conficker’ (MS08-067).
    • : null}
    In addition, the monkey uncovered the following possible set of issues:
      - {/* TODO: Replace lis with data */} -
    • Possible cross segment traffic. Infected machines could communicate with the Monkey Island despite crossing segment boundaries using unused ports.
    • -
    • Lack of port level segmentation, machines successfully tunneled monkey activity using unused ports.
    • + {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ?
    • Possible cross segment traffic. Infected machines could communicate with the Monkey Island despite crossing segment boundaries using unused ports.
    • : null} + {this.state.report.overview.warnings[this.Warning.TUNNEL] ?
    • Lack of port level segmentation, machines successfully tunneled monkey activity using unused ports.
    • : null}

    @@ -110,8 +391,7 @@ class ReportPageComponent extends React.Component {

    - {/* TODO: Replace 6,2 with data */} - The Monkey discovered 6 machines and successfully breached 2 of them. + The Monkey discovered {this.state.report.glance.scanned.length} machines and successfully breached {this.state.report.glance.exploited.length} of them.
    In addition, while attempting to exploit additional hosts , security software installed in the network should have picked up the attack attempts and logged them.
    @@ -120,191 +400,27 @@ class ReportPageComponent extends React.Component {

    - +
    - +
    - - {/* TODO: Add table of scanned servers */} +
    - +

    Recommendations

    -
    -

    Issue #1

    -
    - The machine Monkey-SMB with the following IP addresses 192.168.0.1 10.0.0.18 was vulnerable to a SMB attack. -
    - The attack succeeded by authenticating over SMB protocol with user Administrator and its password. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    -
    + {this.state.report.recommendations.issues.map(this.generateIssue)}
    -
    -

    Issue #2

    -
    - The machine Monkey-SMB2 with the following IP address 192.168.0.2 was vulnerable to a SMB attack. -
    - The attack succeeded by using a pass-the-hash attack over SMB protocol with user temp. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    -
    -
    -
    -

    Issue #3

    -
    - The machine Monkey-WMI with the following IP address 192.168.0.3 was vulnerable to a WMI attack. -
    - The attack succeeded by authenticating over WMI protocol with user Administrator and its password. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    -
    -
    -
    -

    Issue #4

    -
    - The machine Monkey-WMI2 with the following IP address 192.168.0.4 was vulnerable to a WMI attack. -
    - The attack succeeded by using a pass-the-hash attack over WMI protocol with user Administrator. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    -
    -
    -
    -

    Issue #5

    -
    - The machine Monkey-SSH with the following IP address 192.168.0.5 was vulnerable to a SSH attack. -
    - The attack succeeded by authenticating over SSH protocol with user user and its password. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    -
    -
    -
    -

    Issue #6

    -
    - The machine Monkey-RDP with the following IP address 192.168.0.6 was vulnerable to a RDP attack. -
    - The attack succeeded by authenticating over RDP protocol with user Administrator and its password. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    -
    -
    -
    -

    Issue #7

    -
    - The machine Monkey-SambaCry with the following IP address 192.168.0.7 was vulnerable to a SambaCry attack. -
    - The attack succeeded by authenticating over SMB protocol with user user and its password, and by using the SambaCry vulnerability. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up.
    • -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    -
    -
    -
    -

    Issue #8

    -
    - The machine Monkey-Elastic with the following IP address 192.168.0.8 was vulnerable to an Elastic Groovy attack. -
    - The attack succeeded because the Elastic Search server was not parched against CVE-2015-1427. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Update your Elastic Search server to version 1.4.3 and up.
    • -
    -
    -
    -
    -

    Issue #9

    -
    - The machine Monkey-Shellshock with the following IP address 192.168.0.9 was vulnerable to a ShellShock attack. -
    - The attack succeeded because the HTTP server running on port 8080 was vulnerable to a shell injection attack on the paths: /cgi/backserver.cgi /cgi/login.cgi. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Update your Bash to a ShellShock-patched version.
    • -
    -
    -
    -
    -

    Issue #10

    -
    - The machine Monkey-Conficker with the following IP address 192.168.0.10 was vulnerable to a Conficker attack. -
    - The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to Conficker. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Install the latest Windows updates or upgrade to a newer operating system.
    • -
    -
    -
    -
    -

    Issue #11

    -
    - The network can probably be segmented. A monkey instance on Monkey-SMB in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network. -
    - In order to protect the network, the following steps should be performed: -
      -
    • Segment your network. Make sure machines can't access machines from other segments.
    • -
    -
    -
    -
    -

    Issue #12

    -
    - The network can probably be segmented. A monkey instance on Monkey-SSH in the 192.168.0.0/24 network could directly access the Monkey Island C&C server in the 172.168.0.0/24 network. -
    - In order to protect the network, the following steps should be performed: -
      -
    • Segment your network. Make sure machines can't access machines from other segments.
    • -
    -
    -
    -
    -

    Issue #13

    -
    - Machines are not locked down at port level. Network tunnel was set up from Monkey-SSH to Monkey-SambaCry. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use micro-segmentation policies to disable communication other than the required.
    • -
    -
    -
    -
    ); diff --git a/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js b/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js index 4e7570e9f..413a19058 100644 --- a/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js +++ b/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js @@ -18,8 +18,8 @@ class ScannedBreachedChartComponent extends React.Component { render() { const data = [ - {label: 'Scanned', value: 4, color: '#f0ad4e'}, - {label: 'Exploited', value: 2, color: '#d9534f'} + {label: 'Scanned', value: this.props.scanned - this.props.exploited, color: '#f0ad4e'}, + {label: 'Exploited', value: this.props.exploited, color: '#d9534f'} ]; return ( From 35bbd38d2ef42557b5f61384abaea67abdada92b Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 21 Nov 2017 16:40:26 +0200 Subject: [PATCH 26/92] Report uses data from server now --- monkey_island/cc/services/report.py | 69 +++++++++++++++++++ .../cc/ui/src/components/pages/ReportPage.js | 49 +------------ 2 files changed, 72 insertions(+), 46 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 959be1ac5..a03e1fbc8 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -88,6 +88,74 @@ class ReportService: @staticmethod def get_report(): + return \ + { + 'overview': + { + 'monkey_start_time': '01/02/2017 21:45', + 'monkey_duration': '23:12 minutes', + 'issues': [False, True, True, True, False, True], + 'warnings': [True, True] + }, + 'glance': + { + 'scanned': + [{"services": ["tcp-22: ssh", "elastic-search-9200: Lorelei Travis"], + "ip_addresses": ["11.0.0.13"], "accessible_from_nodes": ["webServer-shellshock0"], + "label": "Ubuntu-4ubuntu2.1"}, + {"services": [], "ip_addresses": ["10.0.3.23"], "accessible_from_nodes": [], + "label": "ubuntu"}, + {"services": ["tcp-22: ssh", "tcp-80: http"], "ip_addresses": ["10.0.3.68", "11.0.0.41"], + "accessible_from_nodes": ["Monkey-MSSQL1", "ubuntu"], "label": "webServer-shellshock0"}, + {"services": ["tcp-445: Windows Server 2012 R2 Standard 6.3"], + "ip_addresses": ["12.0.0.90", "11.0.0.90"], + "accessible_from_nodes": ["webServer-shellshock0"], "label": "Monkey-MSSQL1"}], + 'exploited': + [{"ip_addresses": ["10.0.3.68", "11.0.0.41"], + "exploits": ["ShellShockExploiter", "ShellShockExploiter"], + "label": "webServer-shellshock0"}, + {"ip_addresses": ["12.0.0.90", "11.0.0.90"], "exploits": ["SmbExploiter", "SmbExploiter"], + "label": "Monkey-MSSQL1"}], + 'stolen_creds': + [ + {'username': 'admin', 'password': 'secretpassword', 'type': 'password', 'origin': 'Monkey-SMB'}, + {'username': 'user', 'password': 'my_password', 'type': 'password', 'origin': 'Monkey-SMB2'}, + {'username': 'dan', 'password': '066DDFD4EF0E9CD7C256FE77191EF43C', 'type': 'NTLM', + 'origin': 'Monkey-RDP'}, + {'username': 'joe', 'password': 'FDA95FBECA288D44AAD3B435B51404EE', 'type': 'LM', + 'origin': 'Monkey-RDP'} + ] + }, + 'recommendations': + { + 'issues': + [ + {'type': 'smb_password', 'machine': 'Monkey-SMB', + 'ip_addresses': ['192.168.0.1', '10.0.0.18'], 'username': 'Administrator'}, + {'type': 'smb_pth', 'machine': 'Monkey-SMB2', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], + 'username': 'Administrator'}, + {'type': 'wmi_password', 'machine': 'Monkey-WMI', + 'ip_addresses': ['192.168.0.1', '10.0.0.18'], 'username': 'Administrator'}, + {'type': 'wmi_pth', 'machine': 'Monkey-WMI2', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], + 'username': 'Administrator'}, + {'type': 'ssh', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], + 'username': 'Administrator'}, + {'type': 'rdp', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], + 'username': 'Administrator'}, + {'type': 'sambacry', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], + 'username': 'Administrator'}, + {'type': 'elastic', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18']}, + {'type': 'shellshock', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], + 'port': 8080, 'paths': ['/cgi/backserver.cgi', '/cgi/login.cgi']}, + {'type': 'conficker', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18']}, + {'type': 'cross_segment', 'machine': 'Monkey-SMB', 'network': '192.168.0.0/24', + 'server_network': '172.168.0.0/24'}, + {'type': 'tunnel', 'origin': 'Monkey-SSH', 'dest': 'Monkey-SambaCry'} + ] + } + } + # TODO: put implementation in template + """ return \ { 'first_monkey_time': ReportService.get_first_monkey_time(), @@ -99,6 +167,7 @@ class ReportService: 'exploited': ReportService.get_exploited(), 'reused_passwords': ReportService.get_reused_passwords() } + """ @staticmethod def did_exploit_type_succeed(exploit_type): diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index dee3c8cb2..ce5e00f4d 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -27,56 +27,14 @@ class ReportPageComponent extends React.Component { constructor(props) { super(props); - this.stolen_passwords = - [ - {username: 'admin', password: 'secretpassword', type: 'password', origin: 'Monkey-SMB'}, - {username: 'user', password: 'my_password', type: 'password', origin: 'Monkey-SMB2'}, - {username: 'dan', password: '066DDFD4EF0E9CD7C256FE77191EF43C', type: 'NTLM', origin: 'Monkey-RDP'}, - {username: 'joe', password: 'FDA95FBECA288D44AAD3B435B51404EE', type: 'LM', origin: 'Monkey-RDP'} - ]; this.state = { - report: { - overview: - { - monkey_start_time: '01/02/2017 21:45', - monkey_duration: '23:12 minutes', - issues: [false, true, true, true, false, true], - warnings: [true, true] - }, - glance: - { - scanned: - [{"services": ["tcp-22: ssh", "elastic-search-9200: Lorelei Travis"], "ip_addresses": ["11.0.0.13"], "accessible_from_nodes": ["webServer-shellshock0"], "label": "Ubuntu-4ubuntu2.1"}, {"services": [], "ip_addresses": ["10.0.3.23"], "accessible_from_nodes": [], "label": "ubuntu"}, {"services": ["tcp-22: ssh", "tcp-80: http"], "ip_addresses": ["10.0.3.68", "11.0.0.41"], "accessible_from_nodes": ["Monkey-MSSQL1", "ubuntu"], "label": "webServer-shellshock0"}, {"services": ["tcp-445: Windows Server 2012 R2 Standard 6.3"], "ip_addresses": ["12.0.0.90", "11.0.0.90"], "accessible_from_nodes": ["webServer-shellshock0"], "label": "Monkey-MSSQL1"}], - exploited: - [{"ip_addresses": ["10.0.3.68", "11.0.0.41"], "exploits": ["ShellShockExploiter", "ShellShockExploiter"], "label": "webServer-shellshock0"}, {"ip_addresses": ["12.0.0.90", "11.0.0.90"], "exploits": ["SmbExploiter", "SmbExploiter"], "label": "Monkey-MSSQL1"}], - stolen_creds: this.stolen_passwords - }, - recommendations: - { - issues: - [ - {type: 'smb_password', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, - {type: 'smb_pth', machine: 'Monkey-SMB2', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, - {type: 'wmi_password', machine: 'Monkey-WMI', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, - {type: 'wmi_pth', machine: 'Monkey-WMI2', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, - {type: 'ssh', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, - {type: 'rdp', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, - {type: 'sambacry', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], username: 'Administrator'}, - {type: 'elastic', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18']}, - {type: 'shellshock', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18'], port: 8080, paths: ['/cgi/backserver.cgi', '/cgi/login.cgi']}, - {type: 'conficker', machine: 'Monkey-SMB', ip_addresses: ['192.168.0.1', '10.0.0.18']}, - {type: 'cross_segment', machine: 'Monkey-SMB', network: '192.168.0.0/24', server_network: '172.168.0.0/24'}, - {type: 'tunnel', origin: 'Monkey-SSH', dest: 'Monkey-SambaCry'} - ] - } - }, + report: {}, graph: {nodes: [], edges: []} }; } componentDidMount() { - // TODO: uncomment - //this.getReportFromServer(); + this.getReportFromServer(); this.updateMapFromServer(); this.interval = setInterval(this.updateMapFromServer, 1000); } @@ -342,8 +300,7 @@ class ReportPageComponent extends React.Component { render() { let content; - // TODO: remove 0==1 - if (0==1 || Object.keys(this.state.report).length === 0) { + if (Object.keys(this.state.report).length === 0) { content = (

    Generating Report...

    ); } else { content = From 133bd7d80a3d20f740338515f453972e08fa8d29 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 21 Nov 2017 17:37:13 +0200 Subject: [PATCH 27/92] Following fields use real data now: First monkey time, monkey duration, scanned servers, breached servers, stolen passwords --- monkey_island/cc/services/report.py | 81 +++++++++++++++++------------ 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index a03e1fbc8..634219ccb 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,3 +1,5 @@ +import datetime + from cc.database import mongo from cc.services.config import ConfigService from cc.services.edge import EdgeService @@ -18,6 +20,24 @@ class ReportService: 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 = "" + if delta.days > 0: + st += "%d days," % delta.days + total = delta.seconds + seconds = total % 60 + total = (total - seconds) / 60 + minutes = total % 60 + total = (total - minutes) / 60 + hours = total + if hours > 0: + st += "%d hours," % hours + + st += "%d minutes and %d seconds" % (minutes, seconds) + return st + @staticmethod def get_breach_count(): return mongo.db.edge.count({'exploits.result': True}) @@ -86,45 +106,46 @@ class ReportService: return exploited + @staticmethod + def get_stolen_creds(): + PASS_TYPE_DICT = {'password': 'Password', 'lm_hash': 'LM', 'ntlm_hash': 'NTLM'} + 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, + 'password': monkey_creds[user][pass_type], + 'type': PASS_TYPE_DICT[pass_type], + 'origin': origin + } + ) + return creds + @staticmethod def get_report(): return \ { 'overview': { - 'monkey_start_time': '01/02/2017 21:45', - 'monkey_duration': '23:12 minutes', + 'monkey_start_time': ReportService.get_first_monkey_time().strftime("%d/%m/%Y %H:%M:%S"), + 'monkey_duration': ReportService.get_monkey_duration(), 'issues': [False, True, True, True, False, True], 'warnings': [True, True] }, 'glance': { - 'scanned': - [{"services": ["tcp-22: ssh", "elastic-search-9200: Lorelei Travis"], - "ip_addresses": ["11.0.0.13"], "accessible_from_nodes": ["webServer-shellshock0"], - "label": "Ubuntu-4ubuntu2.1"}, - {"services": [], "ip_addresses": ["10.0.3.23"], "accessible_from_nodes": [], - "label": "ubuntu"}, - {"services": ["tcp-22: ssh", "tcp-80: http"], "ip_addresses": ["10.0.3.68", "11.0.0.41"], - "accessible_from_nodes": ["Monkey-MSSQL1", "ubuntu"], "label": "webServer-shellshock0"}, - {"services": ["tcp-445: Windows Server 2012 R2 Standard 6.3"], - "ip_addresses": ["12.0.0.90", "11.0.0.90"], - "accessible_from_nodes": ["webServer-shellshock0"], "label": "Monkey-MSSQL1"}], - 'exploited': - [{"ip_addresses": ["10.0.3.68", "11.0.0.41"], - "exploits": ["ShellShockExploiter", "ShellShockExploiter"], - "label": "webServer-shellshock0"}, - {"ip_addresses": ["12.0.0.90", "11.0.0.90"], "exploits": ["SmbExploiter", "SmbExploiter"], - "label": "Monkey-MSSQL1"}], - 'stolen_creds': - [ - {'username': 'admin', 'password': 'secretpassword', 'type': 'password', 'origin': 'Monkey-SMB'}, - {'username': 'user', 'password': 'my_password', 'type': 'password', 'origin': 'Monkey-SMB2'}, - {'username': 'dan', 'password': '066DDFD4EF0E9CD7C256FE77191EF43C', 'type': 'NTLM', - 'origin': 'Monkey-RDP'}, - {'username': 'joe', 'password': 'FDA95FBECA288D44AAD3B435B51404EE', 'type': 'LM', - 'origin': 'Monkey-RDP'} - ] + 'scanned': ReportService.get_scanned(), + 'exploited': ReportService.get_exploited(), + 'stolen_creds': ReportService.get_stolen_creds() }, 'recommendations': { @@ -158,13 +179,9 @@ class ReportService: """ return \ { - 'first_monkey_time': ReportService.get_first_monkey_time(), - 'last_monkey_dead_time': ReportService.get_last_monkey_dead_time(), 'breach_count': ReportService.get_breach_count(), 'successful_exploit_types': ReportService.get_successful_exploit_types(), 'tunnels': ReportService.get_tunnels(), - 'scanned': ReportService.get_scanned(), - 'exploited': ReportService.get_exploited(), 'reused_passwords': ReportService.get_reused_passwords() } """ From 82e30040ebfef0cdb3cce8cdc4613b584b2108e6 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 21 Nov 2017 17:39:42 +0200 Subject: [PATCH 28/92] Add spaces in time string --- monkey_island/cc/services/report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 634219ccb..51d1bf51f 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -25,7 +25,7 @@ class ReportService: delta = ReportService.get_last_monkey_dead_time() - ReportService.get_first_monkey_time() st = "" if delta.days > 0: - st += "%d days," % delta.days + st += "%d days, " % delta.days total = delta.seconds seconds = total % 60 total = (total - seconds) / 60 @@ -33,7 +33,7 @@ class ReportService: total = (total - minutes) / 60 hours = total if hours > 0: - st += "%d hours," % hours + st += "%d hours, " % hours st += "%d minutes and %d seconds" % (minutes, seconds) return st From ce10ef00e4734252320f91ee6db9426d3c1d99b9 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 27 Nov 2017 15:20:59 +0200 Subject: [PATCH 29/92] Everything implemented on backend --- monkey_island/cc/services/node.py | 18 ++ monkey_island/cc/services/report.py | 223 ++++++++++++------ .../cc/ui/src/components/pages/ReportPage.js | 26 +- monkey_island/cc/utils.py | 9 + monkey_island/requirements.txt | 3 +- 5 files changed, 197 insertions(+), 82 deletions(-) diff --git a/monkey_island/cc/services/node.py b/monkey_island/cc/services/node.py index dc30d60d5..21c2ef194 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -292,3 +292,21 @@ class NodeService: {'_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 index 51d1bf51f..609b17c31 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,9 +1,9 @@ -import datetime +import ipaddress 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" @@ -38,25 +38,20 @@ class ReportService: st += "%d minutes and %d seconds" % (minutes, seconds) return st - @staticmethod - def get_breach_count(): - return mongo.db.edge.count({'exploits.result': True}) - - @staticmethod - def get_successful_exploit_types(): - exploit_types = mongo.db.command({'distinct': 'edge', 'key': 'exploits.exploiter'})['values'] - return [exploit for exploit in exploit_types if ReportService.did_exploit_type_succeed(exploit)] - @staticmethod def get_tunnels(): return [ - (NodeService.get_monkey_label_by_id(tunnel['_id']), NodeService.get_monkey_label_by_id(tunnel['tunnel'])) + { + 'type': 'tunnel', + 'origin': 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']) for node in mongo.db.node.find({}, {'_id': 1})]\ + nodes = \ + [NodeService.get_displayed_node_by_id(node['_id']) for node in mongo.db.node.find({}, {'_id': 1})] \ + [NodeService.get_displayed_node_by_id(monkey['_id']) for monkey in mongo.db.monkey.find({}, {'_id': 1})] nodes = [ { @@ -73,32 +68,17 @@ class ReportService: return nodes - @staticmethod - def get_reused_passwords(): - password_dict = {} - password_list = ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list']) - for password in password_list: - machines_with_password =\ - [ - NodeService.get_monkey_label_by_id(node['_id']) - for node in mongo.db.monkey.find({'creds.password': password}, {'_id': 1}) - ] - if len(machines_with_password) >= 2: - password_dict[password] = machines_with_password - - return password_dict - @staticmethod def get_exploited(): - exploited =\ + exploited = \ [NodeService.get_displayed_node_by_id(monkey['_id']) for monkey in mongo.db.monkey.find({}, {'_id': 1}) - if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))]\ + if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))] \ + [NodeService.get_displayed_node_by_id(node['_id']) for node in mongo.db.node.find({'exploited': True}, {'_id': 1})] exploited = [ { - 'label': monkey['hostname'] if 'hostname' in monkey else monkey['os']['version'], + 'label': NodeService.get_node_hostname(monkey), 'ip_addresses': monkey['ip_addresses'], 'exploits': [exploit['exploiter'] for exploit in monkey['exploits'] if exploit['result']] } @@ -111,8 +91,8 @@ class ReportService: PASS_TYPE_DICT = {'password': 'Password', 'lm_hash': 'LM', 'ntlm_hash': 'NTLM'} creds = [] for telem in mongo.db.telemetry.find( - {'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}}, - {'data.credentials': 1, 'monkey_guid': 1} + {'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: @@ -130,6 +110,146 @@ class ReportService: ) 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' + 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'] + if exploiter_type == 'SmbExploiter': + return ReportService.process_smb_exploit(exploit) + if exploiter_type == 'WmiExploiter': + return ReportService.process_wmi_exploit(exploit) + if exploiter_type == 'SSHExploiter': + return ReportService.process_ssh_exploit(exploit) + if exploiter_type == 'RdpExploiter': + return ReportService.process_rdp_exploit(exploit) + if exploiter_type == 'SambaCryExploiter': + return ReportService.process_sambacry_exploit(exploit) + if exploiter_type == 'ElasticGroovyExploiter': + return ReportService.process_elastic_exploit(exploit) + if exploiter_type == 'Ms08_067_Exploiter': + return ReportService.process_conficker_exploit(exploit) + if exploiter_type == 'ShellShockExploiter': + return ReportService.process_shellshock_exploit(exploit) + + @staticmethod + def get_exploits(): + return [ReportService.process_exploit(exploit) for + exploit in mongo.db.telemetry.find({'telem_type': 'exploit', 'data.result': True})] + + @staticmethod + def get_monkey_subnets(monkey_guid): + return \ + [ + ipaddress.ip_interface(unicode(network['addr'] + '/' + network['netmask'])).network + for network in + mongo.db.telemetry.find_one( + {'telem_type': 'system_info_collection', 'monkey_guid': monkey_guid}, + {'data.network_info.networks': 1} + )['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(): + return ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + @staticmethod def get_report(): return \ @@ -149,42 +269,9 @@ class ReportService: }, 'recommendations': { - 'issues': - [ - {'type': 'smb_password', 'machine': 'Monkey-SMB', - 'ip_addresses': ['192.168.0.1', '10.0.0.18'], 'username': 'Administrator'}, - {'type': 'smb_pth', 'machine': 'Monkey-SMB2', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], - 'username': 'Administrator'}, - {'type': 'wmi_password', 'machine': 'Monkey-WMI', - 'ip_addresses': ['192.168.0.1', '10.0.0.18'], 'username': 'Administrator'}, - {'type': 'wmi_pth', 'machine': 'Monkey-WMI2', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], - 'username': 'Administrator'}, - {'type': 'ssh', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], - 'username': 'Administrator'}, - {'type': 'rdp', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], - 'username': 'Administrator'}, - {'type': 'sambacry', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], - 'username': 'Administrator'}, - {'type': 'elastic', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18']}, - {'type': 'shellshock', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18'], - 'port': 8080, 'paths': ['/cgi/backserver.cgi', '/cgi/login.cgi']}, - {'type': 'conficker', 'machine': 'Monkey-SMB', 'ip_addresses': ['192.168.0.1', '10.0.0.18']}, - {'type': 'cross_segment', 'machine': 'Monkey-SMB', 'network': '192.168.0.0/24', - 'server_network': '172.168.0.0/24'}, - {'type': 'tunnel', 'origin': 'Monkey-SSH', 'dest': 'Monkey-SambaCry'} - ] + 'issues': ReportService.get_issues() } } - # TODO: put implementation in template - """ - return \ - { - 'breach_count': ReportService.get_breach_count(), - 'successful_exploit_types': ReportService.get_successful_exploit_types(), - 'tunnels': ReportService.get_tunnels(), - 'reused_passwords': ReportService.get_reused_passwords() - } - """ @staticmethod def did_exploit_type_succeed(exploit_type): diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index ce5e00f4d..305d39fd6 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -65,8 +65,8 @@ class ReportPageComponent extends React.Component { }); } - generateIpListBadges(ip_addresses) { - return ip_addresses.map(ip_address => {ip_address}); + generateInfoBadges(data_array) { + return data_array.map(badge_data => {badge_data}); } generateShellshockPathListBadges(paths) { @@ -76,7 +76,7 @@ class ReportPageComponent extends React.Component { generateSmbPasswordIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a SMB attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SMB attack.
    The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password.
    @@ -91,7 +91,7 @@ class ReportPageComponent extends React.Component { generateSmbPthIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a SMB attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SMB attack.
    The attack succeeded by using a pass-the-hash attack over SMB protocol with user {issue.username}.
    @@ -106,7 +106,7 @@ class ReportPageComponent extends React.Component { generateWmiPasswordIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a WMI attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a WMI attack.
    The attack succeeded by authenticating over WMI protocol with user {issue.username} and its password.
    @@ -121,7 +121,7 @@ class ReportPageComponent extends React.Component { generateWmiPthIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a WMI attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a WMI attack.
    The attack succeeded by using a pass-the-hash attack over WMI protocol with user {issue.username}.
    @@ -136,7 +136,7 @@ class ReportPageComponent extends React.Component { generateSshIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a SSH attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SSH attack.
    The attack succeeded by authenticating over SSH protocol with user {issue.username} and its password.
    @@ -151,7 +151,7 @@ class ReportPageComponent extends React.Component { generateRdpIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a RDP attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a RDP attack.
    The attack succeeded by authenticating over RDP protocol with user {issue.username} and its password.
    @@ -166,7 +166,7 @@ class ReportPageComponent extends React.Component { generateSambaCryIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a SambaCry attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SambaCry attack.
    The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password, and by using the SambaCry vulnerability.
    @@ -182,7 +182,7 @@ class ReportPageComponent extends React.Component { generateElasticIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to an Elastic Groovy attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to an Elastic Groovy attack.
    The attack succeeded because the Elastic Search server was not parched against CVE-2015-1427.
    @@ -197,7 +197,7 @@ class ReportPageComponent extends React.Component { generateShellshockIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a ShellShock attack. + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a ShellShock attack.
    The attack succeeded because the HTTP server running on port {issue.port} was vulnerable to a shell injection attack on the paths: {this.generateShellshockPathListBadges(issue.paths)}.
    @@ -212,7 +212,7 @@ class ReportPageComponent extends React.Component { generateConfickerIssue(issue) { return (
    - The machine {issue.machine} with the following IP addresses {this.generateIpListBadges(issue.ip_addresses)} was vulnerable to a Conficker attack. + The machine {issue.machine} with the following address {issue.ip_address} was vulnerable to a Conficker attack.
    The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to Conficker.
    @@ -227,7 +227,7 @@ class ReportPageComponent extends React.Component { generateCrossSegmentIssue(issue) { return (
    - The network can probably be segmented. A monkey instance on {issue.machine} in the {issue.network} network could directly access the Monkey Island C&C server in the {issue.server_network} network. + The network can probably be segmented. A monkey instance on {issue.machine} in the networks {this.generateInfoBadges(issue.networks)} could directly access the Monkey Island C&C server in the networks {this.generateInfoBadges(issue.server_networks)}.
    In order to protect the network, the following steps should be performed:
      diff --git a/monkey_island/cc/utils.py b/monkey_island/cc/utils.py index 34026b157..d59c23825 100644 --- a/monkey_island/cc/utils.py +++ b/monkey_island/cc/utils.py @@ -4,6 +4,7 @@ import sys import array import struct +import ipaddress from netifaces import interfaces, ifaddresses, AF_INET from cc.database import mongo @@ -56,3 +57,11 @@ def local_ip_addresses(): addresses = ifaddresses(interface).get(AF_INET, []) ip_list.extend([link['addr'] for link in addresses if link['addr'] != '127.0.0.1']) return ip_list + + +def get_subnets(): + subnets = [] + for interface in interfaces(): + addresses = ifaddresses(interface).get(AF_INET, []) + subnets.extend([ipaddress.ip_interface(link['addr'] + '/' + link['netmask']).network for link in addresses if link['addr'] != '127.0.0.1']) + return subnets diff --git a/monkey_island/requirements.txt b/monkey_island/requirements.txt index 275c8b96a..1aa7288c5 100644 --- a/monkey_island/requirements.txt +++ b/monkey_island/requirements.txt @@ -9,4 +9,5 @@ flask Flask-Pymongo Flask-Restful jsonschema -netifaces \ No newline at end of file +netifaces +ipaddress \ No newline at end of file From f72b32bb671abcadf2c2b60dc2527fa084ce8560 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Mon, 27 Nov 2017 15:51:56 +0200 Subject: [PATCH 30/92] Removed duplicate issues --- monkey_island/cc/services/report.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 609b17c31..30cdf14ec 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -208,8 +208,12 @@ class ReportService: @staticmethod def get_exploits(): - return [ReportService.process_exploit(exploit) for - exploit in mongo.db.telemetry.find({'telem_type': 'exploit', 'data.result': True})] + 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): From 4f6ed955017d4ce1cc6565064fbc90fb542e984b Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 13:40:51 +0200 Subject: [PATCH 31/92] Fix bug with exploited nodes --- monkey_island/cc/services/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 30cdf14ec..3a3509a40 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -78,7 +78,7 @@ class ReportService: exploited = [ { - 'label': NodeService.get_node_hostname(monkey), + 'label': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(monkey['id'])), 'ip_addresses': monkey['ip_addresses'], 'exploits': [exploit['exploiter'] for exploit in monkey['exploits'] if exploit['result']] } From 2aadb1281555d7a6a76be824c4a266153c853a0b Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 14:16:16 +0200 Subject: [PATCH 32/92] Change page structure --- .../cc/ui/src/components/pages/ReportPage.js | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 305d39fd6..be5c0e9e3 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -311,16 +311,21 @@ class ReportPageComponent extends React.Component { Overview

      - The monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished propagation attempts. + The first monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished propagation attempts.

      - From the attacker's point of view, the network looks like this: + A full report of the Monkeys activities follows.

      -
      - -
      +
    +
    +

    + Security Findings +

    - During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function(x){return x===true;}).length}, detailed below. The security issues uncovered include: +

    + Immediate Threats +

    + During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function(x){return x===true;}).length} issues, detailed below. The security issues uncovered include:
      {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
    • Users with weak passwords.
    • : null} {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
    • Stolen passwords/hashes were used to exploit other machines.
    • : null} @@ -331,19 +336,27 @@ class ReportPageComponent extends React.Component {
    - In addition, the monkey uncovered the following possible set of issues: +

    + Security Issues +

    + The monkey uncovered the following possible set of issues:
      {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ?
    • Possible cross segment traffic. Infected machines could communicate with the Monkey Island despite crossing segment boundaries using unused ports.
    • : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ?
    • Lack of port level segmentation, machines successfully tunneled monkey activity using unused ports.
    • : null}
    -

    - A full report of the Monkeys activities follows. -

    +
    +
    +

    + Recommendations +

    +
    + {this.state.report.recommendations.issues.map(this.generateIssue)} +

    - At a Glance + The Network from the Monkey's Eyes

    @@ -361,6 +374,12 @@ class ReportPageComponent extends React.Component {
    +

    + From the attacker's point of view, the network looks like this: +

    +
    + +
    @@ -371,14 +390,6 @@ class ReportPageComponent extends React.Component {
    -
    -

    - Recommendations -

    -
    - {this.state.report.recommendations.issues.map(this.generateIssue)} -
    -
    ); } From 046b18e71cd320b8168bd3c0d43346e85f923131 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 14:22:11 +0200 Subject: [PATCH 33/92] Don't show actual password on stolen creds table --- monkey_island/cc/services/report.py | 3 +-- .../cc/ui/src/components/report-components/StolenPasswords.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 3a3509a40..e7ec00e5e 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -88,7 +88,7 @@ class ReportService: @staticmethod def get_stolen_creds(): - PASS_TYPE_DICT = {'password': 'Password', 'lm_hash': 'LM', 'ntlm_hash': 'NTLM'} + PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM', 'ntlm_hash': 'NTLM'} creds = [] for telem in mongo.db.telemetry.find( {'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}}, @@ -103,7 +103,6 @@ class ReportService: creds.append( { 'username': user, - 'password': monkey_creds[user][pass_type], 'type': PASS_TYPE_DICT[pass_type], 'origin': origin } diff --git a/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js index 754b51f92..7c42b6ea5 100644 --- a/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js +++ b/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js @@ -6,7 +6,6 @@ const columns = [ Header: 'Stolen Credentials', columns: [ { Header: 'Username', accessor: 'username'}, - { Header: 'Password/Hash', accessor: 'password'}, { Header: 'Type', accessor: 'type'}, { Header: 'Origin', accessor: 'origin'} ] From 10375c093e9cea536eb2f4c8b80f056e3e2d9df7 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 14:33:41 +0200 Subject: [PATCH 34/92] Sort recommendations by machine --- monkey_island/cc/services/report.py | 6 ++++-- monkey_island/cc/ui/src/components/pages/ReportPage.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index e7ec00e5e..867feeda6 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -43,7 +43,7 @@ class ReportService: return [ { 'type': 'tunnel', - 'origin': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['_id'])), + '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})] @@ -251,7 +251,9 @@ class ReportService: @staticmethod def get_issues(): - return ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + issues.sort(lambda x, y: 1 if x['machine'] > y['machine'] else -1 if x['machine'] < y['machine'] else 0) + return issues @staticmethod def get_report(): diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index be5c0e9e3..3966311f2 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -240,7 +240,7 @@ class ReportPageComponent extends React.Component { generateTunnelIssue(issue) { return (
    - Machines are not locked down at port level. Network tunnel was set up from {issue.origin} to {issue.dest}. + Machines are not locked down at port level. Network tunnel was set up from {issue.machine} to {issue.dest}.
    In order to protect the machine, the following steps should be performed:
      From dff90ab5340f600e7e8c530252e9bbbbc8309d31 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 14:37:11 +0200 Subject: [PATCH 35/92] Remove duplicate exploits on breached servers --- monkey_island/cc/services/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 867feeda6..5af051e72 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -80,7 +80,7 @@ class ReportService: { 'label': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(monkey['id'])), 'ip_addresses': monkey['ip_addresses'], - 'exploits': [exploit['exploiter'] for exploit in monkey['exploits'] if exploit['result']] + 'exploits': list(set([exploit['exploiter'] for exploit in monkey['exploits'] if exploit['result']])) } for monkey in exploited] From 96972aeac9c55266d835ba9fe1b30d9117821d85 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 14:47:54 +0200 Subject: [PATCH 36/92] Micro segmentation, not port level segmentation --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 3966311f2..6c9c7cde3 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -342,7 +342,7 @@ class ReportPageComponent extends React.Component { The monkey uncovered the following possible set of issues:
        {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ?
      • Possible cross segment traffic. Infected machines could communicate with the Monkey Island despite crossing segment boundaries using unused ports.
      • : null} - {this.state.report.overview.warnings[this.Warning.TUNNEL] ?
      • Lack of port level segmentation, machines successfully tunneled monkey activity using unused ports.
      • : null} + {this.state.report.overview.warnings[this.Warning.TUNNEL] ?
      • Lack of Micro-segmentation, machines successfully tunneled monkey activity using unused ports.
      • : null}
    From da55b0b26ba0bbd5a4344fae90c0c7f53ab7fad1 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 17:11:00 +0200 Subject: [PATCH 37/92] Group recommendations by machine. Show recommendation with collapsible incident --- monkey_island/cc/services/report.py | 9 +- .../cc/ui/src/components/pages/ReportPage.js | 229 ++++++++++-------- .../report-components/CollapsibleWell.js | 30 +++ 3 files changed, 168 insertions(+), 100 deletions(-) create mode 100644 monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 5af051e72..01205e71a 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -252,8 +252,13 @@ class ReportService: @staticmethod def get_issues(): issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() - issues.sort(lambda x, y: 1 if x['machine'] > y['machine'] else -1 if x['machine'] < y['machine'] else 0) - return 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_report(): diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 6c9c7cde3..24c666c68 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -6,6 +6,7 @@ import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {options, edgeGroupToColor} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/StolenPasswords'; import ScannedBreachedChart from 'components/report-components/ScannedBreachedChart'; +import CollapsableWellComponent from "../report-components/CollapsibleWell"; class ReportPageComponent extends React.Component { @@ -70,20 +71,22 @@ class ReportPageComponent extends React.Component { } generateShellshockPathListBadges(paths) { - return paths.map(path => {path}); + return paths.map(path => {path}); } generateSmbPasswordIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SMB attack. -
    - The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SMB attack. +
    + The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password. +
    ); } @@ -91,14 +94,16 @@ class ReportPageComponent extends React.Component { generateSmbPthIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SMB attack. -
    - The attack succeeded by using a pass-the-hash attack over SMB protocol with user {issue.username}. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SMB attack. +
    + The attack succeeded by using a pass-the-hash attack over SMB protocol with user {issue.username}. +
    ); } @@ -106,14 +111,16 @@ class ReportPageComponent extends React.Component { generateWmiPasswordIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a WMI attack. -
    - The attack succeeded by authenticating over WMI protocol with user {issue.username} and its password. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a WMI attack. +
    + The attack succeeded by authenticating over WMI protocol with user {issue.username} and its password. +
    ); } @@ -121,14 +128,16 @@ class ReportPageComponent extends React.Component { generateWmiPthIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a WMI attack. -
    - The attack succeeded by using a pass-the-hash attack over WMI protocol with user {issue.username}. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a WMI attack. +
    + The attack succeeded by using a pass-the-hash attack over WMI protocol with user {issue.username}. +
    ); } @@ -136,14 +145,16 @@ class ReportPageComponent extends React.Component { generateSshIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SSH attack. -
    - The attack succeeded by authenticating over SSH protocol with user {issue.username} and its password. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SSH attack. +
    + The attack succeeded by authenticating over SSH protocol with user {issue.username} and its password. +
    ); } @@ -151,14 +162,16 @@ class ReportPageComponent extends React.Component { generateRdpIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a RDP attack. -
    - The attack succeeded by authenticating over RDP protocol with user {issue.username} and its password. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a RDP attack. +
    + The attack succeeded by authenticating over RDP protocol with user {issue.username} and its password. +
    ); } @@ -166,15 +179,19 @@ class ReportPageComponent extends React.Component { generateSambaCryIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SambaCry attack. -
    - The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password, and by using the SambaCry vulnerability. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up.
    • -
    • Use a complex one-use password that is not shared with other computers on the network.
    • -
    + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. +
    + Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SambaCry attack. +
    + The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password, and by using the SambaCry + vulnerability. +
    ); } @@ -182,14 +199,14 @@ class ReportPageComponent extends React.Component { generateElasticIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to an Elastic Groovy attack. -
    - The attack succeeded because the Elastic Search server was not parched against CVE-2015-1427. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Update your Elastic Search server to version 1.4.3 and up.
    • -
    + Update your Elastic Search server to version 1.4.3 and up. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to an Elastic Groovy attack. +
    + The attack succeeded because the Elastic Search server was not parched against CVE-2015-1427. +
    ); } @@ -197,14 +214,16 @@ class ReportPageComponent extends React.Component { generateShellshockIssue(issue) { return (
    - The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a ShellShock attack. -
    - The attack succeeded because the HTTP server running on port {issue.port} was vulnerable to a shell injection attack on the paths: {this.generateShellshockPathListBadges(issue.paths)}. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Update your Bash to a ShellShock-patched version.
    • -
    + Update your Bash to a ShellShock-patched version. + + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a ShellShock attack. +
    + The attack succeeded because the HTTP server running on port {issue.port} was vulnerable to a shell injection attack on the + paths: {this.generateShellshockPathListBadges(issue.paths)}. +
    ); } @@ -212,14 +231,15 @@ class ReportPageComponent extends React.Component { generateConfickerIssue(issue) { return (
    - The machine {issue.machine} with the following address {issue.ip_address} was vulnerable to a Conficker attack. -
    - The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to Conficker. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Install the latest Windows updates or upgrade to a newer operating system.
    • -
    + Install the latest Windows updates or upgrade to a newer operating system. + + The machine {issue.machine} with the following address {issue.ip_address} was vulnerable to a Conficker attack. +
    + The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to + Conficker. +
    ); } @@ -227,12 +247,14 @@ class ReportPageComponent extends React.Component { generateCrossSegmentIssue(issue) { return (
    - The network can probably be segmented. A monkey instance on {issue.machine} in the networks {this.generateInfoBadges(issue.networks)} could directly access the Monkey Island C&C server in the networks {this.generateInfoBadges(issue.server_networks)}. -
    - In order to protect the network, the following steps should be performed: -
      -
    • Segment your network. Make sure machines can't access machines from other segments.
    • -
    + Segment your network. Make sure machines can't access machines from other segments. + + The network can probably be segmented. A monkey instance on {issue.machine} in the + networks {this.generateInfoBadges(issue.networks)} + could directly access the Monkey Island C&C server in the + networks {this.generateInfoBadges(issue.server_networks)}. +
    ); } @@ -240,12 +262,12 @@ class ReportPageComponent extends React.Component { generateTunnelIssue(issue) { return (
    - Machines are not locked down at port level. Network tunnel was set up from {issue.machine} to {issue.dest}. -
    - In order to protect the machine, the following steps should be performed: -
      -
    • Use micro-segmentation policies to disable communication other than the required.
    • -
    + Use micro-segmentation policies to disable communication other than the required. + + Machines are not locked down at port level. Network tunnel was set up from {issue.machine} to {issue.dest}. +
    ); } @@ -292,12 +314,23 @@ class ReportPageComponent extends React.Component { } return (
    -

    Issue #{index+1}

    +
    Recommendation #{index + 1}
    {data}
    ); }; + generateIssues = (issues) => { + let issuesDivArray = []; + for (var machine of Object.keys(issues)) { + issuesDivArray.push( +

    {machine}

    + ); + issuesDivArray.push(issues[machine].map(this.generateIssue)); + } + return issuesDivArray; + }; + render() { let content; if (Object.keys(this.state.report).length === 0) { @@ -350,9 +383,9 @@ class ReportPageComponent extends React.Component {

    Recommendations

    -
    - {this.state.report.recommendations.issues.map(this.generateIssue)} -
    +
    + {this.generateIssues(this.state.report.recommendations.issues)} +

    diff --git a/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js b/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js new file mode 100644 index 000000000..0b92712db --- /dev/null +++ b/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js @@ -0,0 +1,30 @@ +import React from 'react'; +import {Collapse, Well} from 'react-bootstrap'; + +class CollapsibleWellComponent extends React.Component { + constructor(props) { + super(props); + this.state = { + open: false + }; + } + + render() { + return ( + + ); + } +} + +export default CollapsibleWellComponent; From 881cf5e7931e08372131dc8d849f8409743641cd Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 17:12:48 +0200 Subject: [PATCH 38/92] Collapsible-Collapsable --- .../cc/ui/src/components/pages/ReportPage.js | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 24c666c68..cea8c8d87 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -6,7 +6,7 @@ import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {options, edgeGroupToColor} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/StolenPasswords'; import ScannedBreachedChart from 'components/report-components/ScannedBreachedChart'; -import CollapsableWellComponent from "../report-components/CollapsibleWell"; +import CollapsibleWellComponent from "../report-components/CollapsibleWell"; class ReportPageComponent extends React.Component { @@ -79,14 +79,14 @@ class ReportPageComponent extends React.Component {
    Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SMB attack.
    The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password. -
    +
    ); } @@ -96,14 +96,14 @@ class ReportPageComponent extends React.Component {
    Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SMB attack.
    The attack succeeded by using a pass-the-hash attack over SMB protocol with user {issue.username}. -
    +
    ); } @@ -113,14 +113,14 @@ class ReportPageComponent extends React.Component {
    Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a WMI attack.
    The attack succeeded by authenticating over WMI protocol with user {issue.username} and its password. -
    +
    ); } @@ -130,14 +130,14 @@ class ReportPageComponent extends React.Component {
    Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a WMI attack.
    The attack succeeded by using a pass-the-hash attack over WMI protocol with user {issue.username}. -
    +
    ); } @@ -147,14 +147,14 @@ class ReportPageComponent extends React.Component {
    Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SSH attack.
    The attack succeeded by authenticating over SSH protocol with user {issue.username} and its password. -
    +
    ); } @@ -164,14 +164,14 @@ class ReportPageComponent extends React.Component {
    Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a RDP attack.
    The attack succeeded by authenticating over RDP protocol with user {issue.username} and its password. -
    +
    ); } @@ -183,7 +183,7 @@ class ReportPageComponent extends React.Component { that is not shared with other computers on the network.
    Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a SambaCry attack. @@ -191,7 +191,7 @@ class ReportPageComponent extends React.Component { The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password, and by using the SambaCry vulnerability. - +

    ); } @@ -200,13 +200,13 @@ class ReportPageComponent extends React.Component { return (
    Update your Elastic Search server to version 1.4.3 and up. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to an Elastic Groovy attack.
    The attack succeeded because the Elastic Search server was not parched against CVE-2015-1427. -
    +
    ); } @@ -215,7 +215,7 @@ class ReportPageComponent extends React.Component { return (
    Update your Bash to a ShellShock-patched version. - + The machine {issue.machine} with the following IP address {issue.ip_address} was vulnerable to a ShellShock attack. @@ -223,7 +223,7 @@ class ReportPageComponent extends React.Component { The attack succeeded because the HTTP server running on port {issue.port} was vulnerable to a shell injection attack on the paths: {this.generateShellshockPathListBadges(issue.paths)}. - +
    ); } @@ -232,14 +232,14 @@ class ReportPageComponent extends React.Component { return (
    Install the latest Windows updates or upgrade to a newer operating system. - + The machine {issue.machine} with the following address {issue.ip_address} was vulnerable to a Conficker attack.
    The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to Conficker. -
    +
    ); } @@ -248,13 +248,13 @@ class ReportPageComponent extends React.Component { return (
    Segment your network. Make sure machines can't access machines from other segments. - + The network can probably be segmented. A monkey instance on {issue.machine} in the networks {this.generateInfoBadges(issue.networks)} could directly access the Monkey Island C&C server in the networks {this.generateInfoBadges(issue.server_networks)}. - +
    ); } @@ -263,11 +263,11 @@ class ReportPageComponent extends React.Component { return (
    Use micro-segmentation policies to disable communication other than the required. - + Machines are not locked down at port level. Network tunnel was set up from {issue.machine} to {issue.dest}. - +
    ); } From 88ea57dc8874e88f9ffb664b2f1e9470d190fcec Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 17:16:58 +0200 Subject: [PATCH 39/92] Fix most printing format issues Improve CSS Shorten lines --- .../cc/ui/src/components/pages/ReportPage.js | 81 +++++++++++++------ monkey_island/cc/ui/src/styles/App.css | 63 +++++++++++---- 2 files changed, 104 insertions(+), 40 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index cea8c8d87..4e34dd3a8 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -3,10 +3,10 @@ import {Col} from 'react-bootstrap'; import BreachedServers from 'components/report-components/BreachedServers'; import ScannedServers from 'components/report-components/ScannedServers'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; -import {options, edgeGroupToColor} from 'components/map/MapOptions'; +import {edgeGroupToColor, options} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/StolenPasswords'; import ScannedBreachedChart from 'components/report-components/ScannedBreachedChart'; -import CollapsibleWellComponent from "../report-components/CollapsibleWell"; +import CollapsibleWellComponent from 'components/report-components/CollapsibleWell'; class ReportPageComponent extends React.Component { @@ -338,13 +338,16 @@ class ReportPageComponent extends React.Component { } else { content = ( -
    +

    - Overview + Executive Summary

    - The first monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished propagation attempts. + The first monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished + propagation attempts.

    A full report of the Monkeys activities follows. @@ -358,14 +361,31 @@ class ReportPageComponent extends React.Component {

    Immediate Threats

    - During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function(x){return x===true;}).length} issues, detailed below. The security issues uncovered include: -
      - {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
    • Users with weak passwords.
    • : null} - {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
    • Stolen passwords/hashes were used to exploit other machines.
    • : null} - {this.state.report.overview.issues[this.Issue.ELASTIC] ?
    • Elastic Search servers not patched for CVE-2015-1427.
    • : null} - {this.state.report.overview.issues[this.Issue.SAMBACRY] ?
    • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
    • : null} - {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ?
    • Machines not patched for the ‘Shellshock’ (CVE-2014-6271).
    • : null} - {this.state.report.overview.issues[this.Issue.CONFICKER] ?
    • Machines not patched for the ‘Conficker’ (MS08-067).
    • : null} + During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function (x) { + return x === true; + }).length} issues, detailed below. The security issues uncovered include: +
        + {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? +
      • Users with weak passwords.
      • : null} + {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? +
      • Stolen passwords/hashes were used to exploit other machines.
      • : null} + {this.state.report.overview.issues[this.Issue.ELASTIC] ? +
      • Elastic Search servers not patched for CVE-2015-1427. +
      • : null} + {this.state.report.overview.issues[this.Issue.SAMBACRY] ? +
      • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
      • : null} + {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? +
      • Machines not patched for the ‘Shellshock’ (CVE-2014-6271). +
      • : null} + {this.state.report.overview.issues[this.Issue.CONFICKER] ? +
      • Machines not patched for the ‘Conficker’ (MS08-067).
      • : null}
    @@ -373,9 +393,13 @@ class ReportPageComponent extends React.Component { Security Issues The monkey uncovered the following possible set of issues: -
      - {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ?
    • Possible cross segment traffic. Infected machines could communicate with the Monkey Island despite crossing segment boundaries using unused ports.
    • : null} - {this.state.report.overview.warnings[this.Warning.TUNNEL] ?
    • Lack of Micro-segmentation, machines successfully tunneled monkey activity using unused ports.
    • : null} +
        + {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? +
      • Possible cross segment traffic. Infected machines could communicate with the + Monkey Island despite crossing segment boundaries using unused ports.
      • : null} + {this.state.report.overview.warnings[this.Warning.TUNNEL] ? +
      • Lack of Micro-segmentation, machines successfully tunneled monkey activity + using unused ports.
      • : null}
    @@ -394,33 +418,38 @@ class ReportPageComponent extends React.Component {

    - The Monkey discovered {this.state.report.glance.scanned.length} machines and successfully breached {this.state.report.glance.exploited.length} of them. -
    - In addition, while attempting to exploit additional hosts , security software installed in the network should have picked up the attack attempts and logged them. -
    + The Monkey discovered {this.state.report.glance.scanned.length} machines and + successfully breached {this.state.report.glance.exploited.length} of them. +
    + In addition, while attempting to exploit additional hosts , security software installed in the + network should have picked up the attack attempts and logged them. +
    Detailed recommendations in the next part of the report.

    - +

    From the attacker's point of view, the network looks like this:

    -
    - +
    +
    - +
    - +
    - +
    diff --git a/monkey_island/cc/ui/src/styles/App.css b/monkey_island/cc/ui/src/styles/App.css index 30ea8faa4..b221186d6 100644 --- a/monkey_island/cc/ui/src/styles/App.css +++ b/monkey_island/cc/ui/src/styles/App.css @@ -49,19 +49,10 @@ body { padding-left: 0px; } - ul.report { - list-style: disc; - padding-left: 40px; - } - li { overflow: auto; } - li.report { - overflow: visible; - } - li .number { color: #666; display: inline-block; @@ -76,11 +67,6 @@ body { margin: 0.1em 0; } - li a.report { - display: inline; - padding: 0em; - } - li a:hover { color: #000; background: #e9e9e9; @@ -378,3 +364,52 @@ body { padding: 15px; } } + + +/* Report page */ + +.report-page { + font-size: 1.2em; + border: 1px solid #fff; + padding: 2em; + -webkit-box-shadow: 1px 1px 7px -1px #ccc; + box-shadow: 1px 1px 7px -1px #ccc; +} + +.report-page h1 { + margin-top: 30px; +} + +.report-page h3 { + margin-top: 20px; +} + +.report-page h4 { + margin-top: 20px; +} + +.report-page ul { + list-style: disc; + padding-left: 40px; +} + +.report-page li { + overflow: visible; +} + +.report-page li a { + display: inline; + padding: 0em; +} + +/* Print report styling */ + +@media print { + .sidebar { + display: none; + } + + .pie-chart { + width: 100px; + } +} From e3bd980a123fc66e8c8e9215e46c0c9bc89c3a49 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 17:55:38 +0200 Subject: [PATCH 40/92] Replace pie-chart with progress bar --- monkey_island/cc/ui/package.json | 2 +- .../cc/ui/src/components/pages/ReportPage.js | 40 +++++++-------- .../report-components/ScannedBreachedChart.js | 49 ------------------- 3 files changed, 21 insertions(+), 70 deletions(-) delete mode 100644 monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js diff --git a/monkey_island/cc/ui/package.json b/monkey_island/cc/ui/package.json index 537982386..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,7 +81,6 @@ "react-modal-dialog": "^4.0.7", "react-redux": "^5.0.6", "react-router-dom": "^4.2.2", - "react-svg-piechart": "^1.4.0", "react-table": "^6.7.4", "react-toggle": "^4.0.1", "redux": "^3.7.2" diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 4e34dd3a8..bb45f0d5c 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -5,8 +5,8 @@ import ScannedServers from 'components/report-components/ScannedServers'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {edgeGroupToColor, options} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/StolenPasswords'; -import ScannedBreachedChart from 'components/report-components/ScannedBreachedChart'; import CollapsibleWellComponent from 'components/report-components/CollapsibleWell'; +import {Line} from 'rc-progress'; class ReportPageComponent extends React.Component { @@ -336,6 +336,9 @@ class ReportPageComponent extends React.Component { if (Object.keys(this.state.report).length === 0) { content = (

    Generating Report...

    ); } else { + let exploitPercentage = + (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; + content = (
    @@ -416,25 +419,22 @@ class ReportPageComponent extends React.Component { The Network from the Monkey's Eyes
    - -

    - The Monkey discovered {this.state.report.glance.scanned.length} machines and - successfully breached {this.state.report.glance.exploited.length} of them. -
    - In addition, while attempting to exploit additional hosts , security software installed in the - network should have picked up the attack attempts and logged them. -
    - Detailed recommendations in the next part of the report. -

    - - -
    - -
    - +

    + The Monkey discovered {this.state.report.glance.scanned.length} machines and + successfully breached {this.state.report.glance.exploited.length} of them. +
    + In addition, while attempting to exploit additional hosts , security software installed in the + network should have picked up the attack attempts and logged them. +
    + Detailed recommendations in the next part of the report. +

    +
    + + {Math.round(exploitPercentage)}% of machines exploited +

    From the attacker's point of view, the network looks like this: diff --git a/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js b/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js deleted file mode 100644 index 413a19058..000000000 --- a/monkey_island/cc/ui/src/components/report-components/ScannedBreachedChart.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import PieChart from 'react-svg-piechart' - -class ScannedBreachedChartComponent extends React.Component { - constructor() { - super(); - - this.state = { - expandedSector: null - }; - - this.handleMouseEnterOnSector = this.handleMouseEnterOnSector.bind(this); - } - - handleMouseEnterOnSector(sector) { - this.setState({expandedSector: sector}); - } - - render() { - const data = [ - {label: 'Scanned', value: this.props.scanned - this.props.exploited, color: '#f0ad4e'}, - {label: 'Exploited', value: this.props.exploited, color: '#d9534f'} - ]; - - return ( -

    - -
    - { - data.map((element, i) => ( -
    - - {element.label} : {element.value} - -
    - )) - } -
    -
    - ) - } -} - -export default ScannedBreachedChartComponent; From 5690ddc5d7d80d535d2ad91595c5f51c41ca713b Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 18:34:57 +0200 Subject: [PATCH 41/92] Add print button, improve printability --- .../cc/ui/src/components/pages/ReportPage.js | 14 +++++---- .../report-components/CollapsibleWell.js | 30 ++++++++++++------- monkey_island/cc/ui/src/styles/App.css | 8 +++++ 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index bb45f0d5c..4eb847053 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Col} from 'react-bootstrap'; +import {Button, Col} from 'react-bootstrap'; import BreachedServers from 'components/report-components/BreachedServers'; import ScannedServers from 'components/report-components/ScannedServers'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; @@ -343,8 +343,11 @@ class ReportPageComponent extends React.Component { (
    +
    + +

    - Executive Summary + Overview

    The first monkey run was started on Detailed recommendations in the next part of the report.

    -
    - + - {Math.round(exploitPercentage)}% of machines exploited + {Math.round(exploitPercentage)}% of scanned machines exploited

    diff --git a/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js b/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js index 0b92712db..6dee76601 100644 --- a/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js +++ b/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js @@ -10,18 +10,28 @@ class CollapsibleWellComponent extends React.Component { } render() { + let well = + ( + + {this.props.children} + + ); + return (

    ); } diff --git a/monkey_island/cc/ui/src/styles/App.css b/monkey_island/cc/ui/src/styles/App.css index b221186d6..8575387c6 100644 --- a/monkey_island/cc/ui/src/styles/App.css +++ b/monkey_island/cc/ui/src/styles/App.css @@ -409,6 +409,14 @@ body { display: none; } + .no-print { + display: none; + } + + .force-print { + display: block !important; + } + .pie-chart { width: 100px; } From 013e29b76b96363ed5ac4b43710b12b3e8305c86 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 28 Nov 2017 18:46:39 +0200 Subject: [PATCH 42/92] Improved style of recommendations --- .../cc/ui/src/components/pages/ReportPage.js | 67 +++++++++---------- .../report-components/CollapsibleWell.js | 6 +- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 4eb847053..48a2d1e55 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -76,7 +76,7 @@ class ReportPageComponent extends React.Component { generateSmbPasswordIssue(issue) { return ( -
    +
  • Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -87,13 +87,13 @@ class ReportPageComponent extends React.Component { The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password. -
  • + ); } generateSmbPthIssue(issue) { return ( -
    +
  • Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -104,13 +104,13 @@ class ReportPageComponent extends React.Component { The attack succeeded by using a pass-the-hash attack over SMB protocol with user {issue.username}. -
  • + ); } generateWmiPasswordIssue(issue) { return ( -
    +
  • Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -121,13 +121,13 @@ class ReportPageComponent extends React.Component { The attack succeeded by authenticating over WMI protocol with user {issue.username} and its password. -
  • + ); } generateWmiPthIssue(issue) { return ( -
    +
  • Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -138,13 +138,13 @@ class ReportPageComponent extends React.Component { The attack succeeded by using a pass-the-hash attack over WMI protocol with user {issue.username}. -
  • + ); } generateSshIssue(issue) { return ( -
    +
  • Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -155,13 +155,13 @@ class ReportPageComponent extends React.Component { The attack succeeded by authenticating over SSH protocol with user {issue.username} and its password. -
  • + ); } generateRdpIssue(issue) { return ( -
    +
  • Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network. @@ -172,13 +172,13 @@ class ReportPageComponent extends React.Component { The attack succeeded by authenticating over RDP protocol with user {issue.username} and its password. -
  • + ); } generateSambaCryIssue(issue) { return ( -
    +
  • Change {issue.username}'s password to a complex one-use password that is not shared with other computers on the network.
    @@ -192,13 +192,13 @@ class ReportPageComponent extends React.Component { className="label label-success">{issue.username} and its password, and by using the SambaCry vulnerability. -
  • + ); } generateElasticIssue(issue) { return ( -
    +
  • Update your Elastic Search server to version 1.4.3 and up. The machine {issue.machine} with the following IP address The attack succeeded because the Elastic Search server was not parched against CVE-2015-1427. -
  • + ); } generateShellshockIssue(issue) { return ( -
    +
  • Update your Bash to a ShellShock-patched version. The machine {issue.machine} with the following IP address {issue.port} was vulnerable to a shell injection attack on the paths: {this.generateShellshockPathListBadges(issue.paths)}. -
  • + ); } generateConfickerIssue(issue) { return ( -
    +
  • Install the latest Windows updates or upgrade to a newer operating system. The machine {issue.machine} with the following address -
  • + ); } generateCrossSegmentIssue(issue) { return ( -
    +
  • Segment your network. Make sure machines can't access machines from other segments. The network can probably be segmented. A monkey instance on -
  • + ); } generateTunnelIssue(issue) { return ( -
    +
  • Use micro-segmentation policies to disable communication other than the required. Machines are not locked down at port level. Network tunnel was set up from {issue.machine} to {issue.dest}. -
  • + ); } - generateIssue = (issue, index) => { + generateIssue = (issue) => { let data; switch (issue.type) { case 'smb_password': @@ -312,23 +312,22 @@ class ReportPageComponent extends React.Component { data = this.generateTunnelIssue(issue); break; } - return ( -
    -
    Recommendation #{index + 1}
    - {data} -
    - ); + return data; }; generateIssues = (issues) => { let issuesDivArray = []; for (var machine of Object.keys(issues)) { issuesDivArray.push( -

    {machine}

    +
  • +

    {machine}

    +
      + {issues[machine].map(this.generateIssue)} +
    +
  • ); - issuesDivArray.push(issues[machine].map(this.generateIssue)); } - return issuesDivArray; + return
      {issuesDivArray}
    ; }; render() { diff --git a/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js b/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js index 6dee76601..d896b6d59 100644 --- a/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js +++ b/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Collapse, Well} from 'react-bootstrap'; +import {Button, Collapse, Well} from 'react-bootstrap'; class CollapsibleWellComponent extends React.Component { constructor(props) { @@ -20,9 +20,9 @@ class CollapsibleWellComponent extends React.Component { return (
    - this.setState({open: !this.state.open})}> +
    {well} From f14dc8e2fbc6bb3f8adeb14010620e93e54ae80d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 5 Dec 2017 16:29:18 +0200 Subject: [PATCH 43/92] Add run info under overview section including zero-patients, interesting config values, and config recommendations. --- monkey_island/cc/services/report.py | 46 +++ .../cc/ui/src/components/pages/ReportPage.js | 301 +++++++++++------- 2 files changed, 238 insertions(+), 109 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 01205e71a..830197444 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,6 +1,7 @@ import ipaddress 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 @@ -260,12 +261,57 @@ class ReportService: 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']) + + @staticmethod + def get_config_passwords(): + return ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list']) + + @staticmethod + def get_config_exploits(): + 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', + } + return [exploit_display_dict[exploit] for exploit in + ConfigService.get_config_value(['exploits', 'general', 'exploiter_classes'])] + + @staticmethod + def get_config_ips(): + if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class']) != 'FixedRange': + return [] + return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed']) + + @staticmethod + def get_config_scan(): + return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan']) + @staticmethod def get_report(): return \ { 'overview': { + 'manual_monkeys': ReportService.get_manual_monkeys(), + 'config_users': ReportService.get_config_users(), + 'config_passwords': ReportService.get_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': [False, True, True, True, False, True], diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 48a2d1e55..404d2e374 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -337,127 +337,210 @@ class ReportPageComponent extends React.Component { } else { let exploitPercentage = (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; - content = ( -
    -
    -
    - -
    -

    - Overview -

    -

    - The first monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished - propagation attempts. -

    -

    - A full report of the Monkeys activities follows. -

    +
    +
    +
    -
    -

    - Security Findings -

    -
    -

    - Immediate Threats -

    - During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function (x) { - return x === true; - }).length} issues, detailed below. The security issues uncovered include: -
      - {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? -
    • Users with weak passwords.
    • : null} - {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? -
    • Stolen passwords/hashes were used to exploit other machines.
    • : null} - {this.state.report.overview.issues[this.Issue.ELASTIC] ? -
    • Elastic Search servers not patched for CVE-2015-1427. -
    • : null} - {this.state.report.overview.issues[this.Issue.SAMBACRY] ? -
    • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
    • : null} - {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? -
    • Machines not patched for the ‘Shellshock’ (CVE-2014-6271). -
    • : null} - {this.state.report.overview.issues[this.Issue.CONFICKER] ? -
    • Machines not patched for the ‘Conficker’ (MS08-067).
    • : null} -
    -
    -
    -

    - Security Issues -

    - The monkey uncovered the following possible set of issues: -
      - {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? -
    • Possible cross segment traffic. Infected machines could communicate with the - Monkey Island despite crossing segment boundaries using unused ports.
    • : null} - {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
    • Lack of Micro-segmentation, machines successfully tunneled monkey activity - using unused ports.
    • : null} -
    -
    -
    -
    -

    - Recommendations -

    -
    - {this.generateIssues(this.state.report.recommendations.issues)} -
    -
    -
    -

    - The Network from the Monkey's Eyes -

    -
    -

    - The Monkey discovered {this.state.report.glance.scanned.length} machines and - successfully breached {this.state.report.glance.exploited.length} of them. -
    - In addition, while attempting to exploit additional hosts , security software installed in the - network should have picked up the attack attempts and logged them. -
    - Detailed recommendations in the next part of the report. +

    +
    +

    + Overview +

    + { + this.state.report.glance.exploited.length > 0 ? + (

    + + Critical security issues found by Infection Monkey! +

    ) : + (

    + + Infection Monkey did not find any critical security issues. +

    ) + } +

    + + To improve the monkey's success rate, try adding users and passwords, and enabling the "Local + network scan" config value under "Basic - Network"

    -
    - - {Math.round(exploitPercentage)}% of scanned machines exploited +

    + The first monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished + propagation attempts. +

    +

    + The monkey started propagating from the following machines where it was manually installed: +

      + {this.state.report.overview.manual_monkeys.map(x =>
    • {x}
    • )} +
    +

    +

    + The monkeys were run with the following configuration: +

    + { + this.state.report.overview.config_users.length > 0 ? +

    + Users to try: +

      + {this.state.report.overview.config_users.map(x =>
    • {x}
    • )} +
    + Passwords to try: +
      + {this.state.report.overview.config_passwords.map(x =>
    • {x.substr(0, 3) + '******'}
    • )} +
    +

    + : +

    + No Users and Passwords were provided for the monkey. +

    + } + { + this.state.report.overview.config_exploits.length > 0 ? +

    + Use the following exploit methods: +

      + {this.state.report.overview.config_exploits.map(x =>
    • {x}
    • )} +
    +

    + : +

    + Don't use any exploit. +

    + } + { + this.state.report.overview.config_ips.length > 0 ? +

    + Scan the following IPs: +

      + {this.state.report.overview.config_ips.map(x =>
    • {x}
    • )} +
    +

    + : + '' + } + { + this.state.report.overview.config_scan ? + '' + : +

    + Monkeys were configured to not scan local network +

    + } +

    + A full report of the Monkeys activities follows. +

    +
    +
    +

    + Security Findings +

    +
    +

    + Immediate Threats +

    + During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function (x) { + return x === true; + }).length} issues, detailed below. The security issues uncovered include: +
      + {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? +
    • Users with weak passwords.
    • : null} + {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? +
    • Stolen passwords/hashes were used to exploit other machines.
    • : null} + {this.state.report.overview.issues[this.Issue.ELASTIC] ? +
    • Elastic Search servers not patched for CVE-2015-1427. +
    • : null} + {this.state.report.overview.issues[this.Issue.SAMBACRY] ? +
    • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
    • : null} + {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? +
    • Machines not patched for the ‘Shellshock’ (CVE-2014-6271). +
    • : null} + {this.state.report.overview.issues[this.Issue.CONFICKER] ? +
    • Machines not patched for the ‘Conficker’ (MS08-067).
    • : null} +
    +
    +
    +

    + Security Issues +

    + The monkey uncovered the following possible set of issues: +
      + {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? +
    • Possible cross segment traffic. Infected machines could communicate with the + Monkey Island despite crossing segment boundaries using unused ports.
    • : null} + {this.state.report.overview.warnings[this.Warning.TUNNEL] ? +
    • Lack of Micro-segmentation, machines successfully tunneled monkey activity + using unused ports.
    • : null} +
    -

    - From the attacker's point of view, the network looks like this: -

    -
    - +
    +

    + Recommendations +

    +
    + {this.generateIssues(this.state.report.recommendations.issues)} +
    -
    - -
    -
    - -
    -
    - +
    +

    + The Network from the Monkey's Eyes +

    +
    +

    + The Monkey discovered {this.state.report.glance.scanned.length} machines and + successfully breached {this.state.report.glance.exploited.length} of them. +
    + In addition, while attempting to exploit additional hosts , security software installed in the + network should have picked up the attack attempts and logged them. +
    + Detailed recommendations in the next part of the report. +

    +
    + + {Math.round(exploitPercentage)}% of scanned machines exploited +
    +
    +

    + From the attacker's point of view, the network looks like this: +

    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    ); } + return (

    4. Security Report

    From 8bc9e3a65f50016b10e1a98c0a66ba99fdcd4cc6 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 5 Dec 2017 17:01:47 +0200 Subject: [PATCH 44/92] Add warning message if watching report while monkeys are running --- .../cc/ui/src/components/pages/ReportPage.js | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 404d2e374..e29d9388f 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -30,16 +30,29 @@ class ReportPageComponent extends React.Component { super(props); this.state = { report: {}, - graph: {nodes: [], edges: []} + graph: {nodes: [], edges: []}, + allMonkeysAreDead: false }; } componentDidMount() { this.getReportFromServer(); this.updateMapFromServer(); + this.updateMonkeysRunning(); this.interval = setInterval(this.updateMapFromServer, 1000); } + updateMonkeysRunning = () => { + fetch('/api') + .then(res => res.json()) + .then(res => { + // This check is used to prevent unnecessary re-rendering + this.setState({ + allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']) + }); + }); + }; + componentWillUnmount() { clearInterval(this.interval); } @@ -361,8 +374,18 @@ class ReportPageComponent extends React.Component { Infection Monkey did not find any critical security issues.

    ) } + { + this.state.allMonkeysAreDead ? + '' + : + (

    + + Some monkeys are still running. To get the best report it's best to wait for all of them to finish + running. +

    ) + }

    - + To improve the monkey's success rate, try adding users and passwords, and enabling the "Local network scan" config value under "Basic - Network"

    From eadf8d0eba70aed0122903e02348f7ff5d31cbfa Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 10:33:13 +0200 Subject: [PATCH 45/92] Add support for both supplying monkey island dir or using current directory as monkey island dir Fix openssl.cnf not found --- monkey_island/windows/create_certificate.bat | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/monkey_island/windows/create_certificate.bat b/monkey_island/windows/create_certificate.bat index ac6555f0b..18738726b 100644 --- a/monkey_island/windows/create_certificate.bat +++ b/monkey_island/windows/create_certificate.bat @@ -1,3 +1,19 @@ -bin\openssl\openssl.exe genrsa -out cc\server.key 1024 -bin\openssl\openssl.exe req -new -config bin\openssl\openssl.cfg -key cc\server.key -out cc\server.csr -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" -bin\openssl\openssl.exe x509 -req -days 366 -in cc\server.csr -signkey cc\server.key -out cc\server.crt \ No newline at end of file +@echo off + +SET OPENSSL_CONF=bin\openssl\openssl.cfg + +IF [%1] == [] ( + set dir=%cd%\ +) ELSE ( + set dir=%1 + REM - Remove double quotes - + set dir=%dir:"=% +) + +echo Monkey Island folder: %dir% + +@echo on + +"%dir%bin\openssl\openssl.exe" genrsa -out "%dir%cc\server.key" 1024 +"%dir%bin\openssl\openssl.exe" req -new -config "%dir%bin\openssl\openssl.cfg" -key "%dir%cc\server.key" -out "%dir%cc\server.csr" -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" +"%dir%bin\openssl\openssl.exe" x509 -req -days 366 -in "%dir%cc\server.csr" -signkey "%dir%cc\server.key" -out "%dir%cc\server.crt" \ No newline at end of file From 1f2a0c0e21889f967c7eb465b081e65cc4aee8bd Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 11:34:51 +0200 Subject: [PATCH 46/92] add openssl.cfg to git and update path to it on create_certificate.bat --- monkey_island/windows/create_certificate.bat | 2 +- monkey_island/windows/openssl.cfg | 350 +++++++++++++++++++ 2 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 monkey_island/windows/openssl.cfg diff --git a/monkey_island/windows/create_certificate.bat b/monkey_island/windows/create_certificate.bat index 18738726b..cd1aaaa58 100644 --- a/monkey_island/windows/create_certificate.bat +++ b/monkey_island/windows/create_certificate.bat @@ -1,6 +1,6 @@ @echo off -SET OPENSSL_CONF=bin\openssl\openssl.cfg +SET OPENSSL_CONF=windows\openssl.cfg IF [%1] == [] ( set dir=%cd%\ diff --git a/monkey_island/windows/openssl.cfg b/monkey_island/windows/openssl.cfg new file mode 100644 index 000000000..1eb86c401 --- /dev/null +++ b/monkey_island/windows/openssl.cfg @@ -0,0 +1,350 @@ +# +# OpenSSL example configuration file. +# This is mostly being used for generation of certificate requests. +# + +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = . +RANDFILE = $ENV::HOME/.rnd + +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +# extensions = +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] + +# We can add new OIDs in here for use by 'ca', 'req' and 'ts'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +# Policies used by the TSA examples. +tsa_policy1 = 1.2.3.4.1 +tsa_policy2 = 1.2.3.4.5.6 +tsa_policy3 = 1.2.3.4.5.7 + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_default ] + +dir = ./demoCA # Where everything is kept +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +#unique_subject = no # Set to 'no' to allow creation of + # several ctificates with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = $dir/cacert.pem # The CA certificate +serial = $dir/serial # The current serial number +crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = $dir/private/cakey.pem# The private key +RANDFILE = $dir/private/.rand # private random number file + +x509_extensions = usr_cert # The extentions to add to the cert + +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options + +# Extension copying option: use with caution. +# copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 365 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = default # use public key default MD +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match + +# For the CA policy +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extentions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString (PKIX recommendation before 2004) +# utf8only: only UTF8Strings (PKIX recommendation after 2004). +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings. +string_mask = utf8only + +# req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = AU +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Some-State + +localityName = Locality Name (eg, city) + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Internet Widgits Pty Ltd + +# we can do this but it is not needed normally :-) +#1.organizationName = Second Organization Name (eg, company) +#1.organizationName_default = World Wide Web Pty Ltd + +organizationalUnitName = Organizational Unit Name (eg, section) +#organizationalUnitName_default = + +commonName = Common Name (e.g. server FQDN or YOUR name) +commonName_max = 64 + +emailAddress = Email Address +emailAddress_max = 64 + +# SET-ex3 = SET extension number 3 + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This is required for TSA certificates. +# extendedKeyUsage = critical,timeStamping + +[ v3_req ] + +# Extensions to add to a certificate request + +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +[ v3_ca ] + + +# Extensions for a typical CA + + +# PKIX recommendation. + +subjectKeyIdentifier=hash + +authorityKeyIdentifier=keyid:always,issuer + +# This is what PKIX recommends but some broken software chokes on critical +# extensions. +#basicConstraints = critical,CA:true +# So we do this instead. +basicConstraints = CA:true + +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign + +# Some might want this also +# nsCertType = sslCA, emailCA + +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy + +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always + +[ proxy_cert_ext ] +# These extensions should be added when creating a proxy certificate + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This really needs to be in place for it to be a proxy certificate. +proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo + +#################################################################### +[ tsa ] + +default_tsa = tsa_config1 # the default TSA section + +[ tsa_config1 ] + +# These are used by the TSA reply generation only. +dir = ./demoCA # TSA root directory +serial = $dir/tsaserial # The current serial number (mandatory) +crypto_device = builtin # OpenSSL engine to use for signing +signer_cert = $dir/tsacert.pem # The TSA signing certificate + # (optional) +certs = $dir/cacert.pem # Certificate chain to include in reply + # (optional) +signer_key = $dir/private/tsakey.pem # The TSA private key (optional) + +default_policy = tsa_policy1 # Policy if request did not specify it + # (optional) +other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional) +digests = md5, sha1 # Acceptable message digests (mandatory) +accuracy = secs:1, millisecs:500, microsecs:100 # (optional) +clock_precision_digits = 0 # number of digits after dot. (optional) +ordering = yes # Is ordering defined for timestamps? + # (optional, default: no) +tsa_name = yes # Must the TSA name be included in the reply? + # (optional, default: no) +ess_cert_id_chain = no # Must the ESS cert id chain be included? + # (optional, default: no) From c8e553721f7972c4de1abdce01a52baf7a87396d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 12:07:37 +0200 Subject: [PATCH 47/92] Report content fix --- monkey_island/cc/services/report.py | 2 +- .../cc/ui/src/components/pages/ReportPage.js | 16 ++++++++-------- .../report-components/StolenPasswords.js | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 830197444..048e2fb12 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -89,7 +89,7 @@ class ReportService: @staticmethod def get_stolen_creds(): - PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM', 'ntlm_hash': 'NTLM'} + 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}}, diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index e29d9388f..f1daa6d3c 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -367,11 +367,11 @@ class ReportPageComponent extends React.Component { this.state.report.glance.exploited.length > 0 ? (

    - Critical security issues found by Infection Monkey! + Critical security issues were detected!

    ) : (

    - Infection Monkey did not find any critical security issues. + No critical security issues were detected.

    ) } { @@ -386,8 +386,8 @@ class ReportPageComponent extends React.Component { }

    - To improve the monkey's success rate, try adding users and passwords, and enabling the "Local - network scan" config value under "Basic - Network" + To improve the monkey's detection rates, try adding users and passwords and enable the "Local network + scan" config value under Basic - Network.

    The first monkey run was started on 0 ?

    - Use the following exploit methods: + Used the following exploit methods:

      {this.state.report.overview.config_exploits.map(x =>
    • {x}
    • )}
    @@ -450,7 +450,7 @@ class ReportPageComponent extends React.Component { '' :

    - Monkeys were configured to not scan local network + Monkeys were configured to avoid scanning of the local network.

    }

    @@ -468,12 +468,12 @@ class ReportPageComponent extends React.Component { During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function (x) { return x === true; - }).length} issues, detailed below. The security issues uncovered include: + }).length} issues:

      {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
    • Users with weak passwords.
    • : null} {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? -
    • Stolen passwords/hashes were used to exploit other machines.
    • : null} +
    • Stolen credentials were used to exploit other machines.
    • : null} {this.state.report.overview.issues[this.Issue.ELASTIC] ?
    • Elastic Search servers not patched for CVE-2015-1427. diff --git a/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js index 7c42b6ea5..fde46f85a 100644 --- a/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js +++ b/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js @@ -7,7 +7,7 @@ const columns = [ columns: [ { Header: 'Username', accessor: 'username'}, { Header: 'Type', accessor: 'type'}, - { Header: 'Origin', accessor: 'origin'} + { Header: 'Stolen From', accessor: 'origin'} ] } ]; From 2c8b510b0c16e42ab73b8f31e25c8c2dbf38ac86 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 12:45:53 +0200 Subject: [PATCH 48/92] Exploits used are listed only if they're not the default configuration. Suggestion to improve monkey success rate appears only if no critical issues were found --- monkey_island/cc/services/report.py | 11 ++++- .../cc/ui/src/components/pages/ReportPage.js | 45 ++++++++++++++----- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 048e2fb12..6b0e408ce 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -276,6 +276,15 @@ class ReportService: @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) + + if exploits == default_exploits: + return ['default'] + exploit_display_dict = \ { 'SmbExploiter': 'SMB Exploiter', @@ -288,7 +297,7 @@ class ReportService: 'ShellShockExploiter': 'ShellShock Exploiter', } return [exploit_display_dict[exploit] for exploit in - ConfigService.get_config_value(['exploits', 'general', 'exploiter_classes'])] + exploits] @staticmethod def get_config_ips(): diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index f1daa6d3c..2a12587f6 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -330,7 +330,7 @@ class ReportPageComponent extends React.Component { generateIssues = (issues) => { let issuesDivArray = []; - for (var machine of Object.keys(issues)) { + for (let machine of Object.keys(issues)) { issuesDivArray.push(
    • {machine}

      @@ -343,6 +343,17 @@ class ReportPageComponent extends React.Component { return
        {issuesDivArray}
      ; }; + didMonkeyFindIssues = () => { + for (let issue of Object.keys(this.state.report.overview.issues)) { + if (this.state.report.overview.issues[issue]) { + return true; + } + } + return false; + }; + + + render() { let content; if (Object.keys(this.state.report).length === 0) { @@ -384,11 +395,16 @@ class ReportPageComponent extends React.Component { running.

      ) } -

      - - To improve the monkey's detection rates, try adding users and passwords and enable the "Local network - scan" config value under Basic - Network. -

      + { + this.didMonkeyFindIssues() ? + '' + : +

      + + To improve the monkey's detection rates, try adding users and passwords and enable the "Local network + scan" config value under Basic - Network. +

      + }

      The first monkey run was started on {this.state.report.overview.monkey_start_time}. After 0 ? -

      - Used the following exploit methods: -

        - {this.state.report.overview.config_exploits.map(x =>
      • {x}
      • )} -
      -

      + ( + this.state.report.overview.config_exploits[0] === 'default' ? + '' + : +

      + Used the following exploit methods: +

        + {this.state.report.overview.config_exploits.map(x =>
      • {x}
      • )} +
      +

      + ) :

      Don't use any exploit. From d8aff72da072b0ffc96e2b924e1d236a50b4e4fa Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 13:42:08 +0200 Subject: [PATCH 49/92] Exploits in breached servers are now readable --- monkey_island/cc/services/report.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 6b0e408ce..fceaa086c 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -13,6 +13,18 @@ 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', + } + @staticmethod def get_first_monkey_time(): return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', 1)]).limit(1)[0]['timestamp'] @@ -81,7 +93,9 @@ class ReportService: { 'label': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(monkey['id'])), 'ip_addresses': monkey['ip_addresses'], - 'exploits': list(set([exploit['exploiter'] for exploit in monkey['exploits'] if exploit['result']])) + 'exploits': list(set( + [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in monkey['exploits'] if + exploit['result']])) } for monkey in exploited] @@ -285,18 +299,7 @@ class ReportService: if exploits == default_exploits: return ['default'] - 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', - } - return [exploit_display_dict[exploit] for exploit in + return [ReportService.EXPLOIT_DISPLAY_DICT[exploit] for exploit in exploits] @staticmethod From f2e464f2a6e4062d8b859f5072f05b4dba4ce983 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 15:42:24 +0200 Subject: [PATCH 50/92] Report now uses initial config when makes sense --- monkey_island/cc/resources/monkey.py | 2 ++ monkey_island/cc/services/config.py | 22 ++++++++++++++++------ monkey_island/cc/services/report.py | 12 ++++++------ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/monkey_island/cc/resources/monkey.py b/monkey_island/cc/resources/monkey.py index 8670505b0..37722262c 100644 --- a/monkey_island/cc/resources/monkey.py +++ b/monkey_island/cc/resources/monkey.py @@ -62,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: diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 2145011c3..ea755312f 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -800,23 +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_config_value(config_key_as_arr): + 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': 'newconfig'}, {config_key: 1}) + 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(): - config_json = ConfigService.get_config() + 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]: @@ -888,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/report.py b/monkey_island/cc/services/report.py index fceaa086c..8261b7e7b 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -282,11 +282,11 @@ class ReportService: @staticmethod def get_config_users(): - return ConfigService.get_config_value(['basic', 'credentials', 'exploit_user_list']) + 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']) + return ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list'], True) @staticmethod def get_config_exploits(): @@ -294,7 +294,7 @@ class ReportService: 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) + exploits = ConfigService.get_config_value(exploits_config_value, True) if exploits == default_exploits: return ['default'] @@ -304,13 +304,13 @@ class ReportService: @staticmethod def get_config_ips(): - if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class']) != 'FixedRange': + 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']) + 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']) + return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True) @staticmethod def get_report(): From 09e04a376331392f4fa0faadd7e0a3e491c41bba Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 15:43:47 +0200 Subject: [PATCH 51/92] Fixed condition for showing suggestion to improve monkey success rate --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 2a12587f6..82787f5f0 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -396,12 +396,13 @@ class ReportPageComponent extends React.Component {

      ) } { - this.didMonkeyFindIssues() ? + this.state.report.glance.exploited.length > 0 ? '' :

      - To improve the monkey's detection rates, try adding users and passwords and enable the "Local network + To improve the monkey's detection rates, try adding users and passwords and enable the "Local + network scan" config value under Basic - Network.

      } From 483394d7f512d3321fa7b17f09678385550963b8 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 15:44:38 +0200 Subject: [PATCH 52/92] Report shows message if no monkeys have been run before --- .../cc/ui/src/components/pages/ReportPage.js | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 82787f5f0..a058ce516 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -31,25 +31,27 @@ class ReportPageComponent extends React.Component { this.state = { report: {}, graph: {nodes: [], edges: []}, - allMonkeysAreDead: false + allMonkeysAreDead: false, + runStarted: true }; } componentDidMount() { - this.getReportFromServer(); + this.updateMonkeysRunning().then(res => this.getReportFromServer(res)); this.updateMapFromServer(); - this.updateMonkeysRunning(); this.interval = setInterval(this.updateMapFromServer, 1000); } updateMonkeysRunning = () => { - fetch('/api') + return fetch('/api') .then(res => res.json()) .then(res => { // This check is used to prevent unnecessary re-rendering this.setState({ - allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']) + allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']), + runStarted: res['completed_steps']['run_monkey'] }); + return res; }); }; @@ -69,14 +71,16 @@ class ReportPageComponent extends React.Component { }); }; - getReportFromServer() { - fetch('/api/report') - .then(res => res.json()) - .then(res => { - this.setState({ - report: res + getReportFromServer(res) { + if (res['completed_steps']['run_monkey']) { + fetch('/api/report') + .then(res => res.json()) + .then(res => { + this.setState({ + report: res + }); }); - }); + } } generateInfoBadges(data_array) { @@ -353,11 +357,18 @@ class ReportPageComponent extends React.Component { }; - render() { let content; if (Object.keys(this.state.report).length === 0) { - content = (

      Generating Report...

      ); + if (this.state.runStarted) { + content = (

      Generating Report...

      ); + } else { + content = +

      + + You have to run a monkey before generating a report! +

      ; + } } else { let exploitPercentage = (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; From f2b631745d34f389a52c5b0702b0c7e38a4cf54e Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 15:45:32 +0200 Subject: [PATCH 53/92] Fix bug where stolen credentials had '.' in username --- monkey_island/cc/resources/telemetry.py | 14 ++++++++++++-- monkey_island/cc/services/report.py | 15 +++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index 666bfc16c..7b3a6e616 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -39,7 +39,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 +52,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 +70,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 @@ -159,7 +164,6 @@ class Telemetry(flask_restful.Resource): creds = telemetry_json['data']['credentials'] for user in creds: ConfigService.creds_add_username(user) - creds[user]['user'] = user if 'password' in creds[user]: ConfigService.creds_add_password(creds[user]['password']) if 'lm_hash' in creds[user]: @@ -167,11 +171,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/services/report.py b/monkey_island/cc/services/report.py index 8261b7e7b..ab6c9fb13 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -117,7 +117,7 @@ class ReportService: for pass_type in monkey_creds[user]: creds.append( { - 'username': user, + 'username': user.replace(',', '.'), 'type': PASS_TYPE_DICT[pass_type], 'origin': origin } @@ -231,14 +231,17 @@ class ReportService: @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 - mongo.db.telemetry.find_one( - {'telem_type': 'system_info_collection', 'monkey_guid': monkey_guid}, - {'data.network_info.networks': 1} - )['data']['network_info']['networks'] + for network in network_info['data']['network_info']['networks'] ] @staticmethod From 434c72f69f808bf9f50e2988644762a05667ef91 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 16:33:16 +0200 Subject: [PATCH 54/92] Implemented issues and warnings on overview --- monkey_island/cc/services/report.py | 68 ++++++++++++-- .../cc/ui/src/components/pages/ReportPage.js | 94 ++++++++++++------- 2 files changed, 119 insertions(+), 43 deletions(-) diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index ab6c9fb13..4b3bf8573 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -25,6 +25,18 @@ class ReportService: 'ShellShockExploiter': 'ShellShock Exploiter', } + class ISSUES_DICT: + WEAK_PASSWORD = 0 + STOLEN_CREDS = 1 + ELASTIC = 2 + SAMBACRY = 3 + SHELLSHOCK = 4 + CONFICKER = 5 + + class WARNINGS_DICT: + 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'] @@ -139,6 +151,7 @@ class ReportService: 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 @@ -232,9 +245,9 @@ class ReportService: @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} - ) + {'telem_type': 'system_info_collection', 'monkey_guid': monkey_guid}, + {'data.network_info.networks': 1} + ) if network_info is None: return [] @@ -315,22 +328,61 @@ class ReportService: 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] = True + elif issue['type'] == 'sambacry': + issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY] = True + elif issue['type'] == 'shellshock': + issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK] = True + elif issue['type'] == 'conficker': + issues_byte_array[ReportService.ISSUES_DICT.CONFICKER] = 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] = True + elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'): + issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS] = 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] = True + elif issue['type'] == 'tunnel': + warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL] = True + + return warnings_byte_array + @staticmethod def get_report(): + issues = ReportService.get_issues() + config_users = ReportService.get_config_users() + config_passwords = ReportService.get_config_passwords() + return \ { 'overview': { 'manual_monkeys': ReportService.get_manual_monkeys(), - 'config_users': ReportService.get_config_users(), - 'config_passwords': ReportService.get_config_passwords(), + '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': [False, True, True, True, False, True], - 'warnings': [True, True] + 'issues': ReportService.get_issues_overview(issues, config_users, config_passwords), + 'warnings': ReportService.get_warnings_overview(issues) }, 'glance': { @@ -340,7 +392,7 @@ class ReportService: }, 'recommendations': { - 'issues': ReportService.get_issues() + 'issues': issues } } diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index a058ce516..e5e1fd3af 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -498,46 +498,70 @@ class ReportPageComponent extends React.Component {

      Immediate Threats

      - During this simulated attack the Monkey uncovered {this.state.report.overview.issues.filter(function (x) { - return x === true; - }).length} issues: -
        - {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? -
      • Users with weak passwords.
      • : null} - {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? -
      • Stolen credentials were used to exploit other machines.
      • : null} - {this.state.report.overview.issues[this.Issue.ELASTIC] ? -
      • Elastic Search servers not patched for CVE-2015-1427. -
      • : null} - {this.state.report.overview.issues[this.Issue.SAMBACRY] ? -
      • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
      • : null} - {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? -
      • Machines not patched for the ‘Shellshock’ (CVE-2014-6271). -
      • : null} - {this.state.report.overview.issues[this.Issue.CONFICKER] ? -
      • Machines not patched for the ‘Conficker’ (MS08-067).
      • : null} -
      + { + this.state.report.overview.issues.filter(function (x) { + return x === true; + }).length > 0 ? +
      + During this simulated attack the Monkey uncovered + {this.state.report.overview.issues.filter(function (x) { + return x === true; + }).length} issues: +
        + {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? +
      • Users with passwords supplied in config.
      • : null} + {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? +
      • Stolen credentials were used to exploit other machines.
      • : null} + {this.state.report.overview.issues[this.Issue.ELASTIC] ? +
      • Elastic Search servers not patched for CVE-2015-1427. +
      • : null} + {this.state.report.overview.issues[this.Issue.SAMBACRY] ? +
      • Samba servers not patched for ‘SambaCry’ (CVE-2017-7494).
      • : null} + {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? +
      • Machines not patched for the ‘Shellshock’ (CVE-2014-6271). +
      • : null} + {this.state.report.overview.issues[this.Issue.CONFICKER] ? +
      • Machines not patched for the ‘Conficker’ (MS08-067).
      • : null} +
      +
      + : +
      + During this simulated attack the Monkey uncovered 0 issues. +
      + }

    Security Issues

    - The monkey uncovered the following possible set of issues: -
      - {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? -
    • Possible cross segment traffic. Infected machines could communicate with the - Monkey Island despite crossing segment boundaries using unused ports.
    • : null} - {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
    • Lack of Micro-segmentation, machines successfully tunneled monkey activity - using unused ports.
    • : null} -
    + { + this.state.report.overview.warnings.filter(function (x) { + return x === true; + }).length > 0 ? +
    + The monkey uncovered the following possible set of issues: +
      + {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? +
    • Possible cross segment traffic. Infected machines could communicate with the + Monkey Island despite crossing segment boundaries using unused ports.
    • : null} + {this.state.report.overview.warnings[this.Warning.TUNNEL] ? +
    • Lack of Micro-segmentation, machines successfully tunneled monkey activity + using unused ports.
    • : null} +
    +
    + : +
    + The monkey did not find any issues. +
    + }
    From 8ed439e24ed3ec7563a4acc61458d25775f94016 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 12 Dec 2017 16:39:38 +0200 Subject: [PATCH 55/92] Remove irrelevant sentence+link --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index e5e1fd3af..2d3022adc 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -585,8 +585,6 @@ class ReportPageComponent extends React.Component {
    In addition, while attempting to exploit additional hosts , security software installed in the network should have picked up the attack attempts and logged them. -
    - Detailed recommendations in the next part of the report.

    Date: Tue, 12 Dec 2017 17:05:57 +0200 Subject: [PATCH 56/92] Show minimal info on services. Make optimization for machine label on report --- monkey_island/cc/services/edge.py | 19 +++++++++++-------- monkey_island/cc/services/node.py | 18 ++++++++++-------- monkey_island/cc/services/report.py | 17 ++++++++--------- 3 files changed, 29 insertions(+), 25 deletions(-) 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 21c2ef194..a6ce09438 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: @@ -119,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) } diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 4b3bf8573..ac445ac6a 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -76,17 +76,16 @@ class ReportService: @staticmethod def get_scanned(): nodes = \ - [NodeService.get_displayed_node_by_id(node['_id']) for node in mongo.db.node.find({}, {'_id': 1})] \ - + [NodeService.get_displayed_node_by_id(monkey['_id']) for monkey in mongo.db.monkey.find({}, {'_id': 1})] + [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['hostname'] if 'hostname' in node else NodeService.get_node_by_id(node['id'])['os']['version'], + 'label': node['label'], 'ip_addresses': node['ip_addresses'], 'accessible_from_nodes': (x['hostname'] for x in - (NodeService.get_displayed_node_by_id(edge['from']) - for edge in EdgeService.get_displayed_edges_by_to(node['id']))), + (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] @@ -96,14 +95,14 @@ class ReportService: @staticmethod def get_exploited(): exploited = \ - [NodeService.get_displayed_node_by_id(monkey['_id']) for monkey in mongo.db.monkey.find({}, {'_id': 1}) + [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']) + + [NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({'exploited': True}, {'_id': 1})] exploited = [ { - 'label': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(monkey['id'])), + 'label': monkey['label'], 'ip_addresses': monkey['ip_addresses'], 'exploits': list(set( [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in monkey['exploits'] if From b0547c4f7a4c38abe80c1b55846f84018cbd8419 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 19 Dec 2017 17:58:07 +0200 Subject: [PATCH 57/92] Add legend to report map --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 2d3022adc..1ef8b2f30 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -596,6 +596,16 @@ class ReportPageComponent extends React.Component {

    From the attacker's point of view, the network looks like this:

    +
    + Legend: + Exploit + | + Scan + | + Tunnel + | + Island Communication +
    From 6ddb1177238f1e6cbcabf26157f677e84c931780 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 19 Dec 2017 17:58:21 +0200 Subject: [PATCH 58/92] Minor content fix --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 1ef8b2f30..50e0921cf 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -553,7 +553,7 @@ class ReportPageComponent extends React.Component {
  • Possible cross segment traffic. Infected machines could communicate with the Monkey Island despite crossing segment boundaries using unused ports.
  • : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
  • Lack of Micro-segmentation, machines successfully tunneled monkey activity +
  • Lack of machine hardening, machines successfully tunneled monkey traffic using unused ports.
  • : null}
    From 1f571503ade12e6c1ffb8a08d2150dc3a396bacd Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 19 Dec 2017 18:02:43 +0200 Subject: [PATCH 59/92] Fix bug in create_certificate.bat --- monkey_island/windows/create_certificate.bat | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/monkey_island/windows/create_certificate.bat b/monkey_island/windows/create_certificate.bat index cd1aaaa58..0af3e9960 100644 --- a/monkey_island/windows/create_certificate.bat +++ b/monkey_island/windows/create_certificate.bat @@ -1,19 +1,18 @@ @echo off -SET OPENSSL_CONF=windows\openssl.cfg - IF [%1] == [] ( - set dir=%cd%\ + set mydir=%cd%\ ) ELSE ( - set dir=%1 - REM - Remove double quotes - - set dir=%dir:"=% + set mydir=%~1% ) -echo Monkey Island folder: %dir% +echo Monkey Island folder: %mydir% + +SET OPENSSL_CONF=%mydir%bin\openssl\openssl.cfg +copy "%mydir%windows\openssl.cfg" "%mydir%bin\openssl\openssl.cfg" @echo on -"%dir%bin\openssl\openssl.exe" genrsa -out "%dir%cc\server.key" 1024 -"%dir%bin\openssl\openssl.exe" req -new -config "%dir%bin\openssl\openssl.cfg" -key "%dir%cc\server.key" -out "%dir%cc\server.csr" -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" -"%dir%bin\openssl\openssl.exe" x509 -req -days 366 -in "%dir%cc\server.csr" -signkey "%dir%cc\server.key" -out "%dir%cc\server.crt" \ No newline at end of file +"%mydir%bin\openssl\openssl.exe" genrsa -out "%mydir%cc\server.key" 1024 +"%mydir%bin\openssl\openssl.exe" req -new -config "%mydir%bin\openssl\openssl.cfg" -key "%mydir%cc\server.key" -out "%mydir%cc\server.csr" -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" +"%mydir%bin\openssl\openssl.exe" x509 -req -days 366 -in "%mydir%cc\server.csr" -signkey "%mydir%cc\server.key" -out "%mydir%cc\server.crt" \ No newline at end of file From c4f9f774afdf90e53a16213ecd1d8ecf27d42368 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 19 Dec 2017 18:43:20 +0200 Subject: [PATCH 60/92] Fix bug running from path with spaces --- monkey_island/cc/resources/local_run.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey_island/cc/resources/local_run.py b/monkey_island/cc/resources/local_run.py index 2fe7be3d1..3f11eb2a4 100644 --- a/monkey_island/cc/resources/local_run.py +++ b/monkey_island/cc/resources/local_run.py @@ -36,9 +36,11 @@ def run_local_monkey(): # run the monkey try: - args = ["%s m0nk3y -s %s:%s" % (target_path, local_ip_addresses()[0], ISLAND_PORT)] if sys.platform == "win32": + args = ['"%s" m0nk3y -s %s:%s' % (target_path, local_ip_addresses()[0], ISLAND_PORT)] args = "".join(args) + else: + args = ['%s m0nk3y -s %s:%s' % (target_path, local_ip_addresses()[0], ISLAND_PORT)] pid = subprocess.Popen(args, shell=True).pid except Exception as exc: return False, "popen failed: %s" % exc From 0c286a3419b91556395406869e684acd8cc759c8 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 19 Dec 2017 19:02:10 +0200 Subject: [PATCH 61/92] Remove statement if there were no infections --- .../cc/ui/src/components/pages/ReportPage.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 50e0921cf..d9f3255e2 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -582,10 +582,16 @@ class ReportPageComponent extends React.Component { className="label label-warning">{this.state.report.glance.scanned.length} machines and successfully breached {this.state.report.glance.exploited.length} of them. -
    - In addition, while attempting to exploit additional hosts , security software installed in the - network should have picked up the attack attempts and logged them.

    + { + this.state.report.glance.exploited.length > 0 ? +

    + In addition, while attempting to exploit additional hosts , security software installed in the + network should have picked up the attack attempts and logged them. +

    + : + '' + }
    Date: Tue, 19 Dec 2017 20:35:06 +0200 Subject: [PATCH 62/92] Remove unecessary paragraph --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index d9f3255e2..f85698f31 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -486,9 +486,6 @@ class ReportPageComponent extends React.Component { Monkeys were configured to avoid scanning of the local network.

    } -

    - A full report of the Monkeys activities follows. -

    From 002394f887b0892d74dda09e448fa987ee81a069 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Wed, 20 Dec 2017 18:15:27 +0200 Subject: [PATCH 63/92] Add shortcut batches --- monkey_island/windows/copyShortcutOnDesktop.bat | 1 + monkey_island/windows/removeShortcutFromDesktop.bat | 1 + 2 files changed, 2 insertions(+) create mode 100644 monkey_island/windows/copyShortcutOnDesktop.bat create mode 100644 monkey_island/windows/removeShortcutFromDesktop.bat diff --git a/monkey_island/windows/copyShortcutOnDesktop.bat b/monkey_island/windows/copyShortcutOnDesktop.bat new file mode 100644 index 000000000..caa48f91a --- /dev/null +++ b/monkey_island/windows/copyShortcutOnDesktop.bat @@ -0,0 +1 @@ +xcopy %1 %2 diff --git a/monkey_island/windows/removeShortcutFromDesktop.bat b/monkey_island/windows/removeShortcutFromDesktop.bat new file mode 100644 index 000000000..a1fede451 --- /dev/null +++ b/monkey_island/windows/removeShortcutFromDesktop.bat @@ -0,0 +1 @@ +del %1 From 15b4a8778be5edb0c1faff9384345aaef038cac0 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 24 Dec 2017 11:23:57 +0200 Subject: [PATCH 64/92] Add V after generating report --- monkey_island/cc/resources/root.py | 7 +++++-- monkey_island/cc/services/node.py | 4 ++++ monkey_island/cc/services/report.py | 22 +++++++++++++++++++++- monkey_island/cc/ui/src/components/Main.js | 6 +++++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/monkey_island/cc/resources/root.py b/monkey_island/cc/resources/root.py index 3f5ee9dd4..d553e8727 100644 --- a/monkey_island/cc/resources/root.py +++ b/monkey_island/cc/resources/root.py @@ -6,6 +6,7 @@ import flask_restful 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 @@ -26,6 +27,7 @@ class Root(flask_restful.Resource): mongo.db.telemetry.drop() mongo.db.node.drop() mongo.db.edge.drop() + mongo.db.report.drop() ConfigService.init_config() return jsonify(status='OK') elif action == "killall": @@ -37,5 +39,6 @@ class Root(flask_restful.Resource): def get_completed_steps(self): 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/services/node.py b/monkey_island/cc/services/node.py index a6ce09438..47cfba8d9 100644 --- a/monkey_island/cc/services/node.py +++ b/monkey_island/cc/services/node.py @@ -281,6 +281,10 @@ class NodeService: 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( diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index ac445ac6a..0fcb71990 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -362,13 +362,27 @@ class ReportService: 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() - return \ + report = \ { 'overview': { @@ -395,6 +409,12 @@ class ReportService: } } + 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( diff --git a/monkey_island/cc/ui/src/components/Main.js b/monkey_island/cc/ui/src/components/Main.js index dd143ea3a..83e2b4042 100644 --- a/monkey_island/cc/ui/src/components/Main.js +++ b/monkey_island/cc/ui/src/components/Main.js @@ -28,7 +28,8 @@ class AppComponent extends React.Component { completedSteps: { run_server: true, run_monkey: false, - infection_done: false + infection_done: false, + report_done: false } }; } @@ -102,6 +103,9 @@ class AppComponent extends React.Component { 4. Security Report + { this.state.completedSteps.report_done ? + + : ''}
  • From 6ee26297efe21577eb0cfabcabb7220144134d94 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Sun, 24 Dec 2017 12:11:22 +0200 Subject: [PATCH 65/92] Add contact us at end of report --- monkey_island/cc/ui/src/components/pages/ReportPage.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index f85698f31..b476b3faa 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -8,6 +8,8 @@ import StolenPasswords from 'components/report-components/StolenPasswords'; import CollapsibleWellComponent from 'components/report-components/CollapsibleWell'; import {Line} from 'rc-progress'; +let guardicoreLogoImage = require('../../images/guardicore-logo.png'); + class ReportPageComponent extends React.Component { Issue = @@ -622,6 +624,12 @@ class ReportPageComponent extends React.Component {
  • +
    + For questions, suggestions or any other feedback + contact: labs@guardicore.com +
    labs@guardicore.com
    + GuardiCore +
    @@ -508,26 +509,27 @@ class ReportPageComponent extends React.Component { return x === true; }).length} issues:
      - {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? -
    • Users with passwords supplied in config.
    • : null} {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? -
    • Stolen credentials were used to exploit other machines.
    • : null} +
    • Stolen credentials are used to exploit other machines.
    • : null} {this.state.report.overview.issues[this.Issue.ELASTIC] ? -
    • Elastic Search servers not patched for Elasticsearch servers are vulnerable to CVE-2015-1427.
    • : null} {this.state.report.overview.issues[this.Issue.SAMBACRY] ? -
    • Samba servers not patched for ‘SambaCry’ (Samba servers are vulnerable to ‘SambaCry’ (CVE-2017-7494).
    • : null} {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? -
    • Machines not patched for the ‘Shellshock’ (Machines are vulnerable to ‘Shellshock’ (CVE-2014-6271).
    • : null} {this.state.report.overview.issues[this.Issue.CONFICKER] ? -
    • Machines not patched for the ‘Conficker’ (Machines are vulnerable to ‘Conficker’ (MS08-067).
    • : null} + {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? +
    • Machines are accessible using passwords supplied by the user during the Monkey’s + configuration.
    • : null}
    : @@ -539,26 +541,25 @@ class ReportPageComponent extends React.Component {

    - Security Issues + Potential Security Issues

    { this.state.report.overview.warnings.filter(function (x) { return x === true; }).length > 0 ?
    - The monkey uncovered the following possible set of issues: + The Monkey uncovered the following possible set of issues:
      {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? -
    • Possible cross segment traffic. Infected machines could communicate with the - Monkey Island despite crossing segment boundaries using unused ports.
    • : null} +
    • Weak segmentation - Machines from different segments are able to + communicate.
    • : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
    • Lack of machine hardening, machines successfully tunneled monkey traffic - using unused ports.
    • : null} +
    • Lack of machine hardening, machines successfully tunneled monkey traffic using unused ports.
    • : null}
    :
    - The monkey did not find any issues. + The Monkey did not find any issues.
    }
    @@ -585,7 +586,7 @@ class ReportPageComponent extends React.Component { { this.state.report.glance.exploited.length > 0 ?

    - In addition, while attempting to exploit additional hosts , security software installed in the + In addition, while attempting to exploit additional hosts, security software installed in the network should have picked up the attack attempts and logged them.

    : @@ -603,13 +604,13 @@ class ReportPageComponent extends React.Component {

    Legend: - Exploit + Exploit | - Scan + Scan | - Tunnel + Tunnel | - Island Communication + Island Communication
    @@ -628,7 +629,7 @@ class ReportPageComponent extends React.Component { For questions, suggestions or any other feedback contact: labs@guardicore.com
    labs@guardicore.com
    - GuardiCore + GuardiCore
    From 4af4178344d64031c47506630d02af7fa5d7898b Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 31 Dec 2017 13:46:07 +0200 Subject: [PATCH 69/92] Add logging, turn mimikatz into modern class. --- chaos_monkey/system_info/__init__.py | 12 +++++++++--- chaos_monkey/system_info/linux_info_collector.py | 10 ++++++++++ chaos_monkey/system_info/mimikatz_collector.py | 7 ++++--- chaos_monkey/system_info/windows_info_collector.py | 14 +++++++++++++- 4 files changed, 36 insertions(+), 7 deletions(-) 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() From a5135c3c3f9f5b26becf6f46783cb9b5b7856a87 Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sun, 31 Dec 2017 13:51:43 +0200 Subject: [PATCH 70/92] For the 50 lines before we can use our logger, use modern print --- chaos_monkey/main.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) 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: From 109a9a5cbbc6e54ae3e4dcf4566743964eecac93 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 12:34:59 +0200 Subject: [PATCH 71/92] Improve printed badge style --- monkey_island/cc/ui/src/styles/App.css | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/monkey_island/cc/ui/src/styles/App.css b/monkey_island/cc/ui/src/styles/App.css index 8575387c6..c4bbecc62 100644 --- a/monkey_island/cc/ui/src/styles/App.css +++ b/monkey_island/cc/ui/src/styles/App.css @@ -420,4 +420,67 @@ body { .pie-chart { width: 100px; } + + .label { + padding: 2px 6px; + border: 1px solid #000; + color: #fff !important; + display: inline !important; + font-size: 75% !important; + font-weight: bold !important; + line-height: 1 !important; + text-align: center !important; + white-space: nowrap !important; + vertical-align: baseline !important; + border-radius: .25em !important; + } + + .alert { + padding: 15px !important; + margin-bottom: 20px !important; + border: 1px solid transparent !important; + border-radius: 4px !important; + } + + .alert-danger { + color:#a94442 !important; + background-color:#f2dede !important; + border-color:#ebccd1 !important; + } + + .alert-success { + color:#3c763d !important; + background-color:#dff0d8 !important; + border-color:#d6e9c6 !important; + } + + .alert-info { + color:#31708f !important; + background-color:#d9edf7 !important; + border-color:#bce8f1 !important; + } + + .label-default { + background-color: #777 !important; + } + + .label-primary { + background-color: #337ab7 !important; + } + + .label-success { + background-color: #5cb85c !important; + } + + .label-info { + background-color: #5bc0de !important; + } + + .label-warning { + background-color: #f0ad4e !important; + } + + .label-danger { + background-color: #d9534f !important; + } } From d16f3fee9ba644e7b579b65a16a1179e6db8992a Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 12:37:20 +0200 Subject: [PATCH 72/92] Replace monkey logo with new one --- monkey_island/cc/ui/src/components/Main.js | 6 ++-- .../cc/ui/src/images/infection-monkey.svg | 32 ++++++++++++++++++ .../cc/ui/src/images/monkey-icon.png | Bin 38958 -> 0 bytes .../cc/ui/src/images/monkey-icon.svg | 17 ++++++++++ .../cc/ui/src/images/monkey-logo.png | Bin 56273 -> 0 bytes 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 monkey_island/cc/ui/src/images/infection-monkey.svg delete mode 100644 monkey_island/cc/ui/src/images/monkey-icon.png create mode 100644 monkey_island/cc/ui/src/images/monkey-icon.svg delete mode 100644 monkey_island/cc/ui/src/images/monkey-logo.png diff --git a/monkey_island/cc/ui/src/components/Main.js b/monkey_island/cc/ui/src/components/Main.js index 83e2b4042..cee9bc494 100644 --- a/monkey_island/cc/ui/src/components/Main.js +++ b/monkey_island/cc/ui/src/components/Main.js @@ -18,7 +18,8 @@ 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 { @@ -68,7 +69,8 @@ class AppComponent extends React.Component {
    - Infection Monkey + + Infection Monkey
      diff --git a/monkey_island/cc/ui/src/images/infection-monkey.svg b/monkey_island/cc/ui/src/images/infection-monkey.svg new file mode 100644 index 000000000..3a357890d --- /dev/null +++ b/monkey_island/cc/ui/src/images/infection-monkey.svg @@ -0,0 +1,32 @@ + + + + + 14cbedff-3eed-4f8f-abb7-fffe92867ded + + + + + + + + + + + + + + + + + + + + + + + diff --git a/monkey_island/cc/ui/src/images/monkey-icon.png b/monkey_island/cc/ui/src/images/monkey-icon.png deleted file mode 100644 index 43871c726509a2f6acc29474eb7ac315c8d93124..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38958 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%0D3MFuEk3H_T%iSW}tdHS&7YUpM=q*Eg` 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 + + + + f16be487-8c04-4779-b803-3b42f7cb3e8c + + + + + + + + diff --git a/monkey_island/cc/ui/src/images/monkey-logo.png b/monkey_island/cc/ui/src/images/monkey-logo.png deleted file mode 100644 index a0f83f3f4b5ab5e8416e1579473a414b2e66cffe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56273 zcmbTdWmMZu*Du`CLXoxwGv_D=W!ABYS4H{`Q_A1v!aNh`5Mv-n{uFB`FGg^X6>@^qme52YoVXxe*Hez;O^$ zb5OK4c5v3WGkPNgvNkXxma^10F#;OtgIsO| zOB-nKH*a_aTx|3W!A1_m21X`kR(zzVjjg1_W*|ONRW?~BSsM`}Q!`07J0nFmIVD3k zupt+SRDhqD*M%E8fTfXxKCz3Xg_S+G3m@q}<8nj4|9#9zO8id~2QVM$zeK6YDiDiU z+ZhqFF|gAc0ssJF4lV`&8z&1h8yzt-6M&78iHi}yM9&1^1~73mGZFviMGBqG4rI&? z6czu^T+m;9q^1rIHr$Mi&d$yZ&MXYpb|#DfE-o%cCT2!vW_oA~dV5zZ2YnZMD|@p4 zjv#7eZ)j&`<6vfOMf`U}eFJMp2R>3LrT;F$(nePHe+IU)|IbE2w~Wz6--Z#uz{F^2 z`FCCaw6=Eu8vXxd{I9L;m0WF%7=cFi){b_D(DN`R`)_CH-u>Sv`r8mn4Y$0V8T3%} zEkvyi9W9Nl9Hd0~NTL5QfXqPLY#>8pCMJ*}y)hRXCp{|*7aP3+moW!Dh=YyGh~0?G zfD>T!-*Nu8{>)6A!ptIUoUF_&>;QlmyAZpOmcmvC`i5p!e;-o+ zJ*a=zEox+E=41pCx3jh+{%5DT&HfiExR^OW01z_=J-e|n6Fn;zmm$4Ar#>4!8yg2R zI|mChr!hMx>3{lz{$F>H5lR%}Uv~LF*yq2Gpgj5a>;DJ?^vC~*laUovhU}oi(swom z^XAR$sg$UYlFP!8_6Id32`oN~Y}!C2*lcQIgZ33#^P?V7v@i;aMDxU)r~PJZG(}m1 zjD5iPTv+q1KL#wig^xqbNC(5~(mYHb4AK#4B9~WXk>-l~<4O{j{ppf>iL#j)_4|Kb zR;qotb|xw+DjT6l@T}zMWyTTOYKH?F#NcA}OVDsC+lQH~qV2LA1NxNe_u3&Q$K+th zQHq5vDeS>df99AX+zC=NQ3~QQ;;rog!rn@DNHu>^|6w^>Soj!wWldtWZ|K}FJPE-s z2|hkPV(@gKd?QAdQ6m#Se>rkFOCWe8O07+c!kF+NBwG}xM{2Zd$azL37&#=o4TwBElJ0>Qw*IFc|S zxF>fmXcnmt#($cbR1I%=v16@d@zzyjDLl5Qz~wOvK}joNkT+c|yP+VfI^4*BhbKk* z@`Wk3OUn^+{L6zirLkw&M`fE@eUUZuI5@lvCC=X##8s>_sd}KAZ(8{O&cg;#!!z|| zY&worsjIC-J?71>-BQRr{~ z=O%Fx%{IB3LU7EVyz1a_T(i+_qDI3n`NfS3yZ!#t$44?WDn6oY2WN|+`=hu4P->Is zTd?`lwgQR)@NNO;9wgG$94S$4#dMT64RStWO(On@92^^Jhm*Tur=nqKzitxN8XOQo!^3&fHiEY(5$BOt zukIC;nnU+7$Sv!r!44kj

      Rx!A_t@{S6dUgP!IqAGiq1eQDS&d@JZ>=FA>|9HK3$ zXt)bYE3VUxJzh=Ub7dNE<`Z{jv7@$MS%AtWr`_cLY{IDDy%HTL`%S`Gm+RqCv%LjQ z#&X##Hg%|Pm=gk-N3{L!C-!%b*IGpkwg6l`8jo*(*o9h^=G{^#+Ow)*!De*^3jPtpk;BJJ+R_~+o( zT7^LJmA$YdPAV>DdGuS?tUWh9v3j0L2*wd7zPl7xeqq8orN;yjGF@o8&+nAKf2MFG zC6L&BPMR5NS=G!#enVpP93DC}(G83F?|>r|0Gdj>plV~}8-H^R%_~K|vq_ha6$IHkjrcHKR&L*UBA?1c8-%h2q+UtI{V{4B)`?n^#v)~&nN}I6 zm>vwYR*ou~soX$Cf)9c2+(>ng#oVScX@$h2T4tE$vPZB|qS5(VHVwC`u2aSpWWS+` z`AHu6?)8@luihAj37Uw6nE}(`(1PAF2xW}AQTfJCq>yia&)Aue0xigu9Ws^BWqNnV z1ANDU6#+*A5PrS!T5`NUgrJfyk6}N5lh?(W^`raRFjKID1pf~Sm}=k9zY4LnEvb>| zYTKI@GVvTx1dzuk^=j~ggNn4@z;eYmP-L~)-puLS)uC!@P7fE?G3tc%k%Nz3wxl@c zsPL|?=vExw6VWXP<~|OF&Ik`O&~-oJAt@+z$oD=UH9e77(da`Epvs{P->kl}ccciS zW=%OG>AHf&7K9LUfZKZRN(+kERa+chkb6<}`$qPdNi#}mv8Cc-pJka6m4;RdK097P zwea%B6!uuM>Nj)08r%*HkLZ4<9meB%qeaydj7H;@*pVo}gH^v_{I3wV@pesz@_jb8 z^eKT=X^2oK>vjb$m>Q7%;A3$C0cqv9e*@X-l5@n%%i}-bPG&cJGDZ6pH0GsmiG$a7 z>G=2>$k#F!rfv}30k)R<{%d2GXWHfFgd`qi{5meB=ZO@fLEsYB?H)ZNMBZp5H}dIT z6X}YfZ+7m~TXo6fU&Sz$()1)~B;oVD=vutK+7K4cSCciiV^RHQoE&N$kM!7*4v;#R zmaG|(uo9%|d0BpEsO1ZgEX)!n;}yMluM=%eXKELc#e@v**waLGu&go-K; zGLA5Bb~-9`6~8jxQ(fa16V$4ft`^I!(5`~t9;8P1#oxQ<&@@9=e%QTtmnz@40BlTD zFQz{W%&+32SF7PYIY|1FXg6+DKf-Y&DyAmK`hJe^4&H?uSU1qUzm444oNY9{kNnW* z0w|=DU&`zF$2Kb@wDHbuDi#gv4ae$aXy}M)q-YG(CAw>Grx}#wpC5nb|B@LS^1d=) zu5D^Dc?sf8#IOOjjCQB8jk5%I2a%RY%J*`#_(la3COP0!n-}rYxA$A`w24z&E9Jg= z%e-{&&!J(@u5bdp8;akkR@P-+1H?hA!FVcP)GbLa`re;_V8Fc#s5eKb+i{;JjkA4b=K6|is?>5jON z-lZRWG&#kDIkuRT@xwVc%+!HgJl;_i8NVfKgIT#}jIqsTNIo!04!Z?MVKJeS7Pw0y2HM3GWF!L6Vcf z2N~{Q9KwC0;o|N-+OKzqYLM|$ly*zwnK|&S1grV%^RE?xFVV(NPiXo9zy0>)F*z!H zh?#%9lKxOHzoEkGk*c-M=|TkBjXqn)(^EJBGwOZstuHe+fXQ-Ws5l54aIVv7CRB3tR0LECo!TJs9RjwY%k>z;=vW625&3ToeXZ7#Glxr`;%Yu}RD>yWHoFTeVlrBspjie|j#KgGTP`}aAD2V8jWK`P z6BTK!Yzdp_+AB97gzS>Cs;iu={X?%}XjYN!exG}An||$pI%*JvrE+rdVEjY1LrD}b zXJ?=dDOghBqLM$@#?(dg>Xd|k_ee1kE~XmxVREG>(HIlmJ1vIg_)x?sBvUPVLiBR9roJ+(FQvFk~q1zutY#m z7ZugUef!KOIfKtPvXB_v94dhf_hBK-;Uw1#>j((8_bi#|{=Vw2XCs&NM=sSuc??5? zv@-Do4wdctN@d~fI2DmkHk+2Y8~d$KAV_A)S{1!I<-$dDT{i#q$%=LtRYm3Nfiq z^UEu8{@?}0rK`WL>R=o=u5v|E-zQYhe zU82*Y62H`N!}rbiSw@QaC0cl@_g^-n|4P?lYM0jBCbzhO9J!$=IfdEUo8}!TdzSW^ z${C1tf1PtXYFclGEc+f-TV_wdb;2!$7|WehKcqHKAnNNdr%K%Hj-b^JZ+{yZD1r3l zp$ef{z7QoeykwyL4I1iFq(W7mRm(8fX0?K;weeEB0!`s=2zx~i5E|vX$x0PyQSowxx*=M7EF5)kITrJ$G1_v%H!Uih0neZOdVCs&l12kBy|px(kx*q5uJ_Xq`otv9R)WRr0%dZt zAVf5$wEDR{#>17o5U4YMtg#R$tECb6ndBcDyEL`-fE2YbLT^)Cl=NL=oM~q$ zHOeVqj?otuw?-ci0@qECrp3L#a}WmoYz;9>tENVR{~mnu?;ltYW*6QX7qnNH|Kuu3>-^reQR505=K0$o zEJd*%hc4r{V-O}y8+U*zLQ={jbTF+)p93CRaQ8EwGQjR|) z1NFm5mG#5YEDJ);WQO`mE?*EsWSI7{{Y(!Fgs_m?`bUEcA5FGRHV$GxHhCPOGuutO zxTrY~5l-UC9?NpX1QH|nQmj?4&ro>hs#y{@Ta35=B}*-;ZEq!EWC4GMM%js+c|uvC z#-zBpWI6Yd0no{qz9)(SoPU)<^}_!&kkV|uOpmj*nm+-?nZM_BQPGsmk|;ygV6~ng z?%kN0!syRuQw5kAWNH%-Plq;a-1&*B4YFcmso27;_j{Sclhp(gi^t6VNP$R{nQjVs z+>J>t38ZIAE$Y;H5ph>V+Owf5eotrqq8`3y`j?sM#0<3me1agae-n$6q6hIdE%d4* zN{CA`FCT#U@nR-R8h(hbqi6k%;ydHEnzgf z^dPbu@0dZLcDlc6!(H=vi1{1)x_u4e@haZEnB$77xEy~X&zOQj_2Xz^{`=bimPzhr zbnDj|!bSNmgLOq`d$Gp?K^iInYd-&kW^;}q5Eddd`VghiQ%7|V=JS%i{rt5giZ*g9 zs(_Rjn098HLm>o>i=JzY4MSq`^+4%5UwL^fRxX4Q*0 zTrO!3?_F3JsT%FCij$M`(oea~2DQ@H_EoXb>0(i*o~Q<|hU>Kba`x&FD3vXZ8ftmG zC1xB1mav9M^-=T0fipT0;$cv#sq|?6dMp6<8(4^1#0+y2zJ8~Dv$>%dBztB=F*u>Q z_coxdD0gtV>J?u&$z4oMABYg<}$R6w*yC+v7XCn zS90d$D6An@zE4hD+u^&MB{?kF!?IdZFoCOCW`_j_H}jHcyn_=+C+EQX7l7G$@7FV$ z9eL``P3x*Sa3H~pZo8X>q%sz=wt`q?KA!{ZGsL;07X*)yerKQ?}lxZo!SIxc_|rdL)cw9CXi7>bWI1@hOa$`R^>owLQjVy}tvy za-5Vi0usgU)Hfzy_mt z2hy@zF{CLZ#wOagOZs-79_o2W)*EtER>_vT8ik9_4n|*9T=+v!a|L|oM&7j#Iv5GCc9O#cPNG5LQoAJO4Iya~j}Fe)C0A}j-vKrFVbdD$I2=qn)oC=I#x}ce znlAO;&+4#pNKfU|KPYk24rqLDJ)CGCMM6*qoZ06u|H>e1biy0i*_gY3@9=p{nylrl zQ=?@?9V=z%7hR`g_%B4UjCY7o6PU*s^!*#Y7zRz>_3YqiQ_z5|*@93=R!E>ikLb5u z)bFuHpOr}%D7!A=;?b>DfZ$D&CEQ1oTQl?m>5o||gp;}Qz0vagK^YmO9#7}J*jO>) zfM($#H!CHY$)6k-P#Yw7bFJBWh(Cr}=sdd;EurF87J%-a!C*(aXv2#8n6Yu*5!tXz zTb^(JJn8!{NlYK8B#KfhQ!pqTl;S-%{}MFVTL0^VrevWSR>qcI#Z=xl))p!I3%bFk z++Qd0*~%*)|D~8#f)rTyd?;Ye{*dDJ{Gi_-_0|9Nwb}w_A;7<5dH(J4&!%A~D?~)u zNx7y!NXpLO0|o1lqa?Pf;)TX{DMizPt#RQ)$sWAt(}OVE@QyHL56g-6&hw*X$ zy6WP~B-)%#T27p8o{#{i)g+T!!x2dIeA*OcwpnXKeEmJRy*FM-Y3yB(YaVt_*WKhX z3Cydws21Is9xB&)L^`(>5jG}y{Nz}tJlFiaqlMFbo#s6|qgbBF@(d5PH=R_YoJ7u< z@i+qPFEI)Q-KGwI$;B_o)C&y>0$+UL)}YaRm1Eq-x1YH7aV7e`rs}^$K6uOXvXx-< z+#>jO4R8hlt4OM+uE>*v_(KN~0K8P4TkVU1C8YW!loSuS@py@F z#$k0w2tzhk1wu-VBScsvEOln{@4$73-XVbPX8~JkuEGV&s5R@`5ntQU_}+2qD&(Tr zo_EBO+o=Tryw0@6aMecSYK^c27L$(l_P4T$tnHfC%|&by?&)(wg=J){mZp{*V^?#^wpLF;NnKQSJKOOVMOt;MC>A=@x+oBVLs6rstUr3b zquX^I8O!IWo6|gZtdRRbU(G>ImXI&Py?(|~Nh=Cd!e@i2P56jk`c#K9YhPHaV!B-T zj*N!1if~Ob98i6LSMhTPdKcHH(PpwB}(>TDf?rX zRqI!my~$RWw}TJDH2EoFViPJU&0qKCGQ3W9>35YLa$AM{=*Ch0<^!faD1~?X+4*>A z)BVm$jx8nlm16nTbeq(-Qbf$?({Bo=yX|bLJKGacUsm*d3y*4Zq7K6Q(AQ$AxS$MF zUl?xv4s8~I!I&pJ@?k`~CuO?5nm}gSstqSD;F?Ndeqx$;;La>Tio%mY18bdwNg;Sp z>U{5JXm<`DM|1bNM|aRio1U*f=9Pmq3rZV!9yeKvS(Mg`q#^yEV>Te=b0{&~V5!e3 z!HSu|e$G*}_Tj7S;eK4W7_&62YqWO9e{f@`xT2pFCJzavs|;tsTB|LF@EVj*C`{Nt zuAMZQW&_Y!qC6gd7kda;YOt41S`$rCx6xtwM74oo`lV8&CQ!MQ35o0T+TYR)X_TR7 zuH3zKAJHlzNC52+L#i=l!5prC*Z_rYa8*{p%v<~PhBI^OPK4s~U{QW*tK&Z|2(_8=&A=@V_1M{Lb-K81 zak2Xtcg*c)(xRhSxVpN~i6hea?$8r~xe}e}- zt^#iZ*|y_%7K{&!aeCmQ_qx3NpF2Aj1!*8QNZ`dFB(aaP2_c*M@wZ5nou8CFXKyg<^*V0^Z0Wb{s?+A}O0ZwW^Ifieu>9xJ zbM8Kpw%j?eF&M{?epU6KvS6zWoNoG@Q0R*;Wc79LfDC+=kuw-kSjHHRcA=~QQHiC> za?Sno=S~co6WDI|`=B{vkOFc}hRbySr6gtW>%_9Td^5B6>@j>Zc`VN#r}(^&+%at~ zIaFpNXpkd1Uigoo*LMWUy8A&VL($qA>}3}0W)aOyS2P~{*2pfA$NlT?d_P@T*xNHk ztzPa>jE9ffYQdke9k2#76@9SyQysAys?5ydqIH#&e72?zircERKGleVi{lL(sb^AE z&VkhAm&&mZi1pU*w6lx$bs+}}#dvUi7{z1&-2EH`)PKBmhO=%FO>=*oLJ#G=(zo1XqgG3Q(RLN&PD z?i6QbKoh*UJIdTIA}FDc<{X4b;&lO$Czcd zW@praFBI2OTla^lFJm|_q)l@;dv!T@w*8Gkv{fnY_ zpw59UBYL{&DX4`q`p@EwcCb}3D0z-NEN>Aac7Go(yoGU|sXe^lA`ZeGzd!i~Kbs*a z5{=!T%L6R)p(YpS{B7U|;9Hn|$uZn{2dkMkwUE?QSLYPeR5+ln24@En6^p~Bw)#S) zpCBkiDWluiI-ceQyeRE2R{K%6t2GC5}r4a-E4K7quio;|Mq~kDj;4cd&N2o+wMKhU)7V;v=zcS6f zFZkp%9EeGZGGApj$z;94yS2T&Ufx^PPG&2GQ#%a1w`$MIs3?-0VaE)K1}7e2 zXme<|XNhMy-2o9kSBP(wlSum=1G@K)DLZ}G?V>4ZcHXN;gxugr9x;JCv4gfltSF(N zNL%unqhOIN-K=mst||-Sc>idj98V_G?bM-BoSK&x13p2r`BQ2ZuE6|2EUhaoa{kth zjfvtGMRk(UjQP&BQdZX23MY(yGXyq`GEwt)IQl4NkVS~(k#HjJ;@qm|`B&@-mB0D% zK3uAWRJ)aza5~{~l_oB2WNO-WENCf)MeSW zBj4XXf|ihkTp=HmWtjj47ao#E!A3db0;DjT)wa(R-cA>7;m$)`l(69tMn<6@3xwL- zSGyT~WC6TBf~`z_plNw34UXuIeynU}FnmD} zrJG*Qr z9a_|CD+VNHmfi4z`LKghP?lxdqP~EkS~;jF1|w$3byF)|?|XIzqRG zu=5u0Mug#eXRaAwqT+{=wx6M5h}6OQ{zf1{8h`L=t++w=3JYgi|M_d zVt!<;?y9dpP`8W4O~q*^AuoUe=nLQ#O4aw2OK1P>-6VNK-louEy>v! zoOUlWSjG9}Lc_$b)OncPoiku_70k|?EC>eqiNPwXII_E4Ry@mx!0#4IA4Wiy1RCWH zcY`?5^JtLw-s&*adD}>o465mX%gfh$y^$|hS&WCU>C`LUdp_QDLp^t)uG_{E zlz<2$UkmHQGe3iXuYC)@k94HJ5J>+;{fmY1QPjs(y#t@i`{B0>Z>~&8$&v0gfZ8I9cOrx_fq8qM8!S9LN3& z=(z84Xu7||q|@-m9$bkJj#RM=!Sstz7?L7Wh`MjrA?^ux zQ(sZZm6mMx{l~KCYF)L*5EQjPd%`faFa}}Y(s}PR#4`|(UV*f-vE57v-9v73HsrW` zo<`V&bkh(uoWunUTd?4%2Dz4OXyGk#dS46IYDs6z?Q0j_srS#r*-|oES|pKx&&405sai4kdLa1LF0B#P^L zy4Ad@dB0;M*rFO_$SdP?HIpJ3oiPo%5ir8F7{;Tq{eDl35E>7sKxT<&yfDcrpgKFf ze2tSbrYKPa%MxS`+D2Ep=bzqLxO({p!sOYUBrwNs)c^|N#hZq8v{7I2$Z`y}M!BS_ z%OAKsznRd_x?liR^WLrbMoN-En`{m$Ki*wN@IT~{p8xVU z*q_LL;&`b#r?Q@IpYQ*aNtV-D>^dEb2&0wl5)V=5Dg^`yFz6rK&PYZS;gA)7FM96N zNZe!o3wYw<)x91VxFx)?RJN&r%_yZ%eM}H6-I;qCH@v%poO32_wIPK4L}YCUbLRnD zw-zR7kkSG%Ha24SxQTfEa=lwmzIdl5B=l=rNAO<23kwUE+3(6l*Pz&E+lCD@dbAR{ z!qO}`&T4yT9~qAwX5Goh4;EDzmQORvQ)A3vv)eE?HNzCNScMKz_~9Ll-$PLl$c*re z9FnEzU;7(|N0V$ALG4-qB%fZ>xiTdcn%dsN<&c^g>njnOd!4uN=z>3X2`1aIq+Kte zVbor(9855JNa8VPv4KJdhN@xD9Le>&?#4h#!pbH$q(=&7`5TDM`HA>@-1`MoVj<(P$eZ1R;PKt_+!gLVG22)l<4Ym^M+(~rH# z2k2u|3^3Cg?hl1*PxpA%O+S2m4kEzk8{KEOx4ZR5@?P}?JK>BF8|F&~u=2KvKOA*k z13%6hH#3@J{6CopOf?~7XvLd({Pa6ZzM}H4%rz=_l)`wJD{zehuBgc3SLX*ThI~SdJ+a@{YUK+j6&D$(ET7JD%N)-g~xCtnTyRUU|{y?emAX& zaPh$N`yIGfD!R`?AuzLf}E%EF8W8Tp4{_}D8{1h=9R6`S1w zQ`%u-c*s*`l%m{>-;N?`=(YbCHwMWwRt`wQJMX@*RWl6CQcl&52rN`4ka@YSuZVwt zx}G#@EPuX(C4@+}<3AYuVMDcb;aqXuC3s2Bz?LnKlZtUsNITcU>7o+S(A4+%(CKbH z(cIog7hXMFM6Y*@GUIk|Fb}N`glg{h3M3JH47s@h49eEAj;16c!Wx)2$@OZx%d&gL z)54)r#d`zaB!fG{JqrtzvYGs6iLgIXen`?FdMBlAnZQx`T`4^0sn*$Rb48m@qbjfw z3G7>Mznh_1Yvqx0G+#O!f0aVjQjOses*`;4jW1sqwW z>CB7XG1p8(dCq@zZ%2e1DQ&)SekiZ2$f7jBWE_{!Q~?W`!Wn)|l`npJmQgh=v}&q_F`s0KpxfXq5qD!7SC!1EUqPql|J8$ z5D@;OptlLZE?V}U$vVBif1jy5$gbQX5Uy^tWlrvXUCuufKR$_@DU7kVLAH6AjY|81 zX{pByzWh`CMs}+vpc+{nuOl&RPXi{-N7-_j<9Z<>R*klWK3#iCpj(3{iYB9=Q~Ae9 zD$xyi>?x69;{E@j8L^!Kw4>bbWSO);lygTasU_0;^%zo9rmCS_XYz1^-92k_@NV|pn#8;xU&QF3 z$0tvQT5A=ORcJ?A8N?{ofu)Vu;$~=W! zJUhE1)+61q8OKgvQ|)U|g3MfwEAqf!wPNa$v*XmINxRytiK}tez0ffepir#a7aaAY ztkyX%$w+)M;crO7nLG#5IsO4tCR>A%xfMSPvd|F|Fp$5L$NugR%J)}AqrGGME!eM< z+YW*2zc>-IhhWDFn6ck{0u{LxuGAavz$m-5JPzyDRd_Z!9hcdx3x6cy$FHxij~DRf zPjx@xsys3oP36;C2%h;s*6I$oLLq8Afjgg8*di(=RSx-q;9~E|&v< zwe9^>pb{Nj@a?wDi6AB|G3ZF#wQ<#D;(@B`due0WVT>}9`SH3Su_kTzo`l?Z5=iymx>LBqQ`$E?i4DI zeFKNwC*14GZ&Ea!5c9W&i$-7CbI1Tsukr%Vtj;7W_y)RKT0`{Zz8{bc&eqzSd?=)( zrm8->+FTT9oq2kx64HYU~o{QpLOLBo%S-1x=2$aA4NG|Fs#oV!zGA&w ze?C#FZ`ZHOvfVMa9$T=*Y>D+5TKxFr%k9h+hEqC5ejI}xaXulOo5`V$Fp$$M?ke0>7fsDS4**5+L%rje>jwofFa zNDk4YT_QcPw?@iK23N)LAU5_?Wt@RG<>OGe?S;_&fu5|&U|j!|N0b6uDQW)hVz@X* zboTTX(+;(O_JuoVW0g{sREnTK%kJX=-wG+YxQ$bpn6Zeon8?x^T{09&C%C6r=_gZx&{~J`&3oM<1vUs z=M{HSjLP_oHe#@kXI?Jb3XV|-wA@n9fU=lP?$l%}c02=W8Xa-p=jII4JM9f8Eu7=(#=-R)(nHZ@zSIXqC^o+SUbv)dx=^*V zyE|KDF=Jpfh1*fsz<|_vDE|AYVP>GD)$wtN@ORr`gkfmmTRnHn_@!AX0XOsI0e=Ds zGihNcTeYTmQ!CQ!( zU};obyjKUM{Jj+g3o}@f=J{i>=NPW4B^?WCi!x>P597>5S{G1~e28Gx`KjJ`XAU!c zmAH{qa%0rMHR`TMV|eM{clvU}-thJFUZT=y&6JtRtM6gpnWA=RqENSVlMEcWl;nCe z_c^uMV8S_(o1MjK{op4n^X2d^cZTBDFua9YnF_*|thj}euzwLfA`@3adiB)G0|(LJ zXkOj53SpIs1Sa7&>4ACPrwY?xzcbLm;h;vG{~(!C1!?R4uLW z8MQcK9L5`&9j#VnAwzX`9a8{=EDp+1sVt!ntXjEqWqIIz{v_`}0poVN?CiRwFDWV6 zTtk43Z5o-FLB2mXH^*%q(iamW0VX?S7|$y$y!)17c~@MQ}eX0NgQ@zyg3GB~Uz7e;|gMSG2VxJ<kV=#2&$xz1Q}aEv`Uu{e&MXZa!>(9>?ve=p)SwHGQ2Z^1P-lma)~t z<8r2UtFJAneTvD4LVrEoTi+@zBj`%?8DD$?cAkf;QIj~V!*#vxVWIJYSQ5MCkk^yI z)7d*-US6B6K9r{Wog~MV&t{B3a`JuNoTM`2JSCN5h~j}uLtfUp6<3B_#ZIoXsvWGC zhI}S53UEfZsrAbxJJ#8?D&L=KD~fJtSuSteFLf~M%JAEL4jRt#zhKpuHIPc%!t;vZ`pe*uBN^O#y-E1pcMoM%>*>{%-6oz*vz-? zghPLHA2a%d2PP|&f)QYYpROk5pY8*QyaJouZ)ePSpef{aF7sb#SmtpIN_}{x=fbzB z#knYd#x*n)HEB;abnMdM)2tcST!Y^uQjn&MQ9f!OUQTgqK>YJy6h%KkFR2hKKnv)P z>hZ!N$lp93rwTvy8TYZ4MZ$Eoes6dap#X~xOfUA9V6}CyD(eJKR&Z@ZG1vLnXbX08 zNf7~_#z)JIz0tkHE}E#ukm~jrv82aJ5w=Ao?l-WFf(fUBlUd%wXP(CV!B9U>L)kT7>OOd!lG(-2>ek( z>1=xE(CFFXjAZK8x08A(c&FbTj8S*m@_4oYE`WER_Exw&+*2>rsLLIIXfgQz4BNuN@96vH8OXr=9WOPhn2Q%P5Y3 z`K-~X>zlcs7|CsM7Q7wrxFky>V-EU@XDdx`P|*H-sn5>IxyUke6JOCd<)d+CLebGrh>LSjzrsF>pIFq!gzzF@5&sHlta$Wsf*N#7GAg#pFkLG9ni z;MJ|iN-lxKfwlh%(EyqTE_k%egmJ>VANO!f7%pGQm|gjCfplmVjS_5=OTg_6tH^(c zmO}KLOsn_h2Z|KR4-~VHc|C~C~*=cxPX`$6@g+eF#Y?__dzCCRXa4K4#`+7Q@jo5J|zuo^|)ySrc|=% zU0(e{;>Le6F8|ic)!`tIiL|~cW)r{ej`{S($~va+iPwg?#Lnj(WIC8H7jWe#)>ORg z9jB@M{BXVbRiO`tMO<%t{rEV{y7f6u!25ptHwp%Z(fI1cxCGH3)DbK zToeWpmRw`cklQM5;LQ^G)bmXysH?|)~AlLhb5nn)8927(R{BpX8axq5f za<E{cw|s=z0VTyCk24$RYw<}6hT0=Ns`qI{9oal`XB9MmSavouvw0$;Vh2Cm za@p^!ZFGm+9hSDelX^l3;CSM(`t6YYa!{?3YrfJF+VZ zs+h@z+mcR}x;OBw=!n2Chh*(aN=@|7<9aj{f3e*+C(L-&Lw=XQwb%Ep4hJf#Prb!} zV51~K`NrxA4a1k>#pQ}lDe2Lj*aRfyukqLW0&2~S!2uhJ?}_|Iber8UE_a4E$(4w{ zkN^!>x1&8T`YGp{-D}0XvBUsCHZ$PN>rRsOHwr4n$du^*5r{1Y1d2RElPHg|#-taX z#_4QWcnnkw-L;KK^)#Qv026K+T+EAoCx%4h33vjce9(Ll3JyJuWqQKtAPT!hWaLK6 zd1axJV(t6Dsu?2QfYn+tgU55u&Np%zfzW-}>83x_+%2RTO5yp9hEr?^aopM4Gc8}O zhdQJ$?)dolcZoREK1u;lbh@*@KS&!5fmyXxd3(2@KtdcPgt2slMd@b_lQX3z0v2k{ z;c*X@4-8e_R7hCU`HkxX!C(Z|c?E!#6_TZVJG0 zPtVPzl-J04%TGsFBqZNHo+&Zj?C#v+B&L5a${r`|;J`|G5EX@r&uQaxT1%76X$wQZ z<0KkcOZ(aDSt&M6v*t!~YfD9kEK?{A&bvbA&TWyB)3SMigwCb&$~@ z6#!XsE>4s}*`wLUNY6is&qDc#w3rc6ei9lSJwH{lAe^QIKAyCM?VAiH9W@la`MEb2 z7TpJeUaC~{>B@`$gGC+b&OXl23H2b@OS=`dem3Va>Myrf-rXv&MzIXW9QTJ|p`f&X zIK;j!Nd?bZslT&&r?P=*GN-pB;pO50rke*V5>jHs$3^dl0MNHmNEixIzz!6ACwjtM)~x(>QJnS?9V;cm~(iK!;bK$ucI zaWL?Y4s(@J7e%Ys3@-Iyp-lD6{Ha8+3jz*ef7*&jKT@B$G~oC0><8?GVswJ0b0KKP z_bZbRq3MjQVLr49v6Nj)7qqiwsJR8(a#GE9!E1Lh*s<8J%xSkJNysff?om=wqE=&p zL-_@m)2f`_Csy!TmZ4`i&3h$-L3{zrOdv{_A7hJLW(pe$KKH)XIF)J~Uf5-vyBTF^ zy0FP~_7{eMuZu)jrNPEi9gZYuTNRCwIqk0Epxu&uNDm!n0k)iMyLjeJD)Wtc!upDt z7# zU%yF{Mo)~!X>8keW4lQj+fEwWHa50xH@0ote82sj_Yc@}W_Rw~xgX6|?Vsi~1ZeBD zpVTAUV`a6+1dIRUL0%Ttq2=#Pr{ut}$6mk@jwwruj>Qej@t=ybcpu zd^@^4Rbqzrk!vy9Xn!aw?STP_kq1#7dA4WVsPXh=VRuq`?j+~w)}_XNBb_dv$=&^M z*$N7!;Dw?rPXlB{NvtJWgPp!+c%*q)=1-cQg8$&7bSbwZff{gHo>iNu00n`YG3ZV% z;?M4U)nqy)pb!UK+<=kQ;A3En33OkpI6aPoDDu?thqU`U$kZxgYi(|Rk3KHudRNSD_9tDI#!lF*V=ZPsPDf#sAdf9R!=7zFMw*AE#?dD5}7Ce_R zRlVd7I7V*nRfE?&PbME9p9{Wx2M1=;aleHrXuPixsLS_f|K*!tFXr<&i=L{^kvA`X zA5U!%6S=y%GrM$J3-#w2NtV6du z>7*{c8HN0^3Od?}gd?igruj~ofWd9nRA)=;P89I!(ZXpRU~u=Ak&~$%kpHY1%_DS3PLo4 zx3bZO0q7tBJ-6U;jxfJ@xfmK4SoLR14=_a4RJ6*ymKbsRDzP zfLaD}mkjI@v8orpO^j@L=j-{LNnT*9u0=`2`dRgRA+qv4U+&B*DL!8>yKYe^3*#_- z^ByDyLld~@w3}^5iNX_1y2ZmD`>**DgK9maG?M2Vvynf27E#@yurUq0Y#}y z+ID-#Y&YH?Ze`C@aj4o6nio>@=V|4hDz=8DBfWw~j$9+gR?%0{;gP1ZFOrh8yS??C z(bRGbgCo?UCIqRv@v+(w1YwmMuK%1PYo>yTGFBtjwA`yF#P>nYW__Ok_m_pbc(XtH z%q0hu_=)Af=x?U~a5-Z5^%~4oKpc!Bm2y1S5ZMKa024i;HFBFUAb^&Sw* z>lfku$V1F%81{$;A=uCX9sl}`b%_(yP|J9Z?wX0blEY??KQD1=MqoK|SUH3OXY4$U zjVG!c*L+ng0o27sR1lXjbB;jD(FK6QE`DAOBXm%4!Iw$}IyLAW5gfpArtic+x*SfC)x)OgunlnS*yc zDZhR(ZkuGDnfd~_01a+JLNPh_z*uY!rhgqqDsob|U-|S|x6$s@UnGRmkt zWGNED784TYfZ~f_O}*HrI>b;`-gb2J@5dvd5Ky`cJ}!x*3z?b|q!T*SWg(B{IWF`c zx0C#lCkYA+a2h56jy}liN~--Rro(Q^&+@?!3=G6vfI-%5qZ+dSfhWDdEmhxfags=^^^}bXDz{%~8ls{0Z&U zOrgW^K!EwNXIgKx!l~40TQHTyF%#&#o#6f!_ji1}Vc44jEImig&)aHbODg*>{qR6& zv6om^*F(fYCR?co0~b_%v9q4l8}~2AeijZ^D|J$Is-c^V%0YGWsBdu2Xtv|x4t{=o z5>F{><@rfARrNxy8g%QHj7R{+Z}yV3Wbxn`SdmXd-rQ6PrE6U=q-t#lM}x1ZK?Fp!8@ zpj!3HO*w2#bfA~j_v%so4;t86YkKhb+6OQXF*rFoZ#u;>JcB-y;+Getz*hGNksnm3 z6}2@T4gLh8SZU0$aS4|&{GV?QAg;&+q8!AxA2oWVq{h~k>#d2&2{o$244>1ni5r0J z0sniIauO&HdSHL3VeesViEsXdY2&{Vu9eUNcHD&r}RDNxG`x2LI zl18HB`U|d^oBIbN<8*mqBBfx;94*D(dR*wR6BznKz7vh&M~*?p0Yr#(Dkc zUz(<^tju(y9RdA7j~6qCpOWK!+CLa|M#_jVX9?Qpp8yvF=QL1@7^_V-?5oayoh0MI z?aBE(T(YGuFL~RMpCLIDb;)&vOD9F24nnW173O&VfC7W*FR*z6)dWl_V$wYd>AsL~ ziH?6|8H1cj$qzV!Gc&JzKwc0k?fQhO`PioU2J_E!k={?i6h3FXI%S!x++!B|Zp<=^ zj71r88k?GEeq()smqS_$465nk%c*cAh0;0DJvi*vETu}Td7^LtZzpcS$@|IUGPLlW z#9!3HPef>wm)(~@c&CC^@G=Z|TCG!%y_`o>-4 zNDwzg?~76tZ=FK^k<1wkV9)COdo~vH4vES2Bi(Q&+HH_bnD3&+PW5; z4F8jDTnX#?9jkDGOvpQXXWH|R6qy;xjE~ff?ZrJ>t|Lb*A08du-B-rODgIIv91K;p z@zY4}#2;XCwp$Lfb|?A0JkeGf%hkR9B%CASubSVrQOly)C2RjFg4=NW(QDZiea9yL zUu%`%-v}ajN#ihZ<|Og7stZ^R{eaof7jRIJWG@4w`ae@b1_=hNm1_qaI+ka9RfAwd zALj76kV>wZ4iTX=H=L}bbg|0SAYiAz>T(x_b=W>G%0BR+3?9vBF+cc|nTfaIx(omD zcHXoZBvnv4>ug|X=+?_x(K7AUZX-Vgd-)w>-|%%Kkq7L>7`Ic|PdK@_y8ul(s|~7A z>bBBqd4+?8BUK>_3({A6Ox|Hy-{FsW&9s+nVwD4|w40FyUQq1^(V+6bO^)b)TP7C$ z%0kS(1M(`*O_;|GEDYacGTl;`Gtdk=7Il&o`a2=kQ&1p5Fr}T716zvg^?b80R?t-p z1!?^B{7gtkhYV>k1O=r_enuHKGtD`z_SidG`Mn%BT|BYWhRX&Ol*@gFYGXRr}N z6~@uRcx=fzbD%ubPgS}sM9Pd;u*@yZm;Za3QJEDUm7Gf&j<^C^U-~(@0v~$G393YU z6QRX*Dmd)W;$b|XG3nnH64BDa19IHKO5|SFJCG2GS$FJtz||hA*i9E4c?d^DyNP+F z{K~B_CZaYP2S;2N^6}WKxy(?aMsgz!GqzaE8Y06Ots=IsokPqdWbxf!WkKsd{D13% z3Z16nOr@dK;*z|QpeIFZDCQTf{0^&9C|jvV-sVc$sv3MX*Sx_Kc3su=v4fG&A|N7W z&7&Ev+2!p8;=V`I!9qjpSCO!;Bvs8M6(hBvK%@n7(q)^e6Xaj_?Eni;zg}eqy;u+4 z=H>hdSFHWeTPi$imB7V7?)#@^Rqtsa{ z2YY*RD_T(5q(-=7Rj%6RA@0@hG{R}OCFr=NSryd2(z zwa%o0i@;+aA#yBfE6cy0sS$IYWgwQ2Oy_Iix#~}iuHWUoc=TsA_c$7?4HCY%L@rxPFwJQ1yBJ&QT?Z@rXqcH=CME2AkG|Zcu9U z+4UuiKq75VT8JeUAA4}nb3Z0+ZfUu8Di};rE36_FOZ+lyRSXqNr;+fFvVz%F`sUI* z(p1zlU0Dvx4q1cC(YkEfU{d?zgHN>|eT{M~Yb|t}wRnXcVtByR>>g2Phl#E*ot$%J zK=-s_czD*jDm7D|;~w>E*uVY#O0~KXg@$}_VVd=p+X<%0L_#!5J)>zvqUb?8aJL1| zKWXf0tIk1?VKSa(#C94g;Yak9Q={|x+ajsu&m2ty_nKXRWsmwKZJ~rc`Dd&ke{3R% z=w6hI-VMly+7CWo>sl$sQ3GB_$P_?9{y3b6nqr@8Qh3uXm6D8Nw@eMAN||#zfE=LGv2yAWM&u1}K3}nW#MLm3gm>@J(S#!aUZIPYL#Ede>(WQXY_fM0|yH)4(9AG5SnjF_rj)Umw{^rBU(m5$NgZ@x%B5Z2H^SIL`-) z|Lo(CZPp=Bkei zQN{lVkEnT%KIPhwzlH<58Z@B1;69bMFf4zYy!s;%F%2?aRDjFdJ%h{R%ij{UpBocT ze7QiVXl{=!Fm!NtxDT0AI|K!zzXh&XXD@F{Y}&aSsFQ^l=-a8jvKQ=~Cq6?jnNA{ZJw1 zEm-+?_0{}A9Y0mPRwWxD(vZ9h@VTZ013^B)<+?Mzc!S2xXB4cXLd zd>Yq(PeDi5TlsEL{do0_j^By-$di7lvLa!vvjM8nhXYQK9R`|R%(b=2uS%|3w9yJg zoDI%g9(#OCtVYE2Q7V$nM`IL9x@QfIWBnt@QYT+Pu6$#_$cy;QH?`jcN(?*2P30jSILwXd7 zsjBEN;YEW6u8WOp?u0~3jaFJf+CZpB6v%2?g`KZ9;monpfQ7ZUzh1OWH(02hE!Fsw zvvhGH60Gr2xn_o60YPc9B`k=?mz*d*Q< z)=b%eeYI5r(+gJy@Tq5(^yxRZ=Zza}pyP z@^*pM{yN3L!JPovYQ}Ad!R3=;6EFBS7bPO40uvTM(UP%&^1NhS}3`(QMI=!5Ez%`qYnh_nZj*W zkf%H>n`q|r`Kjj-O{n!B3s5>VaGIdFMyKT7o)sQu7ub!dity<^M={01R1vB%!{1_| zzxAy6)MzAIb9)l(Q8et5^PW~TlB2$EtI6mhYy42Y3B0K=p3s}P9g{%c1qS`0bem2X zlf%~iqR#sTEgq5&KwsSXEjB5+P)a60^^GzRDOGXb*e^2@oH@v8VAs$?)&8|aIWQ!Y zyXV^}NabKYcJbsW5d~khiXft|{dda3A|A8YV=Kr;UGe40(IPNELHd3J9=6<3wO zFXybKghT(sUZOl^fAJ*g?R)tb#gIBjBe6S5FS@$PgI90eh0xp8YS`v8L5rHqb-~WpRTNlx)pOPo2-VBT zM%Hi+2KZWO1Y34krwSP5o;R1U9 zzgGbOE#l@A6)O^#Gwypu`)Os{)|PymZ?<+4+JMBV8rn_QuO4s_QBnQJi*+8Ta?cX< zk!9?~HoQXd0FaivC?|#DZb+La0#Jik^P)61zW+k&jVT7YJ*iqwuoV%QEdir0q>7P` z7U=fko2?{F?}sDwGEwHN-{MYROdY_Ad;7}zPU$-ZPutMSSt|X(z25gd$Kz&XF$ZhC zt2O`Q5D>l&uO9x;Y7Rd@wOY$JR%+h#ocJZ_DQ|)@t7&Y4l{%fbaA*#XpLaAAZ}}s8 zN&$x8uy)y@x50HoJ7ax%p^I4a6#^^LoEUuh;prE6~?KH!R8z{Cba8mY}QB1cYQAQH-F8;8T_6tXNSWrD$&{U<=AtL47 zjv{9Y6@k6P0P}>esmzg@fan1C_R6giKCmoX*X4X4gkYRwp@E2fZ(>NmFRl;c6xe>? zvhdZW+?@CD*Cfj50iI*5v#ByRTl;QFzjdSWJC}1g34S~OK^EIT-+p_R2&tlDwRaZ8 znjaU|RiT$cZ=+_dUy3F;pj)j*b816hR#{orc5zY(uj6rPBr2egFMZho9l#<1-GM40NA^7F zahT5VpNp|Ir%N2np;wl0P^!ly?_>R?CvVyd8C#Lyy5?+k*14(aBU>6_FjRHtS{=W? z^SR`rA90&seueLa@$mzSW~RnYCN|Wga$_lQdVptz zT$?Gr9#Evh%Na06d}5_M%S+F@j0aIWrwW2j^+UDAfry%c)@_A zPz^CMF!O!7Yo~~=Sk^%4@Ky^YQ^Z++cJ%vZP0s5Ou9^ojWC8s-@XHZQR;`*S`O-p* zGe7-SFCUdTRqBj)3DfBS*Q)vmT1CD3etEBnn$M!n^|W=Pb+B}x9IXBK*0 zQ(=Sf_ZeLt3O+wXk|(w#qZiEbbX`D{+idw`<$s~U$pxMbyo1h=U-Fr7BI}n#^gfb-yE(VA=M~# zx;(`tA=qy4$3bSMucqeoY>qaJIRBBw_+pE$z8dZnd#?{Fcj=;*Dz$;lmntNi&zDO( znJX7bLpe@|(T7QEC#b2HbABi9&%)0}dP(IG%#q#01bk45n@|cXhmV+ zl6LHYyzB5-qgb)I!dfpk{9u&K$c|BRY=@XZ^Zjd4X@wXvQ((9D{2#y&xm-|Cpb9=^ zR@T&wF6fhK&VThK$<9oqe#@^unvyU@a^MX?LF*CKYCXF6g*0v2->1lW>f4lXW~AJj z>wy)ruFc_XCtKozEt)ubY+g;EXptQnG}?2fSbx}&?U&vxszH8H+#g(4-!lS@YmNfr zTb5O_V!&i@8fry`8$-W-5v%s{FTRn$b*?^5d4=#(FZiQ2c>KFTfl{at=}(BLxHx_D z+s-KJN>iCK<6U*!?%Ws1p>5*e<5hxs22J_9w{qlaA}Mx-0ZRogLAe9z;a`4obN!c* zuhE(L1Q^yitR-!f^;_R_+cD88tn zL4Oh+cN85)14pW@K%-9Tj!!H>}@^^b`7| z8j&-u%+Qrlo)xbu$j)lZ)NOw!GM{axBfaF4_|E@{Wwq+WVEIz5@8uA6UgnZy znzv3TlUJM+V9LgD!x5)dXcS_t$*oxCF?nroboh;R7G1nBCqz9_ELm2^KxtO7YLa@` zW4ZU7fyCD6Fj=%mqF-wfWb*g1r+)wLV$KK+6^fOQ5 z8N@t2d7MsXB2iY!6$SMLT^KuA2MS$glu1SRP#%4q%-wma?pTI&H<8rRozDsmyHd}| zOc8Zt77@9b{dhv=@=a_&IC{@n7#}s6A8&tczz8-$R!Xg(GlG6U9!jUa3^G%xR>2M&+ME&g(~HPpj;!Q+;QNJ+rg3b1}((>!~ax1kP$b*KE7_ z;dOQVIv}LtAuxR7g~<>wK}B#`%jABxTjD28U7#iQFimQJ|082za&l9apKe1W{9{RQ zxdn2r*c#qkzO0pE{zycZ@IAIB`oKo2Xng1L>*f1T0vSC)0)D2X)<$z}#(?r9Mxk%9 zo0l2wDyrZbzj2qGyRA58LchXU@@sEAj?NzxK&SRn;zmS7;7+xwoI=-J|Akh?2poqu zK*GAFvv8PUR8X^+sZ-%cbv4a56((_8ZVrEyPR&VF+3#8FH1yCA`Q4v&jc0G6dg%RK~*g@T<+1}C%own@7E}PGhHAQYIs55W{?NXKl+6?jVWZDT?Ss6Oy z0Gd{%A=&!h&vD`Ba~fN_K6rm~Tf%hc+>e9ZRt~$?SxzECURA*kuRI1bq!$s+1s~xm zEeF$nx864puvlBGM>`XqPc|>P(uRe1MSx*9(9?i4Bri6rjSM zI#PGH@V6W_k4N9K`bmB|penQxNOyMbV6bXU#h6!q-fB9tW(HSBoB{$0&^{ZCU*VoqzBdx>2Z$MjS z^}_Xt^*$Koyio6RiMY_iPu-O4Hn%fvYD^2U>nXg8c`#|0)s}pU@M@?PfJ0iVFhhg4 zCLlg{%SsTHuwJQibL3<{u!sAp8tPv(0$Tx03;3|Gu!SL!^m6wiPhUR_>5|Kwj8%<) zN=m}0ROx1(ES4cIY|qM~yLJ zUSn_AuGtSQ7gkE0)v#%iIs&gB$tZ|@)rYp{?n+=ejMVexn}s!G>ZTU~6M zrr;jpQ?2mouJBWi&yJL zT5W*0W~+v@j{{NFAD+M9c@XpGOYMX@YhBFip56pu$^DdKA{pM_#0%~7pL~b!H*&9) zX;tyDge9tJJ+w7pQ*y`l1P zwKKVk)_J1~A$h?J)!`-52|<@$`)`@RAk$i{PRs5$ESS8!Is+#uZGqP01+Ss9O#nL* zgX?};qD(G%o8wr)ycU}cOxYY4nl#>jg#Z>7A7(;mqqzN5X}rf+nC%0#@3({Eeh1QP z7$%U5z>__EJo)vSiF9zX=o0|~k>9fTM-yE~XwL8cjX#HfsRKq7)3-g*gxXgmotL*m zme2XuVzZ7k#TQ3AcSf0I^?bWxBy#sWtKr_<_ySE>Ok2ZY*$dxf^=l~BI^YUNf}l&b zaJM(Fl?F->BvZ!rL;Jo~?)OlbP;6{0!;3&by!3ynj9)(Mi#=Xybz-+M?ACc|O?e)}d$5p_=w@b`cb-j9F>X|Jz|~v{p{mWGw|HKFFL}@R@`UZOXl) zXiaT^$d7s+_lpw_$g+tHnxKgmsuJHEo^8^@>*dw926?vEJ^RB@=f+e6eiCx9B_<@G zUGi7!-HW6UQ&EMjG+Jxn;^3rjGxlB1lt^ng1eo`XUm3y}yQc*^oVph^N4@PZt@N+; zoP*wQ+`7%4Gxd_qvNziw?PpaZfBgCWajw*Qeh-sIH$hW6FfijHw=TkpvSYe2=%_VF zwso_Smi2SCsd*q&7MW3<%6j88PhH_YP)F9z@W|%EZ{II|LYY|$n0$XxXW@ffp ze{L2VvZ7dKOgo;Y@Ww(jT=9J;8v$N!5zMVWtSS1zX0PURN%F;hLi7CgmOYwn{ov6@ zt1=W_AO88nUu_kZClc4R`SbDK4BPJQ<*^}6Aq2Vn=R{tQT)SL2+hq=yUkX1DkuIFz zKE&2r1dpNX#B-)>Ns)d}bKf~cBpQ!Yxw>Rd1aI$G2j}x;bR})=a?Vbw0D7RM$<3Fk z&{NY;jjeZ>xLB4YRUpB4t&FfzSD|b|h9TG?e_U@gyWrE#XE-8%UX}K&zim8i*P~;r zL87|WC53rJXXL1nwfeVqT^Ehkn=FGx@O*Ou-hgX5&(~A4oQM5leev)nhnLM>nwF?}L@cctwG&Guwk?^2qz8 zSpOQc0^0O;GnZZJDs!XD(OOL%Fv*KAPUsje+Gu8{rDbV;5BP5(qHLkk59{M=D|^49 z$F&V2gHUFA#)?X^wTn$-Q`A|RXwjmutT&-o4>CWJ&os>kvp)X*cMJ=Wt2}Re#l|tu zWBhStpW~#TX`kX;v>3P;j< z8O|=m@(rnFvtqA6!o>-LvgQ2a<~7~D`S)b7pA4U5Gf!Z}wnM9)<+=#1=bwrHTl z;jlCUg?3_dI18^>C*IvV`+oUser3~R@IQw233eaNeB{(JD3_uMT5JvXMXNT1naCNu1}9%^gHTp)&9O_Iu-QiZl$Y0rb!@6 zGQYm43te!jO06O$;<$O18P@=9Ajuh$Dl|M&D=V%tZb}vMhj!xDh-_hgfhiiyLIh+W zq(9{LO=OS<`G;?8uy}dMUX(cG<5ZrY7540A*M@m-UgV12ZztIhW)qUTfsA=&b!{Qg zD~Y+tFzm*&Q^{a#gPC^nM#m@6p6;v{f4;)`aFT-!9hQR%G*^;OU$U@bT=mV}*iPj7 zHFk_ayWRYNi>w9(}h2@Ah=AQn zYFxc2%|Q#@^BSW7-9o?k0@KyFd)v^>NHc}X5&ozyts^Xka!Ei#W4RoF8X#>t^ct$Q z>3loXu&AggO1s1a*Sto@(=}Q`HnxRyC&B*cAl4n({cb7#y}WWm>-bl0@TVx+)ig<{ z=tEQO)$WOkpAA+QK`bjSclj6w=E^n}*O+?GcD@*^*CT@KG>g6|G0Y1j7TK%pw#H?C zue||I*6Vmb&+qSSLk`tXjG@V4t!bpkbpe*(On`={<{58frC>p^`D@ajOcZWWF)@*( z;IflBg*Set8}q8siy?mh714O|l)-Wmc6R4Y(ytAFIzAYz?%B@ip+hyG4lWODram@! z@}fx(O;Q#U!QJvjL{?rEa0MB?rKt4R^XWZ}sY^ zavKcgY69USzy6@siw&=iJfeX6zBrnJ8NnGOEm4G3UKIFH2t+YMz+kB7|GfCOmVn;g zUOkoO(o$c9b8dM%NxRW?59c95^YE?45}kDCUoC?nHxQM_3oKG_@epOL*_;oXhHBa3 z4P~#0<4@v)d@HmZSV--HI%A>egd_e9gt&2pnk;Z3B(c9xGuGl45t`Q6aT+hDMKWL* zky!+o2!Vf=SUMbqGqZ7~WUauHbb${e?zoEGnmfp5y1}e6E(hxPrut}jnz)9ug|n2r z7Z$6`!f_gO2e1lAO#ofq|g-w=8p*#%QeNUH#WtGC>^m7l1gK0{6G!IqGOl| z;lg(8%70J1LDZ`P)BWbqnpOkUvIMuRqPM5^n>#vumwAp>vh}x7x$G(H-|X(uF|IbfQj9kceexjGMzbC0wl3U+ zgg8rgJFTDW5y|#tFnY$I6|fy|hQ?l2(6v-Rs-agyn_BRqqn*9=@7rZHDEy=oxm6r# zsqrC#3rwr{w;~a5!H@pEx!>sfAlNTv+7M7POq86G%$a3sP51ze(TOj(w?-D-J;a!L z(IG`6z!A>oaj(DtBSwnK7@j4X_g^AZ&Bda5GHg5G4(jezW#X9BO)k}t5~61-_t27l zkImE)kYi?dwtG~kRSCBw3i{RenEhQ2-u!9vqrI6iKPXymOhchd0+-62#6&V7^q@m+fE&xGOO5f`RCMHHSVPN*t@~5%6d+7rn6K*qw zSa_rN<-k|djnPN!sLGlcQGWn}u&*a^o~})b-^kQ)B*n|!vK5*^oE64;(cDA&eeQ-?^-H0M@Q6SA{}deW_xP%XHn2&LD*|i46;f87v+PMM zi7Ey^0tV%bvVQc~gbd}d&nAmmgoTK^?Y_mS(?@MFJH|myRFO2OiBoGjI2Hq@3to4j zI!6e06AYy!tLd8{_~VtJwD~flP$a&mkI9uoxYmC8x|N3}G6*nl4?!>^e9HO$5X5MS zsk|d7@Uffk8MVrug5$FPmLTHo{UK+tJNmDq{K*+^fv zqG?hXqXM$#!68L@8w!z~XLYl{=yK4ME+ULX+42FN(mk_74ZTl%l0}MiS>Tg=$fA z=yVI-cqlr%$pxsrmx#8zI?)uLnX{TcBWL^vc~^`z0ly&g^W`*+R5+y>IuhZBi+3l6 zA}raf3oN*^*`s=2@ACSPw3$v)V?uWvFUU9__{|B0BqjgatSZVb!%zp%0{~4LDk?MI z3kp$DHj3L^`t8g6ePlZx7T0iSE0+W`@q)Uoi_Jf02zJLrA-1Thnfn*anhOiQ!gw>@k`sSK*bFqKl zoU5Fgf`WqA@hJuv$Dsb>S!uEo#!F)-i^lRE64`aHwbFoMz1CgFp07uvH~#XD!BC&> zMt%TbX4Y_Fbt-3@+5Rgpf=Ts~f!`5P5jl4L6Jub;5YSJre`mnc>O19c(S`3`_@kv5 z@F)pBjLe{Pwl-=9eDsv-6aXA3pBEyvYp{#q#>N+TCD!JQ$0{6*$Gz5H=C%5ekkJqw zmGz%>MyuJ7?zv6Q9NkB?DiU$GuHjc-mFTmo-&u}>$tys zzcg%zMWDDF$?{pPULvqNO~$ykR@&gE*4s@xnr#i%y9)xsNO4*yP;dd5Ep9ESO{6wT z(^q9*VpzExK9M7dg&m`omekE>OVP(S(bjTIl!8_{`RE4MWV4*EIPmylr5j2Ilnm;= z6|S~BmTA#&mL}Kir4G}xT|~lQKe}3{o6fq|=nP36S3o|q0JV2;JKrYC!s{jImqgkP zi&2PS@xlt`c++N%5k#J+@;!j};J2~SU_wHIshI#;@ApV-Hk5wqC?W`(YL`u2^Nn&& zC)3ST;|gg+>`3p=n)tWB86nm>t?4+YtGO-5)r!MkI)YkSe04N&T0fE&y>o<@Tjol2 zfj_hzYSyJ`s*WhMq0LL=mSWMtP+ZlfzG#x_{*tmfxvYv`d#B$MS@Ki>{0_>{VWxVP z=S%BY+{`N^jdIHG$OlJ1nrrjUz6KQ2{`hU1^zqO(fE|nxGNl~t5RN8yfS%%>HAGdb zX8<>ArIX$&UB>}IzSt}q`DZcj9kG*Lbd)(rUcm)1ik;+}$XXxiF=-lziGv|TSDa+f zbejL{8`_7FQf#3XYD!*4b{~#Zy*lKUeT%oGjFJYZ3><~qF%Pu`G9h()A)eK2O@u)s zHcd2-zx}DJg~jcdV8z6i8)-F_^!L{ASg+wZ$DBa^s~S1&+zNEjVH*T;89G4p`Zz(3Fpp2g?KlmdR{=B3u=<&{ubIL?3h zkEa#^zhF$K?JsHrr1r`hyvfPmsKdQkUK^RbYkt~1iJl5VCpg^VSiBXA#*4M8_w)J2 zlH*4Aa=b}6gL7!`hnwizhDrAO=mJ%+2yUcE!wELQZsP+pF-++1^PPfdya3$a-N9a{ z?=m99yp_Wlh3EMoZLZNVKL}(`Fs(8FrSTAYW~xx^T(MHh-irzFwR5eaaACISNdHFc znMf`&=fdODq#0DhV6(*^hf9kNi!*IX$z~XlFm{Z0*L{W!pH-6UqLLa=#WK6Cg4uyf zK@6Eg@3c5+4go=*DZ%MDXafA-EJmO<((}AJ^R;O1%}ToV&&KHrM9l{3`N~9ySp$ts zHL>I45^Tb+$q&1G1V?>l)b%ML4(#~~$U}aEkFVzPt!2zrH+||~=Xirzc{NvGtn>EZpL_|;D2U9@I4=ry z<-ZaUtW-bHm8w$l%S;mrc6;+D#y)q8tz$WQtJKfhbM8NOtU+zrr@m2%s+b-q@D$hI8*`7r8&>_Q)+4!!I0 z(99^y+NxdXAXF7mohG`-)mS=(N5N;Stq# z)UtU~FOrO}93Gwv_u`yy5&V!7KEN$F1Vy8D9sx%9^Kll@44Ee?OqD0kI1RXD65&Pb zd70>Lo-TOXa2m{)rS8QszZyR2R;sg(7gQ?dNh2yM zYR);l_1}82EIgMhPp~SP$#djyM5QNjzBf_kKqzOt5w>+PMDc|puU{5J*w@xzg8HH9 zj&;Qz%&Qt!aUcWT60%xE-Lr)vo=;1l@uNfT^LAPReOX?Tz1ythxWt80 z2;&17v*1F7VMvR}W@7|}i_PK-m5H3ULMdG&LMssWBG9^?Ex}KcTD)mK`T(YG!qd zbp8GegNGE2%x4D941xTrJI(-%T2z$%gAb<*R$4Zgy!MVx^Elh^w!gc6zCscX52H2| z0NTl1z5*(j&9IyIC3lur#$Xq378(8ePeg6DCzj7@EQ`1X%-~@nJRDSE;^1t#y2A;& z=nH)`Kt~ZNC@}J)jwpI*D8*_$%->f z4Mx2V!{X=aqBUMpR@TIKa*i2^d-M4c@AVaIlLLC8zj5~#6lQXj51edNc|1yv z_7Q4yT2);EKO;EbJl#9vqg z%gJ6f{NX2&)~vg`fz2|t!sBsqHnR>D?fvZ_j9xHCBKqk^7TmQRK{>z3d^j@?Itp>VDjLghTnFfo+ z1xJ(DhjUQdMe}LBMKnII6n*<0_&I-dcIVrk^m(h6P3L_Cm%^q8L5KM5!6fMQvLm~) z<3JPpbHO!@!;NVDY2@?hpHSi~bCRfsSb1^(7w&kJ(}FeLyWhM0ym0lX;&F2(1g%%U z2W6Vzz{W9n!F)aseZEu!Zta)gLS=b@U$G~31;~IE22_NMB(ZR_via-NM~KHJI8EW- z(1rLxVV8&&22ZOC$5DpV7!nqi_i(-vk0~d$nk5#p0#0XjQxz}QW_>*~a5pnbKYes7 zKKrCL*d`jqmGb^->Ari;UaXhx&2k(pnTCMiy5}p-G68QijCwhH4tjx^C}$i;6)}A& zwNgH__dr5C;{QX}a)eUhO?zx&&LpGLqQXoy-52#PZgzcux8l()4{5qssMF9FjJTPs zmOvgI)52M?yWyxao>d2jj3gd#rjLwVIwbtZyrE16-+SY#rZ4C}rA*|5Ka7Lrnb0wB%5PJsx!}guy#2@-nW^D$ea(wsYmU?B^gD+J4;W0-XH752 z+MQMCa=D&x<-0ytqg(Y6D*B>0vMPwY{Jfu%wk2IG2$?cn40FH{8al76U0LLb+S0Xi zS4vDwq*bd|QIjT3=$R*EErdu^aribOrm(zP6 zzelhG1Sb!o2GJ%WiqxxnO4JD5*uo_XY2})gRK0q2O;2{`n-l4iN5)c}8g3`;Et&i2 z=`Utz5k{&dRHOX-{NndNdEX_7;%lN!uL=$-PmQdCB4i4|FlYDP-El=lMSjIh3gH0| zks8(AVV-vF-lY+j4(&S7^cmCXp$8tKYp=dmSJJU|{aSkYwU;#`i?2WXn%cI}PqmW1 zJzdk0WoBm51?OKtJ-YSKtq%^@u%Mj3aK2X8!}mR`5z3E0|A=ONH{zR!sm*KiJ31rxcd=4 zO^xE&iBQoOMaZfsVyefy+`mu%r>?m4iYV0^;f-x6+cbR?#B`56_?Y(HJ0HG7y?gbh zmMvPAUzePcOsm$cqPDHu(tvXZ=!y}+S%I@#zH+%Hq8vSPwB~%R2ZYF)K5IIiIC+9D zzVKpA%;;|oL*%%2!&+LjWKm&>t7J11u(fLR6d?-*lxVSvqPVztT2gY-qk@nnsNM)~ zz(NqwT08~*WZ<+jGBPwjFodOAvu=$h>H~L$$Q=+Xqj+P_-aWK_<9bSnPoO(*y_1?X z)sN(H?AS4yJ#RLprKi!z;Uj6#xr6)~k^r!%{B6c>1t)V(?iU0MIlgA7QBw#d6d|j+ zSecub8v#+1x3XR#`~XoM^inw8f@r|87|zWpX(<#F6Qh~m-+jm3bn}fj>xu`x^9}g9S^&Cm(&15)%BMgZA#*OVegfE7-GdPhLq$$&;m}rC$poR;or! zK@lI}}xM-O)8C0FVaI*1!#g$N=BLy|}De^iSG;t!~I*6*{51ji3c#Fqpo_+K&K^3^CQ zC_<(n9fBkKaa;1Xp@NXbtKJB2fCF2$YMEvp56%o`_;8LMIb!4)L<}NEMC$nT%TF~u z*rN|Vs;dV>Jsv|2#AY0K%ed2YZoYt6QxH4XsCH!XcmxTE*L}IyLR{cY!JrftCp7uB33L2=yXBA9xur&*{McFK@lG0<6A<$ACSM_uSw4+fnnIItZVJG5vW z1I`)X_mBgQZQX`-G;8jx!qU>xjYUO8_lY^RQH_RzB4i37h1iUK6C2-sf{-cShcE|3 zi7QvHr1Xq*8a8y87KFEcz54W*Xa7PQHf#u}2g}dTr}>NLYu1g3nFvD;gthVq#8I3! zb6S3GUhWAiboL@#Mg~qo0^_Fuy60asz1UU zAYNONwrI}E_}sc>Yt0-UPSJ3V26krc)C@WD^7H7jF_&qUi~f+Cn@e*S&ULL_zqTYY zGV&uqz+Mn@=D6x<1x3gdDi!kc^CyXuc3(lrR0QEL2gHLgzz_!$?f)>;fOrvv4NlXu z=gy{#of$N0_$bYg17`ZV=bNG;TDoFs*}^3YOGF(%!=YIau-&SM6%-*;sFWxzEuAR{ z*?%BzQt>Xs8}RcgEGpCr#DOT;C$eeDiX}9C_H=^t``9bT`ZeT0Di{#LnZM606#rdS zTwMH6ac=Qu)w2rW3>7r^EJ19I*keNm4yl6(<_h5rMBzZ_DEO1b$Hh}YVS(mTcFBd8 z=;kH!ASpRX3n`UfkiWB_pa7=y3sr9_I0+T~UO^HCBRB~?9TgSzZZ#X z63*Wj{9dwQ(*~E|z~2@G?B8M@oKQWZP*tG_nSw%9IkOZM(W0e`%9bo&QXCZ(HJL3D z_o<#yP=riDL7`Fu(K}YJU5#Xzg%J@EON)z(A1=-<-llp+;cS7@k(~{QC77}u8+7iV zLe&{n4@oIWWrc->=^PVrsp=7h$^vEVp&*lx#fg9KR~=DhkuJ(~Wr?Vu2$>>e3JMAe z3SmgdsxGSyigQ)8ZQ(DrhzbVu8;f0bCPwQ(^34XC(3G3YKfA_&)UKFB9v6VxVJFO`ycO? zBi=(rPuK4s#E*&){o^utJx+oN;`%)Pe~kEC>~I@nxQ#_#-@%=uwkT=%p4){vyvgV)@>i$$t(*9R1agN(*)GZ+ z-VZv7dy;gn$3E0glt!YY@cxv{d&l|w{a)U~Q0GZe@czR@fy3bEy( zG9kw})Z|5lXTQf|8_nZV#@|Qsf4mrF4k=U`fC?ojynLM7y zxc-{_4js7w6Zvmj9_y3*PMf(8Zsoa%cizU=4d4Zfc`}dZ-f*rH@7jg??HG@70@u5N z+lcu&iZ=pm2p)B&%dwcaltdE@xcmx zbOpD0Bll}Z?$-nSuFbix>+zUDpSFgt>&Ne%!w0@PJWuNJxTB7B{QWt+PFy@!>hk*# zzvE(_CoOqyp5XU5!tL76bH5SSSH$l#m=BKgxL-~(VX4LAjc0b?Hl^}G($gpP_3F8e%4@q&v*e^-+ihE&%0ebuN!jPJ@ecfg}nYcnb?x^cz+?DGZ{Qqxx8OJ z!p}Ow?T_(O&q1CCe-=b-Rh1Vl3PRSK7rKH%xS@bILIni{eOxUF*)J7A$RbtWD<~L( zB4P>(l@yAQor!(S{u|ZnoM4fGSv`t z27UN5>Pv!RcyrAJ$c`cBA4G1sGjtROxh;^(LvFpnV>BYJZ^_@cqdVa=Qi!;_wLH?R-OB8FZbti zZhsH%*JJ!0e(%5q<8hJ?Hh4xoZp#K<0K|Pag4}w8X57D1$!*}$MwFY$t>+oS6Cb*u zZd^a~UNiZ(b9p0J$&0EL&x4KpJ!JoVx!-^VqI}BjZ_3x^aDi^7H@6$(QjN!;fPXvA z^8w@NG2pp?PU$;$aa(a+FHwFbw@#%xUsI3kLz|;`JR^8RfZpUf{(l+&{{WA1A>V^O z+RpEU{%*)lc^z)%XLhAidfR`IThDkkj}7*V z4|&eC<9^u8?ZRCCo%_B4f1kzQU(aJaj@&x8?|9#u!|zg;@5$jg-j7Q!UPoE{4%vKd zB=<`R6Q4QUhEd!PHF<1S^1I^QZ{+W{@;l*Ohw^&ITG_~kHlDdUoco{^*R`1IIm~q? z!`MVzUx)jmF^~NL9^2YH9_MhIv9_m)dop>SLcc?o2R-2mZqs4$dx7{3Yx7~=Q!50@ z{Jsi-t*^-2!py@U4q2HfxIS_o$f}w(AK*w!0WZrPsFzUv518A)!%CoBx7ifcFeTmz-8x5FQt0Cxw{0fiI9`$18A()YdjHz=-~@O*&5K)U!2 zPNa|U7s0=cZhTM?wCDD9gvJaJ*k6KAq|O#17^ zfX?)p9EjTRt$DRFAb%rwq>P3Ex(1d~Gk6RTr2?Uj3OV%A81%XL{{pXi z*LiKUdhfoEpO@hE-D)N<(ZM;V3b`Y^l-lx`byOxVK zoizAtjLAzpHmC~*R*0fA-DBV+uDOcb7Q)5M49qq{$g8xmhv<}ZnJ_qE{V_yz@N#}m zU+#37IL@2X3&G;CiRVAa*ZKm4a6WImBYb|lOVs^-6ll2wP z`+j_Y@UHV}J&A&-?=El#2Ac~4^eW$1or#uJ^+6&ybFS-7=A;7w^Jdx!5K+C-aR;R?5e>Js}0dkn$>J{~OoB@=SJ z^gkXq8MN(MpWogV^*&Zv2w6qx$aXtK#E3;79&F$f?@o#JW5b4T*Kh;Z!vE=Oo$upV zd@(}jw>y3Q-$)V>BRb{J=D`As_2>nvz1ap4BOeY99MAX6wO5oVHcAw5(_4cOF%K8x zK_8vp@jkaOH;z;CLY+Fh`l)-RiDqBm`2dce4?tGo?Kpdpf$J~x^ZnistaGDdiZOuS z9ekkjm^k|*VlL)VG|tRtjP>U3L=NU_8e*();CO`FCJS-Am)vdO`8aGm>xVuJ)*)ir zfx9w^7Au61iH_SVd4faycNV@vI}t59D@iu4VAH ze~7+c$72BfC%Bfw0j|fPYngb?zeKr5l+h;l#4kj_VdiS)q(in(@$+96e{L|bhrB?Q z2o-6y2%__uq`IzzQWlF|Pc95mnkZNR*ZAQktl%;e*G@KY4NiwmMLVbZv^`st(W12B z&0@QaHsjrsyy}KX1V$j6Ow`dzEYdbQeGD-aB078^agKPn_Sg9Y?cxJan!)?4Ss-H2 zRjlKF2?P-H??oBrahTR~h6>+>j< z3@i$8OBX-&K-7sS8#2OXeJ1LALX;JHI@$ohA@@cToAkq?{(t+t$66+yfs87^jX%$l zn3-ZOonxWje_=_@VM%M~64p~1dGZeAD zh|hC2@Ig6G!ty~1nfnd)Rf8axAl=Y~l`&6Zh5M25$!Cd;DrQ*N6d#q;{7Gy+9Sn(n zzQ;tpiBFkE6ZtJOc*-}-p}=%k6mSA@-&l;dZfKs5JIfVmuK-J#DO zwlN=Y$eV3qJUfc=6}k03cC6E^T0cHXZUY6o zfY|UmF+tuAFd=a=Mil?=Oma4QfZ@Xti)|gij!>t?Ow}kPt zuQIgO#hAiJ?pX`ZNRm1BjA5cZSKC+zU-BW#E^w%W73SOk&w^wI7K(qDuuq+pgv_eh zHWbotTbv*`bwW9wD(M~W7ZbKsfal=^f3MGJcejOh=TLBwS|HqO;@W@t;mV8DRv5u`}vQCWMWE1*yJv=4p(Cs0tV}i`p7k$RF(r0j}EQG9s#rqC; zA{kgDX2&J5?f_W!VB)k+KAUSo1@!f`Ci*AI4_D?mAv2(O_MOKyEN}qWtovDL-YSdp z4V(W97Pu!paS*t>d;E!eScLSF#qKX9|6*kiyd%)$7!D+?jBI>BYciiFkIK1uH}E{F_zgo#tQze(2C z#s+2Ow2F{ZzKVP!zA1J&GBcrzj*z}6Gcw2qfpLG$3ddsOz2Kjg??<{|W)nyFpg>lp zaD?q;v5hl4iC+W2J04>VF)s%?cwhyLiWRZxK1&XP^|tR?xV0~bth4%Ds3#UldW{P> zVvRpQau8={pE~OZ8OIxpvv_-&jpzOeq9rzY|3%X4G$j3K9Ue5dOURg`in7thgC-Xc ze3AZXLA+v@kTo#TMu?oQ(K#s}4>AW3S8t$`ZS&LCxqdh-f6ya*!3;U_{}$g%a8!SZIf_9l`&>b{45+8t1RVynZ+BTC#nGqKRn8I zBkmHLY_>5ozygP^@$bHe1`gVKCAci{`Eq~3QaL8xuQmwM2%GPw=ldAQkYTNv{;|24 z1KolgK5N}+%4XkQ zW?`R#zW6Hs{Eno33dHFgKl5|H4_B6{2wCtG4ix8B(>y@dYuEAiX8sF@P8t3Z&|f{{ zXM!B@*?_FX!n2!}CQwX2Un33}3ykzWi5tyvL~gO2Rb&ORf*ZzvJCU_CG~;acp$w}y zU-4o$u@?IKnK#)!*W-||CP3X8651C`H2FWT&kIAq$==~PRM~8IAdbw?;n`-t4zsgQ zaY(e8BnQtMlp1WON{-6Fks%PS9W(TQiwQo=#{Jjv1VQ{Tc;V+tc&A4L_>_HTbAlWu zdr|yXnh?wUV$?BNGDntXVlyyB$e4rgV{?2zmoP{45`G497l82wf+s`7ds(0;3p|(Y zTH*%dSbrq9C=Py{M*ZsS5VDSBEXa6wi}lYkArW!ec#oc(@!Tvf@*?DrDD0Ln2QLl~ zrT=@c&&nY>1gyH<6GJDDVFRCbAi}WguMsFvX=j@gs&koYpbrsx(yj8iFZZcOtSF~n0rGk+y}|k7;Xc`zXzRZ zlwk!A_}dp1Au|Wt=^gy!#|i{rYr)IC>Vk-2o`m=E7c|!+Bgd&O%Op3^kPIlK10cyy zy|9X#N^+ID=i9iqjVM?6`OQ~ZZdUQ$#4Z66b7ZH4Ia|tsn(>}P#pk;vuPZx*Y(Byj zNp4S!1THa{TflG|bzf`Z+ShEphfxWVueIQnUi0U;O@it{a*%yW5i*0APyx^M6%F|M z68%rYGhPt{qKVFlv8@+(WY&}x9FiUVOqLE2gJ^C!*)!MH!-Y2Qe@v&XXrpgt+Eym( ztz)xY(`0rMEJ8U$uWTH&ac@o8jS=q%NBsE?uj{ck?kzL%E)bjBNf>IBkd*}4r@ocY zXAg>X)yHR@;m|Qk5i)bAUjfgo8>Al~|36^!86a3Qe5@*C>`V?efOyeHoA&uk*uO9l zYw5uJxWMkI)w96hG>^=?Zk-w*XjJHPu^)+%HD4(PoMc66VZ>t+mx+Sqs&jG2t+w~XlfV6|;mJ=2&E-F0~X!yc6x zF5FmUA!M!!cxKQE!r7!hlJG1jXy4#EVQ4Yk#sq~Lk*g~9y3$L4`&V3@9JJA^F52yqeX_&&0q zVSc5>eac_Z;O|L}1tZh}ukvU7-AeKTsw+ZfU~-vVc)rT@pZyRH7)N|3shJ;&c6Mj0 z$f0CC4_ap%ZAkHXKPZy}`9#eX*W^_oA@djq1T&2`8*0i9Asb*&zfqBZ8NZ$A9!D$b zdj(tPc62$narmiVcu>N780B(&_o*p<_9<|><0bX+6G#Ngky=KO^9DLqhLFkNcrG=0 zy>F1+Km?TK4zT9FxcC691>U@XD_L+XJuVv#|GIXDw2nI+7 z8YnuvSsj7hjk;2EWnykY2lIxFDh@c*G`lAK7qQq>?=V}<<{>Yd^mVYoq#9OW)S21W zygpC@*iT8Gy}@V53?b&xVNs?kLS}5yA(F;k8&!bFd}Z>UIX)ZX&;acN=k$rB*M&pq z^ET_SlX7O8&ilb11*YhH+KtQ~4&ue{ZQQ%x zp*Gp&(!XLu1YJ(T=D`d5h!NeO3}Rs@%o|IDRvFxZ6B z8R7|R#HmIm2m^Ypd`a*67jfFLlPNdO#=Xr0oZtu2naXZEgv_pzc#=;WR|Pn*j5Aqx zEt_p?S=kr1=&ww&ZnWVPAxpAJXxE-AZ;ux0I>Xa0wf0`g1&jETop7Q z){8~CRcE1M3_j4g{;-KYj3E0~dFv|s!sY~yR$M1?SWM*v>_nw`-=PDo2T*YsiPo`P zLO&Sh=yk-4!7_z>C2_o0Cng9F2o1s=cimVT5yM1Qjd%JLR^iEo8#NrmdZo4GCa{n-x>tQci zG&;f000+h#Vx|pZ5<@lM&)6bP<6i?LV*K3ZsruY7Slf0Ec!L}yWMxjRb0#wHhM;flSPsu;aiJNUZ{{!{ z>c<`dz43_(+NaK1LT2amFv8+}h^6RpfGjaeOvNMt+kU$QVrJHf*We4usy;gJ}l)8i&0LB@nYl6b3jeIWo3T^!Df4 zY$l}&=aI52UTAIczKs+fuoETzpuurCB+5RoajnH%tH(v*8aD3GgB{<%jxzK}oY@?Q zi?5l}mkB$u0NXn_FRX?J7 zec9V45knG4aFWd=%_m>jNi)*Si3!1$ly;oajUHORvQYON6W3Y^QI^jEtIhP+HFUa| zcaw#Lr@L(q+g=I-R2D*Zm(BalM*6WsE|e@~97=!8Td*5UrN zqLcOAr&d{5hY&@=4a2H&6MSCpR)owL?4}AZI|=h7afFzh^K5L^R-$y+F^3HqArq6o zqmA2Jn0yvemR#lM=dvJUP-b!5t>Q)<2||G=D=N{xa9nJFI~}Oku9z#r#9Bwt`rjQrwtZC`G#t`%}OX1mRH+R zEL*WG>u2M6=^-4`+icuxb`yP!^s@s83n&fwKg_kw`Ud7$c`43h{i@DS)7f-f)*&Ln zCP!7rpoO4W$j}4246aRx?#%?~zF)j^X9Im(&jLYtPPFHM$uW$hGqb^u7T;UdU;fud zpI7?WqLqb^^|X1vnT<~`+5z+dRt*dmkaZ{NnkFYV6!>;hRfHtZ6pPPp3B|k=&%)gC zXSxo3;-LVWolBI*$(>NpQLdN|N>BOO6vj&)f~&}$+4KiCMZ^Bk%jW$^D{|7r^>3Tt zo}p9xGDuKf02$xSh)6#dZGPJ#@zB$0Uu=;p%On#Pk9PK{vyzaBi36_0ZWt6Zgv?*y z++h|c2+V!0nz))Z+~{p{%o0O1tWfB;b+Vme-hgXB+{N`lxDO+y(q@f?{v@8=+u;0; z6Xj3j4sZ67&(DYt2$SrUlKbDTL-S#W=Ea{w$ zn2plW&t^Y$5byRt2qIc1**N_Hu|dWWr(y&ZzMldchU_pBKTllqBDoEBwwh?)G|`UM z0oqtslxO*n1)sG}K9?XY+n-r~A@g8}{;;dlxRpilGTYTwLc98idd8U~W_e6}j0P8f zT0G-Qo%S~qWo`iO-y5({94?;yoRwIx;yF#kpJ)91e6jc)si2-?>sK@Qh|F1MTHUA4 z9zupe5Cy3^o{&%-B6KVl^|cT1Ja7rW1laf>E`m%QIk|43O5a1GzDpe%SBO8al5jZjtv3O2HAR7b0?|#3=nBeAj3MR*#FU8IVk8zG zQo+pQ4cfb%4Mcg9xd)uc^>Vtwv30W5fY(9re`Mbe41SrL#d}N=Wvl^lTcB}^6a}&W z){?z|*B(Ip_m~B#$rv&oA^$t^j2>R^fUySGJj%em2(;Z-JadV~{=$L$H}W6d2J;De zRvf;zi|g++(GQq!5ObQfU|dd>92G*yMCXHeCDWOH5_SM0wuZSb?`K7baxb|}SB)<4 z+6E`BH{cg!#xf~~j76y)&Zn~uB+o<`3hSS2S{CY!>ov@Qz%?OJV!aU0yc1i{VJ@!GMSZCmiT2SVdhUxy}bgsqa8rv8hIcYI1V$Y|r@&8Wli1lWX?B{!MX}igx{9>$M!90A72^!MN zz`_vLhikkp{oT*zigw+OgrG9b>-_+0#-rQCx>`imGemm>BC_cg`wtd_m;0HA7&oLv z8o={rme)9z@!ENx=d>?yP<++KxFTE>_LhcBq@ip5)o1)XEp4h3$Eg6gB(+^lQNX%FqwtI8QEQHay$Mm>i@uC9nfb@q0_?-W%D9OklYoXe(`fs zzn3>(Z?5!ZanG}|$C&fOJx%TiB+%=@lzW5M?}&f+JZ~P}?*pAu5|@MGo>zQCF1PUE zC7n-_>7Lki>~qtROBeo&_ns}T8Sm75W-)dbe$D~$e{f_IeTdlip;}+8uLEJ2C7yen zbz67y*n9NHTY2NE!JBkvpE)&`4}Z`R;r{awoY`i(u!bR?Y{zxt5cvxq6uhtN$n9M2 z&9!m_oTvCu0r5JFWd7?w|6y%HM~a+$KZ<9-FbWpc6S&;(1UK#VeC$8*ycRH*Af{i< zgsh>@9_87y{wvzFN5ZpP@IH&ZYADx@@%hw_7(7(;r9ugr#0h`}Qp-YQ>5zgg%CK0s zb4m-d_t)nM4PvlC)RAklk37LnPl|ijRd8Qm(;Y!>>&7I}o)c6Bz-eS4ZrsPw$16*_ zF#fr|j=AFd4t&7D_Xiv@K#@sk`trf8$ie5-=DsRnC-xc+`PEhCdE9{yl(yETB4i2* z3JMBgjZo2-NY&K}3JMAe;S5E{6ciK`6ciy-P*6}%P*8;I6kv^m*mt$m@HjT+uvTkQ z3EKu!=n>?ODOp_&m1Z>3o!5lIDZoluw!$684eeFemIDT5ohgL=n|4qcj6D#q4(8`;-(jXGTY^-NjTxq+!BTga0X9=#%RpE$wdIMA0?V|Om3*9Gg>@PC z4@_)f{Rn@qA);*dxfYJ! z8+?}1xg2w#op2c_4ncpw-2g#eJqbAX@LE7bizEy6Mf12GB71Q#{UM&`6*jYl!U2|7&F5pm;Zi~Zp_)FGtTLv$+n%CTgU6Kck%ZfBFMn+^ce%|?Or-PFhvyC0jKQYJcjTK zUBv4N_tf($80%4i#hQ$5U!%LsNcL7T~goXZpaV&0@ZX;{j5k{z+pRD|Yv ziwATMZ}zcFxLWvdvY7mzkvrc{iq7J`fQiQeu4kb`8>}7?h7Sy!&o3nX@K$h8MpO#; zjC{iVb`G8H-{o;mox~f*J_p(gv+5b-KeD%{k3rPn-*hV(i#BnTH#s;jBVOEmvYyHY zPOxy`P2}-M_TKfp$l)yQ?KFB7ol5k1gbxEhlYRgIZ$eYTV*samf1!sk9`7(gtIgaA zIEflQn^G+QUZ3Y|qSqW)#`7{z`XNMLN2H45yqV!W;J25@b$S!624pN!&j~)PCI#pN z5Eew;Xk}t-GI-81MZ6*T`@OHtZdqWlU0CRg$sL>+ z4wN}M*I=`n=AgK!EuZL47-;WBUJKgD`(bnVH^~Y9>bt1QLQud-{bq4~{1T+@WIhzU z>*x2`LpWDwq4Idv1u|KNumq&f#a@?MIXv ziuZmt#Dpw{#eJb1+hD;CE|K`G*=H*ui)PV+lLI;1=Ek}%1e5Ja2{QE*i_b1$J%G0u z>P-{(A0_*#LVPiRiTlwIE%qxcw6Pb9xUD{OjGua^gm|M%W3jhCbawFB;K=k0xn2t4 zA*Pd;_|$1aM$93F`k>N2h#31BmLS=E&TVHaAtTl`?QnP??IZ@=OBGa*&8)jx>9O!JcCM= zsyZP961c;W|7nQ>1NxDT%{0@YNp>X2$;}9A+j#y70|e(q8z|HhvY*>T;Ah%&x1B1^U7y>=vRbKBn+vm4ZKe}x@ zgv^7FLO{qav@oBqGC^eLv6$VE`_9=w$edRA7uncYvmKaV8FYH2ajUSZOIX3&j*$)3 zD!06m_GGQpM$ke2#m0Nz?ZCU*N&48s1eXNS;H?tI z{#pmeeTacQ0IntnOw>8f;CngEmHo|Pe@6$AL_K4r(zB6}b#P!%zqZi#iIwP-S=}TPv%Zr0z+X9NP{-lTG5KrHFB0DOi%LkyC`kP?P3Xal0s#q5 zO6&s|>D0H-j?WC#Z6^7olU&(R8{+*pbm~JyrMSvY$SPZ`#or~@2NP8IR=r2YV}|u~ z;Jq>|-UojVINB!Ie10yA(Tu{bF7IXvY~F1a>rIGmtN>PgQ&^{E1Q64~C_yhX?{*38 zaUu?8J)8FjinQ@xo~wr?w;BDLC9%F)WG&(kta*d^W|@f%`%f0ZI)SH< z=^R#$+Z0uv)r73XfqI{&GlT#71N>}Y!YVT`Y%h!V?UwX>m&xnrN_ZypM2|=wN0@T| z;_w{&hly*`b>_)Q32n%9;JxgM{jTxz{YTsnP5@DWWQIeFwQ(;FE`eM&YMQJM@d=%Z zxq79Ad+yQsF4@A~p$HkD02WEE6K1@@6uh2j?;b!z5+kt2Um( znU2MdBePL}%j#h8+JQQoV?7CN*y+G~+2P1=QXi)CJ^Vi(cW^9vORlfJ&HDo#NX-)5 zqjY+>ESW&yZ!_X)SnlfC68uPEg36eWY^vn?u+g**a?Q(hy5(BEO0XcI)BA4)jX{Z| zMTX6DoaDA4y~Y-Y-~TRy*PZXcX6Y0mt8L@n+v!XlGaQMLd7>zc2$@~3bWMQYYFIol zu5+N@Fji)am*4~Es|JAU!N=i3$SRxFA~DgCZOQWo1=iPGt}`iRK0n98&;_Zs+8}i)H ztNTlfbwUSt!h!K#Y=Y>ek|e*eilQ%-k&qE@ezFJlPCw5YL2e%zUmTP8Y%Ai@n@Dby z6SJa^k0n08tIg9TPXFGiDY`jK$jS_I1A1oi6C_DWiY33#R~FlHE1el|cua`S*(W9U zfecGQ1nbWCsTa=T-EGwQi39Vul#Jzc8&3M6>}rM+A*+m{Mc`Kl>eO>ehRG~q;rW`E zLcUx_;p<$&UZ4z(h&}b?2cQw z2V9r0@e4T4Ss~g74#*Zjncza#@c8;3!XOIt4!Vuph3DFwkKa4ccM)VPkhh+Z$7dwR zXenekn+REdo$uh!(Ol>Ij*_3f(4u%r2KMj?+KgBQ;@$QV-~Tcj_r`e<<9mHhu};EB z=JG9m4h?nz!=q}F-^Yqe3BsEB8ooRj!hK_-=pfa9q5Trzah6v4~##cC(W?g zE*Q-?;c^ko3BHcbArqY3I}~Q9tb`1r#a)u?Z0N&fKq>zxKiA?kmM8JqR@n4*lAa9* zP>ANtxD$+$-1pcNtY*aa?uWa*<}7p)*$&P@BcYbo>wG6;LmKJ8`H*iSz(^6m|dUmK;K_rLqy`urJl#1VL zefHLY_(tolPd4I<>7b3t>uXpfetMW`;hr51^t~+N=q>jj@9^pIRSGUDoFuvLqJ205 zSumm>!a;8d&zV|54khhFum@BGM}|Fdh|jzY#XbqW?EZML-4^!?MaTkR zA;}LF!F$%G7jKih%#2uAFYhQv&uXArW85bOgob>)c$uF*@u6RLQR zFe2IKK)rYQIZVUR-8+c#L<&<>CPD_MWM5w|*?KaFZIJL#Pf35h4`GuDvtq}$pw z#rwuPa5|Gkpnb#Xz%tXRa5`&)^nH;t@3(nz>Kpv*i;9q$1lad4d?worawdqdUbr4_^K{v1 z3!0dmbi*V*+o}Z|tnYr1+z(cCKZ$-0J-(Ra(7XgEnGoP$bdf}E%!A(@I79?W&GRH# zi-~!)!+XL=l52F;)4+NdQGtXEaLRuROhR{^`P)d8RU}smbXF5G&j9}0iz9+KWMPP& z@2yK(Np6!^Suhh%%~xM%7il)`og%r9t(@TXvKBP<)5jYfm={?5-}~862lyO{WD&Aq zk~i=`gsht}9-l8|k8s9wycH+04x;^F5g404Wz3n4q= zBU13-*AP4#M9VvM3BpsYl~}(SoyV1u-z}G{>oS^2ZVOJtW&^>RiLl~Sjwf&)@B7&t z$N@s;gzjmMpYQ)h)-x1zCT9&HoFHVE`?NWV`v76&ZuN6d3kn@pJ6Ja&{~CmF?Wn|Sy_KKu9&|adzh9HeXI8M9BnTYrv zcxTi=WSTxCSNe3;6S7n$X&BhR{Yw}&;Oq}7+Joyiut{2)(r~4+!B4kcPme=EiUnBb=wa#&HT{@Mp6IOg4 zR~#c^)%p}#IGH`Vnb(>^5ak0@1yI?{?>&hmo6zY>Stqjs!%GUCT~K1AUS2MAdg2iomxG&Ne38zj_wu7iD@ z>`=Y&+#gL1N*x-6c&~)6Zk5BttV*G>5i)P$F~H{}?-b^dekSJYC5bW0Ox|-y^7~m4 z8^CwUiBug{7N=f>iC|9NUpTK20_O^y=O7cPx#XGn* zo#M!nnR{J7MZK#T&vJo=xR1@B&q7W@Q<>TI!A<$NYAt9T` zf6W972(@?-2|Vw0sO}4>ilH(SGG9gSlMXx&ermt@`AwD?EMlFR2|3k7@;ibs^)qoj zESYLM(AWFPT8=F}LzxZ+?t6=zjgF7hG=JvLMZ9fYWl$STw=VAPQrt^v@ZiOYOMw=5 z2^5zi#ocMJ;!=vWK(XQwTmuP|7AX`6ZoyIr6uZ3N{pQZRGk5>(&e`3WJ$vRkJA2M~ z_71a*+NhW=2^30@nCXBr=KT%o`2F+UUIezA*n1PqSe78tpIxRDP18k;p(tVY$Rcf` z7uf1O5`Fxg&%gV6y$N6B!sBLgD$}8s5^H^(Ju&MyGLUdY8eeuw3OBx#mHr6#i2`q3 znu7diK9ITU_7OCrEmpK1#bC*-J`z?AcQ2d9;+Wo1hj^Ome6g#ZWX>z*JFVwZ3&Gk^ z*cCrh{$q7wDV$M96UMS5brK>PrlIzDKbn~dJ=O4h>=C^k(1nr=+UCh@qNYpUlu7FC z(AP|g(1RUK1OAKTw|>pM}*pje<}(g&Ox zDUToHvR2u2e#v3ppU2aoyecC7)~{PFY<PDsSrtgQdAP__&0(kON|f6wWw6mZEn9>8q1 z_nh@|!!61bf8}NWHTQ`2hYA;tSHeb4O1Qf?@i@Y{t|%$|IHpNK%gTD30a&+g|0jRJ zhd`UD5p6$?rih{+z_r>+Q!a=2@x=xOmZMvi#PjTQU)o?@FNYy!krcjP8nuT0eJ2{5 z)1N#Xiv6^3N5-~eR zTMqZstBFnS@3e#o@H5pgce1SXc~gN@g?k+ zzaMYbS~CFj?JQj~mL4c_N+sWNFnpd&IDa7aJ%7s392lqj>e){yi;s}82|#vgc-}qz%7vK)?-~IE7D{eeaUcfeRb2`LP z8W+mrkh-TWx7Dj2CPrEK$8m4mfhSk|fwtj=6J*Ao$9T;wBy!VyHLY4RCS_t`Lg_WJ z?y^Wp83+XNq&mgPFVdOaN@8Oi`xlk*?;@VxwS6*c^BM1#fr zm}6F|vjq$3`UzFY`Zy4#mhso7X-I-5UsiuzAJf$+VPWSAL3+#^a5TWM*$v~*=VomB z`)Q%PyxL$kXX3?}(Cprcy!5=~5kRqHI&#=uLLBews#RpnBN(na^b29hdq-0orJ2k7S9kZ= z7&EJ||P@;)qK zHI^w(7SfJ1)*w@?AZWT74;G8L$`DMe(iAiAAgb@@;`d-NBLCtd1e+au+a|nq4P1l4 z88LkIeu;9?&38Ln8CswcoYT16`{8jvnRu#svKNNl=;y$Scxp_GO ztE>MRj=NV8!41vS9hS8s71ohXYcs@FMqZ?5g^j1dQr#jdS^B1nQCa#EoKlBDzwQoF92&}6-aB2OY|tmiliK_-BtqI&M)z@KRz<@ z(8{;;5OxH$?K=67kIE(aE%0#mukx^mwwK!f`F=kuKppcOY3BjaK$jhM4;G ztbuGzc>HdO{nqhx%i2Itd2Kbe;)jnfLafI`OS0Cw#$}%$JozvqultKxkgJLxZMZSb z*ahZFBb`o2$B=(>adbtngr|xKnMsZ$qe{rLVEeIW{GgcZrEdv17atRr5G|^mXP-EZ!{sogylB*Z zwbvHOs_EATv8$6p;za|eGH!vDHRLb!30 zt7P7)5p>3SL=_Ok*Grj+3#5}TTaLry2xD?G$=x6ut-dCRhl*_qEvUN&rMyQmFGm; z@h+YRA~(>sT1nt5Ns}jP7>3r26^3v+GQk3dg2@d)SuerJ%2(XF5jUS4$sN=a(R%Sp zB55ZB7Pm1f0Zv*5v zQx7Z?!?sJ*248EvKRoM@>%2*-U9{r*HH&RvAJ#mh+{)ei*pEmdkn<2OIW#magg{x4X z3}$g~cY>6=zc!W9uc%l}l>Z#Vk1y-rY~Twt1yC+^S&RHA)(j`x zJe@EhoO@*YVUS;acXMegB`i`I#~&jXUc($FslFil(yrN&+Ib~}f>(Rg-$5ALlvSh& z_Y@Tktiq8sJmmR7r!@)SE8q%>@FVwn@ke#s@lk?-EPSZFQQCD#2RFJV+qp~s<5S?X z>i{K`rG*98xIK0YtoB0=;+h0QCPD#t=LBx}T=QJf-;iNsW9~?BYy#1;z&|dOPpkyN zy@@LfJjXJyh~3IP;N3&jb7RDq#Ea-qfTI3LS#JyZ>7@sBA98CY$p(N4v0$a!2Eryg z5j>1}gFQv>%(}G=XLTrccSn=3SO4a>Pvu+T)5DuTn^>8}9g~p(uG9#gNquP#d&E?6 zM((HR`s6)@Mcp{GT;`K@^-X7XSGA|QSLv4aYRAj0OPbv6wV6wz#9|#= zG95}U`Jfnuoi@?K+h^I%fuk=QZn@WP24(F31g9!FBPOzl)N|Om9fqZD!tm(m;HRaj zghZrkZFItsJ)5PLp@CMXMls$!Y>Kz}{R`eY8iSvW#6KXe$L{H=oxFU<3bE?Qv2gUd z4(!d?2$A^^>owC_<$6Q@kc`(^X_Uw@8`~?s_iSr1TR~W4l_#Ejb{F@k)3c!b`*1;1 z&#g|dcUVIgG)H@=px_mF{N}!TpHM_Kx&9_cWq7DFGiCo{>cAdlqCY#wdTE@-g_n#F zk9Urb6!Nk-Sb%4$$Kmj%$R^H7r`sG}wB~SR)370v)1|QHBC;XO|8&GV-dm0a1CAXJ zmFb;Gd{?(?-9(RfBE77)0?br8m)~QwkOM3E^Fhhe&{}1g)$Qb95m$;UEN{1s(C{Ud z3icyMu-94NPbGYlk<^~~%d8ZVdyN{WXc=<}fY`!mc;7A`hOjZHIpsO*K`s z*}oIQcn3Hu5AeOPf{ph;3Bkd0 z>+&e#>Tzv~s}#aM)4U*KHW(x#h|E7ZgHo3=+(}gQts-8cy~jH<$r4U<0<+JBUyD5~ z=&)Wbpyp&%&0xX9k2+7*s4e|HWyaf=+mYh9fjgdhjZ|*=`Oy(jQK6^#_0VFt# z%k_xLZh#)3AE!NX6>azF&86I*ErT?yuN0xHhz%i1g7As3kCU5M#Q1i z??>?A%Iwd}EMf%NPCDgqj|Xl;1x9Z)<`9&(3%#VY;pPZ;U>wfT&e%1<{8d0QpRO0H zT9{fpYA~Bc=mMS*fB&L53dHY0x9h-Urm^I7#&)-=BwxQ%NNB(YYL;_ml6|^^lBw4? z3j2cgl_xIy{u=JXxZ{MkGb}FD||RqOY?2Puacm0;( z`XT@pjpj%h)ej4h?!{v4(_vLSRa3eY)f9;mI5WAsVcIIV*I^ELreX^iT_p}dxmOeQAb;Lo6+d5;FN2YfsBVmF!CIT&*gZNGGL>|YIo5yXv$upSJ?+_K@E zC=tFJ8e)SuarR;%{2%jfa|w_?^XdN$8zv*fSsux`q#bUPid;nuh-6Jn`d?rr?}|f; zknDubw1oFXh1whK&PSEt6WXbe;ZKnj;-2g=>wt_X5D~9S+*^_zlzFu*vF&w#)suw} z{L7ATRLePFFLD@BO%0K8eS-5#+WNX6G-(OS-aQR#g=Oysj0gEuueXbx-bZ)MFz^0J z?V+QaFCcUz8FlT9u_nEG_&H&a0Cz8|W1yt7V<-v@>-E`S>%Q(rJaYy%mV&1qdFV%0 z-I*v`V6U}W?1l*Jfdo)4Gb)S<2Zj$TdY!fra;E%hC9X6Q<*g9AjD6-~{eAX61aIkk zGMr|>{Kj8=Z$(==^^CyhnG?ytvEJdTEUfL<##sf|01Vp{J%}-mV_c>BgrNtV*pZj~ z@xhLUhD}GepsmHuW@5X!ZQt|{%k-%1aHBu2+gc^#J{a2W9N&2w@eV-)S@5{Ct%9nq zT;Xc%2P#ATejXx2E+3k8L40$g!n!^uqHA$jxR)+M0jo3WxKEzwTF7k~=o^{JM2gd( zurY}R<7yxQEflY6&RM0s^Jc*;5Qk|?o2*KWLPayCP{0%<1`@1J8$Uk=1-@2btRjK;}$Dgh`W+egZ47nP1U*xBb?IoS*5u18NRa zfiR{DiPkn6pyd75p+twZlBnO+T^=VV7_0Em@`icg;@?9c9+hgQ(7M;9oZBKkxVkNS zVEUyF`4Y+#$=aeK3)IcRV25ObMXHcm(ieh4usfGo2OuNbCt{#tIaf1sKS(pRY;95> zAMf#;(8GG*#Mcv-rGO6DvGZjbNjIT6x+CYwdON85D!&4 zFee`|tZr#q@xO2tTxv;kcZM29(dq9kLBq({iaa)t zW0E5 zxh)k@DD|#%qc8KBx-D)*f5qa$%{5eQX_tiKZ_ZefcS8OUE|IEYJn$TF%3U-~73bJ7 zEj6YM>>G_sg^4%R{jB{|f;{d^%tDF=>*=p^V=_7eL?rfyGH` zf2rT8+|_Oa;$Q;ra!%1hoyq1&6Mg%bpScIPKc@wU7B0B|*sbzTU!eJ(lNVB-N_5S2 zL{Dot7_45_VKLgj$8z9(R?Oa^($vFEhxsvaMvw`(N5=T1B Date: Tue, 2 Jan 2018 12:40:31 +0200 Subject: [PATCH 73/92] More content fixing --- .../cc/ui/src/components/pages/ReportPage.js | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index b55246874..3d4945dcd 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -9,6 +9,7 @@ import CollapsibleWellComponent from 'components/report-components/CollapsibleWe import {Line} from 'rc-progress'; let guardicoreLogoImage = require('../../images/guardicore-logo.png'); +let monkeyLogoImage = require('../../images/monkey-icon.svg'); class ReportPageComponent extends React.Component { @@ -349,16 +350,6 @@ class ReportPageComponent extends React.Component { return

        {issuesDivArray}
      ; }; - didMonkeyFindIssues = () => { - for (let issue of Object.keys(this.state.report.overview.issues)) { - if (this.state.report.overview.issues[issue]) { - return true; - } - } - return false; - }; - - render() { let content; if (Object.keys(this.state.report).length === 0) { @@ -383,6 +374,12 @@ class ReportPageComponent extends React.Component { }}> Print Report
    +

    + Infection Monkey Report +

    +
    + +

    Overview @@ -507,7 +504,7 @@ class ReportPageComponent extends React.Component { className="label label-warning"> {this.state.report.overview.issues.filter(function (x) { return x === true; - }).length} issues: + }).length} threats:
      {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ?
    • Stolen credentials are used to exploit other machines.
    • : null} @@ -535,7 +532,7 @@ class ReportPageComponent extends React.Component { :
      During this simulated attack the Monkey uncovered 0 issues. + className="label label-success">0 threats.
      }

    @@ -583,15 +580,6 @@ class ReportPageComponent extends React.Component { successfully breached {this.state.report.glance.exploited.length} of them.

    - { - this.state.report.glance.exploited.length > 0 ? -

    - In addition, while attempting to exploit additional hosts, security software installed in the - network should have picked up the attack attempts and logged them. -

    - : - '' - }
    -

    4. Security Report

    +

    4. Security Report

    {content}
    From 59203e29a43b10bb12530a51dc161c0f085f3674 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 12:55:27 +0200 Subject: [PATCH 74/92] Split main function to smaller functions --- .../cc/ui/src/components/pages/ReportPage.js | 628 ++++++++++-------- 1 file changed, 335 insertions(+), 293 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 3d4945dcd..528092523 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -45,6 +45,36 @@ class ReportPageComponent extends React.Component { this.interval = setInterval(this.updateMapFromServer, 1000); } + componentWillUnmount() { + clearInterval(this.interval); + } + + render() { + let content; + if (Object.keys(this.state.report).length === 0) { + if (this.state.runStarted) { + content = (

    Generating Report...

    ); + } else { + content = +

    + + You have to run a monkey before generating a report! +

    ; + } + } else { + content = this.generateReportContent(); + } + + return ( + +

    4. Security Report

    +
    + {content} +
    + + ); + } + updateMonkeysRunning = () => { return fetch('/api') .then(res => res.json()) @@ -58,10 +88,6 @@ class ReportPageComponent extends React.Component { }); }; - componentWillUnmount() { - clearInterval(this.interval); - } - updateMapFromServer = () => { fetch('/api/netmap') .then(res => res.json()) @@ -86,6 +112,311 @@ class ReportPageComponent extends React.Component { } } + generateReportContent() { + return ( +
    +
    + +
    +
    + {this.generateReportHeader()} + {this.generateReportOverviewSection()} + {this.generateReportFindingsSection()} + {this.generateReportRecommendationsSection()} + {this.generateReportGlanceSection()} + {this.generateReportFooter()} +
    +
    + +
    +
    + ); + } + + generateReportHeader() { + return ( + + ); + } + + generateReportOverviewSection() { + return ( +
    +

    + Overview +

    + { + this.state.report.glance.exploited.length > 0 ? + (

    + + Critical security issues were detected! +

    ) : + (

    + + No critical security issues were detected. +

    ) + } + { + this.state.allMonkeysAreDead ? + '' + : + (

    + + Some monkeys are still running. To get the best report it's best to wait for all of them to finish + running. +

    ) + } + { + this.state.report.glance.exploited.length > 0 ? + '' + : +

    + + To improve the monkey's detection rates, try adding users and passwords and enable the "Local + network + scan" config value under Basic - Network. +

    + } +

    + The first monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished + propagation attempts. +

    +

    + The monkey started propagating from the following machines where it was manually installed: +

      + {this.state.report.overview.manual_monkeys.map(x =>
    • {x}
    • )} +
    +

    +

    + The monkeys were run with the following configuration: +

    + { + this.state.report.overview.config_users.length > 0 ? +

    + Usernames used for brute-forcing: +

      + {this.state.report.overview.config_users.map(x =>
    • {x}
    • )} +
    + Passwords used for brute-forcing: +
      + {this.state.report.overview.config_passwords.map(x =>
    • {x.substr(0, 3) + '******'}
    • )} +
    +

    + : +

    + Brute forcing uses stolen credentials only. No credentials were supplied during Monkey’s + configuration. +

    + } + { + this.state.report.overview.config_exploits.length > 0 ? + ( + this.state.report.overview.config_exploits[0] === 'default' ? + '' + : +

    + The Monkey uses the following exploit methods: +

      + {this.state.report.overview.config_exploits.map(x =>
    • {x}
    • )} +
    +

    + ) + : +

    + No exploits are used by the Monkey. +

    + } + { + this.state.report.overview.config_ips.length > 0 ? +

    + The Monkey scans the following IPs: +

      + {this.state.report.overview.config_ips.map(x =>
    • {x}
    • )} +
    +

    + : + '' + } + { + this.state.report.overview.config_scan ? + '' + : +

    + Note: Monkeys were configured to avoid scanning of the local network. +

    + } +
    + ); + } + + generateReportFindingsSection() { + return ( +
    +

    + Security Findings +

    +
    +

    + Immediate Threats +

    + { + this.state.report.overview.issues.filter(function (x) { + return x === true; + }).length > 0 ? +
    + During this simulated attack the Monkey uncovered + {this.state.report.overview.issues.filter(function (x) { + return x === true; + }).length} threats: +
      + {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? +
    • Stolen credentials are used to exploit other machines.
    • : null} + {this.state.report.overview.issues[this.Issue.ELASTIC] ? +
    • Elasticsearch servers are vulnerable to CVE-2015-1427. +
    • : null} + {this.state.report.overview.issues[this.Issue.SAMBACRY] ? +
    • Samba servers are vulnerable to ‘SambaCry’ (CVE-2017-7494).
    • : null} + {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? +
    • Machines are vulnerable to ‘Shellshock’ (CVE-2014-6271). +
    • : null} + {this.state.report.overview.issues[this.Issue.CONFICKER] ? +
    • Machines are vulnerable to ‘Conficker’ (MS08-067).
    • : null} + {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? +
    • Machines are accessible using passwords supplied by the user during the Monkey’s + configuration.
    • : null} +
    +
    + : +
    + During this simulated attack the Monkey uncovered 0 threats. +
    + } +
    +
    +

    + Potential Security Issues +

    + { + this.state.report.overview.warnings.filter(function (x) { + return x === true; + }).length > 0 ? +
    + The Monkey uncovered the following possible set of issues: +
      + {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? +
    • Weak segmentation - Machines from different segments are able to + communicate.
    • : null} + {this.state.report.overview.warnings[this.Warning.TUNNEL] ? +
    • Lack of machine hardening, machines successfully tunneled monkey traffic using unused + ports.
    • : null} +
    +
    + : +
    + The Monkey did not find any issues. +
    + } +
    +
    + ); + } + + generateReportRecommendationsSection() { + return ( +
    +

    + Recommendations +

    +
    + {this.generateIssues(this.state.report.recommendations.issues)} +
    +
    + ); + } + + generateReportGlanceSection() { + let exploitPercentage = + (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; + return ( +
    +

    + The Network from the Monkey's Eyes +

    +
    +

    + The Monkey discovered {this.state.report.glance.scanned.length} machines and + successfully breached {this.state.report.glance.exploited.length} of them. +

    +
    + + {Math.round(exploitPercentage)}% of scanned machines exploited +
    +
    +

    + From the attacker's point of view, the network looks like this: +

    +
    + Legend: + Exploit + | + Scan + | + Tunnel + | + Island Communication +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + ); + } + + generateReportFooter() { + return ( + + ); + } + generateInfoBadges(data_array) { return data_array.map(badge_data => {badge_data}); } @@ -349,295 +680,6 @@ class ReportPageComponent extends React.Component { } return
      {issuesDivArray}
    ; }; - - render() { - let content; - if (Object.keys(this.state.report).length === 0) { - if (this.state.runStarted) { - content = (

    Generating Report...

    ); - } else { - content = -

    - - You have to run a monkey before generating a report! -

    ; - } - } else { - let exploitPercentage = - (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; - content = - ( -
    -
    - -
    -
    -

    - Infection Monkey Report -

    -
    - -
    -
    -

    - Overview -

    - { - this.state.report.glance.exploited.length > 0 ? - (

    - - Critical security issues were detected! -

    ) : - (

    - - No critical security issues were detected. -

    ) - } - { - this.state.allMonkeysAreDead ? - '' - : - (

    - - Some monkeys are still running. To get the best report it's best to wait for all of them to finish - running. -

    ) - } - { - this.state.report.glance.exploited.length > 0 ? - '' - : -

    - - To improve the monkey's detection rates, try adding users and passwords and enable the "Local - network - scan" config value under Basic - Network. -

    - } -

    - The first monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished - propagation attempts. -

    -

    - The monkey started propagating from the following machines where it was manually installed: -

      - {this.state.report.overview.manual_monkeys.map(x =>
    • {x}
    • )} -
    -

    -

    - The monkeys were run with the following configuration: -

    - { - this.state.report.overview.config_users.length > 0 ? -

    - Usernames used for brute-forcing: -

      - {this.state.report.overview.config_users.map(x =>
    • {x}
    • )} -
    - Passwords used for brute-forcing: -
      - {this.state.report.overview.config_passwords.map(x =>
    • {x.substr(0, 3) + '******'}
    • )} -
    -

    - : -

    - Brute forcing uses stolen credentials only. No credentials were supplied during Monkey’s - configuration. -

    - } - { - this.state.report.overview.config_exploits.length > 0 ? - ( - this.state.report.overview.config_exploits[0] === 'default' ? - '' - : -

    - The Monkey uses the following exploit methods: -

      - {this.state.report.overview.config_exploits.map(x =>
    • {x}
    • )} -
    -

    - ) - : -

    - No exploits are used by the Monkey. -

    - } - { - this.state.report.overview.config_ips.length > 0 ? -

    - The Monkey scans the following IPs: -

      - {this.state.report.overview.config_ips.map(x =>
    • {x}
    • )} -
    -

    - : - '' - } - { - this.state.report.overview.config_scan ? - '' - : -

    - Note: Monkeys were configured to avoid scanning of the local network. -

    - } -
    -
    -

    - Security Findings -

    -
    -

    - Immediate Threats -

    - { - this.state.report.overview.issues.filter(function (x) { - return x === true; - }).length > 0 ? -
    - During this simulated attack the Monkey uncovered - {this.state.report.overview.issues.filter(function (x) { - return x === true; - }).length} threats: -
      - {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? -
    • Stolen credentials are used to exploit other machines.
    • : null} - {this.state.report.overview.issues[this.Issue.ELASTIC] ? -
    • Elasticsearch servers are vulnerable to CVE-2015-1427. -
    • : null} - {this.state.report.overview.issues[this.Issue.SAMBACRY] ? -
    • Samba servers are vulnerable to ‘SambaCry’ (CVE-2017-7494).
    • : null} - {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? -
    • Machines are vulnerable to ‘Shellshock’ (CVE-2014-6271). -
    • : null} - {this.state.report.overview.issues[this.Issue.CONFICKER] ? -
    • Machines are vulnerable to ‘Conficker’ (MS08-067).
    • : null} - {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? -
    • Machines are accessible using passwords supplied by the user during the Monkey’s - configuration.
    • : null} -
    -
    - : -
    - During this simulated attack the Monkey uncovered 0 threats. -
    - } -
    -
    -

    - Potential Security Issues -

    - { - this.state.report.overview.warnings.filter(function (x) { - return x === true; - }).length > 0 ? -
    - The Monkey uncovered the following possible set of issues: -
      - {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? -
    • Weak segmentation - Machines from different segments are able to - communicate.
    • : null} - {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
    • Lack of machine hardening, machines successfully tunneled monkey traffic using unused ports.
    • : null} -
    -
    - : -
    - The Monkey did not find any issues. -
    - } -
    -
    -
    -

    - Recommendations -

    -
    - {this.generateIssues(this.state.report.recommendations.issues)} -
    -
    -
    -

    - The Network from the Monkey's Eyes -

    -
    -

    - The Monkey discovered {this.state.report.glance.scanned.length} machines and - successfully breached {this.state.report.glance.exploited.length} of them. -

    -
    - - {Math.round(exploitPercentage)}% of scanned machines exploited -
    -
    -

    - From the attacker's point of view, the network looks like this: -

    -
    - Legend: - Exploit - | - Scan - | - Tunnel - | - Island Communication -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - For questions, suggestions or any other feedback - contact: labs@guardicore.com -
    labs@guardicore.com
    - GuardiCore -
    -
    -
    - -
    -
    - ); - } - - return ( - -

    4. Security Report

    -
    - {content} -
    - - ); - } } export default ReportPageComponent; From a2da9614a64d7e0d716344e56b8d03eba3f05130 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 15:27:32 +0200 Subject: [PATCH 75/92] Make deb use virtualenv --- monkey_island/deb-package/DEBIAN/postinst | 10 ++++++---- .../deb-package/monkey_island_pip_requirements.txt | 3 ++- monkey_island/linux/ubuntu/systemd/start_server.sh | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/monkey_island/deb-package/DEBIAN/postinst b/monkey_island/deb-package/DEBIAN/postinst index 42b8f1eb0..502c90c5e 100644 --- a/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey_island/deb-package/DEBIAN/postinst @@ -2,16 +2,18 @@ MONKEY_FOLDER=/var/monkey_island INSTALLATION_FOLDER=/var/monkey_island/installation +PYTHON_FOLDER=/var/monkey_island/bin/python cp -f ${MONKEY_FOLDER}/monkey.sh /usr/bin/monkey chmod 755 /usr/bin/monkey -# Fix dependency bug -pip uninstall -y bson +# Prepare python virtualenv +pip install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER +mkdir ${MONKEY_FOLDER}/bin +virtualenv ${PYTHON_FOLDER} # install pip requirements -pip install -r $MONKEY_FOLDER/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER - +${PYTHON_FOLDER}/bin/python/python -m pip install -r $MONKEY_FOLDER/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER # remove installation folder and unnecessary files rm -rf ${INSTALLATION_FOLDER} diff --git a/monkey_island/deb-package/monkey_island_pip_requirements.txt b/monkey_island/deb-package/monkey_island_pip_requirements.txt index 275c8b96a..26f0f9ee2 100644 --- a/monkey_island/deb-package/monkey_island_pip_requirements.txt +++ b/monkey_island/deb-package/monkey_island_pip_requirements.txt @@ -9,4 +9,5 @@ flask Flask-Pymongo Flask-Restful jsonschema -netifaces \ No newline at end of file +netifaces +virtualenv \ No newline at end of file diff --git a/monkey_island/linux/ubuntu/systemd/start_server.sh b/monkey_island/linux/ubuntu/systemd/start_server.sh index 817da7a55..e2ea895be 100644 --- a/monkey_island/linux/ubuntu/systemd/start_server.sh +++ b/monkey_island/linux/ubuntu/systemd/start_server.sh @@ -1,4 +1,4 @@ #!/bin/bash cd /var/monkey_island/cc -python main.py \ No newline at end of file +/var/monkey_island/bin/python/python main.py \ No newline at end of file From bee1bc6f8f0701fabfbc697dcceb12b527893148 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 15:58:47 +0200 Subject: [PATCH 76/92] Fix python path --- monkey_island/deb-package/DEBIAN/postinst | 2 +- monkey_island/linux/ubuntu/systemd/start_server.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/deb-package/DEBIAN/postinst b/monkey_island/deb-package/DEBIAN/postinst index 502c90c5e..44caa26de 100644 --- a/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey_island/deb-package/DEBIAN/postinst @@ -13,7 +13,7 @@ mkdir ${MONKEY_FOLDER}/bin virtualenv ${PYTHON_FOLDER} # install pip requirements -${PYTHON_FOLDER}/bin/python/python -m pip install -r $MONKEY_FOLDER/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER +${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER # remove installation folder and unnecessary files rm -rf ${INSTALLATION_FOLDER} diff --git a/monkey_island/linux/ubuntu/systemd/start_server.sh b/monkey_island/linux/ubuntu/systemd/start_server.sh index e2ea895be..ceeab57f4 100644 --- a/monkey_island/linux/ubuntu/systemd/start_server.sh +++ b/monkey_island/linux/ubuntu/systemd/start_server.sh @@ -1,4 +1,4 @@ #!/bin/bash cd /var/monkey_island/cc -/var/monkey_island/bin/python/python main.py \ No newline at end of file +/var/monkey_island/bin/python/bin/python main.py \ No newline at end of file From 3e6a2136d93f7868cb9880e66652377776e728f9 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 16:13:20 +0200 Subject: [PATCH 77/92] Don't create already created dir Remove monkey_island dir on removal --- monkey_island/deb-package/DEBIAN/postinst | 1 - monkey_island/deb-package/DEBIAN/prerm | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey_island/deb-package/DEBIAN/postinst b/monkey_island/deb-package/DEBIAN/postinst index 44caa26de..601fbc046 100644 --- a/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey_island/deb-package/DEBIAN/postinst @@ -9,7 +9,6 @@ chmod 755 /usr/bin/monkey # Prepare python virtualenv pip install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER -mkdir ${MONKEY_FOLDER}/bin virtualenv ${PYTHON_FOLDER} # install pip requirements diff --git a/monkey_island/deb-package/DEBIAN/prerm b/monkey_island/deb-package/DEBIAN/prerm index e7924c738..98557e487 100644 --- a/monkey_island/deb-package/DEBIAN/prerm +++ b/monkey_island/deb-package/DEBIAN/prerm @@ -8,4 +8,6 @@ rm -f /etc/init/monkey-mongo.conf [ -f "/lib/systemd/system/monkey-island.service" ] && rm -f /lib/systemd/system/monkey-island.service [ -f "/lib/systemd/system/monkey-mongo.service" ] && rm -f /lib/systemd/system/monkey-mongo.service +rm -r -f /var/monkey_island + exit 0 \ No newline at end of file From fd64c9dbf6c76a85309f0e6727b4766cdea3a76d Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 17:53:28 +0200 Subject: [PATCH 78/92] Make sure we're using python2 --- monkey_island/deb-package/DEBIAN/postinst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey_island/deb-package/DEBIAN/postinst b/monkey_island/deb-package/DEBIAN/postinst index 601fbc046..3fa922a01 100644 --- a/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey_island/deb-package/DEBIAN/postinst @@ -8,8 +8,8 @@ cp -f ${MONKEY_FOLDER}/monkey.sh /usr/bin/monkey chmod 755 /usr/bin/monkey # Prepare python virtualenv -pip install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER -virtualenv ${PYTHON_FOLDER} +pip2 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER +virtualenv -p python2.7 ${PYTHON_FOLDER} # install pip requirements ${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/pip_requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER From cfa70e109a29e8cbe1b9d5fc9cd0b59c79ab86b3 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 18:09:51 +0200 Subject: [PATCH 79/92] Start browser when running island on windows --- monkey_island/windows/run_server.bat | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey_island/windows/run_server.bat b/monkey_island/windows/run_server.bat index e2d7b70c1..a15fbcc04 100644 --- a/monkey_island/windows/run_server.bat +++ b/monkey_island/windows/run_server.bat @@ -1,3 +1,4 @@ if not exist db mkdir db start windows\run_mongodb.bat -start windows\run_cc.bat \ No newline at end of file +start windows\run_cc.bat +start https://localhost:5000 \ No newline at end of file From fb5f1f8302a6571ee6bcf0dfd0482f35b515d9b9 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 2 Jan 2018 19:09:41 +0200 Subject: [PATCH 80/92] Monkey icon moved to top left --- .../cc/ui/src/components/pages/ReportPage.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 528092523..2c6551f53 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -140,12 +140,18 @@ class ReportPageComponent extends React.Component { generateReportHeader() { return ( ); } From 30d81e05c98933bc688856d7c1e6a802a37eb52e Mon Sep 17 00:00:00 2001 From: Ace Pace Date: Tue, 9 Jan 2018 18:37:59 +0200 Subject: [PATCH 81/92] CR fixes --- chaos_monkey/network/tcp_scanner.py | 15 +++++++-------- chaos_monkey/network/tools.py | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/chaos_monkey/network/tcp_scanner.py b/chaos_monkey/network/tcp_scanner.py index e149da8e7..7b37c3278 100644 --- a/chaos_monkey/network/tcp_scanner.py +++ b/chaos_monkey/network/tcp_scanner.py @@ -28,13 +28,12 @@ class TcpScanner(HostScanner, HostFinger): shuffle(target_ports) ports, banners = check_tcp_ports(host.ip_addr, target_ports, self._config.tcp_scan_timeout / 1000.0) - if len(ports) != 0: - for target_port, banner in zip(ports, banners): - service = 'tcp-' + str(target_port) - host.services[service] = {} - if banner: - host.services[service]['banner'] = banner - if only_one_port: - break + for target_port, banner in zip(ports, banners): + service = 'tcp-' + str(target_port) + host.services[service] = {} + if banner: + host.services[service]['banner'] = banner + if only_one_port: + break return len(ports) != 0 diff --git a/chaos_monkey/network/tools.py b/chaos_monkey/network/tools.py index 4ce8fed2e..b26fe5d20 100644 --- a/chaos_monkey/network/tools.py +++ b/chaos_monkey/network/tools.py @@ -132,7 +132,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): time_left) # any read_socket is automatically a writesocket num_replies = len(write_sockets) + len(err_sockets) - if num_replies == len(port_attempts) or time_left == 0: + if num_replies == len(port_attempts) or time_left <= 0: break else: time_left -= SLEEP_BETWEEN_POLL From 1ab1dbedb1d8248ddd89e76509498b9f50d8d9dd Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 9 Jan 2018 19:16:18 +0200 Subject: [PATCH 82/92] Add TelemetryFeed entrypoint + all logic --- monkey_island/cc/app.py | 2 + monkey_island/cc/resources/telemetry_feed.py | 85 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 monkey_island/cc/resources/telemetry_feed.py diff --git a/monkey_island/cc/app.py b/monkey_island/cc/app.py index 6cfea1502..3990d1003 100644 --- a/monkey_island/cc/app.py +++ b/monkey_island/cc/app.py @@ -17,6 +17,7 @@ from cc.resources.edge import Edge from cc.resources.node import Node from cc.resources.root import Root +from cc.resources.telemetry_feed import TelemetryFeed from cc.services.config import ConfigService __author__ = 'Barak' @@ -88,5 +89,6 @@ 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(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') return app diff --git a/monkey_island/cc/resources/telemetry_feed.py b/monkey_island/cc/resources/telemetry_feed.py new file mode 100644 index 000000000..4c5e3b354 --- /dev/null +++ b/monkey_island/cc/resources/telemetry_feed.py @@ -0,0 +1,85 @@ +from datetime import datetime + +import dateutil +import flask_restful +from flask import request + +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... + timestamp = datetime(2010,1,1).isoformat() + + telemetries = mongo.db.telemetry.find({'timestamp': {'$gt': dateutil.parser.parse(timestamp)}}) + + 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 + } From 50c674a2af02d6da8e8350a5362f98832697ed4a Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 9 Jan 2018 19:19:16 +0200 Subject: [PATCH 83/92] Add telemetry console frontend --- .../cc/ui/src/components/pages/MapPage.js | 58 ++++++++++++++----- monkey_island/cc/ui/src/styles/App.css | 3 +- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey_island/cc/ui/src/components/pages/MapPage.js index 81f3baf8a..bb9aa0d74 100644 --- a/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -53,7 +53,9 @@ class MapPageComponent extends React.Component { selected: null, selectedType: null, killPressed: false, - showKillDialog: false + showKillDialog: false, + telemetry: [], + telemetryLastTimestamp: null }; } @@ -77,13 +79,18 @@ class MapPageComponent extends React.Component { componentDidMount() { this.updateMapFromServer(); - this.interval = setInterval(this.updateMapFromServer, 1000); + this.interval = setInterval(this.timedEvents, 1000); } componentWillUnmount() { clearInterval(this.interval); } + timedEvents = () => { + this.updateMapFromServer(); + this.updateTelemetryFromServer(); + }; + updateMapFromServer = () => { fetch('/api/netmap') .then(res => res.json()) @@ -96,6 +103,21 @@ class MapPageComponent extends React.Component { }); }; + updateTelemetryFromServer = () => { + fetch('/api/telemetry-feed?timestamp='+this.state.telemetryLastTimestamp) + .then(res => res.json()) + .then(res => { + let newTelem = this.state.telemetry.concat(res['telemetries']); + + this.setState( + { + telemetry: newTelem, + telemetryLastTimestamp: res['timestamp'] + }); + this.props.onStatusChange(); + }); + }; + selectionChanged(event) { if (event.nodes.length === 1) { fetch('/api/netmap/node?id=' + event.nodes[0]) @@ -156,6 +178,26 @@ class MapPageComponent extends React.Component { ) }; + renderTelemetryEntry(telemetry) { + return ( +
    + {telemetry.timestamp} + {telemetry.hostname}: + {telemetry.brief} +
    + ); + } + + renderTelemetryConsole() { + return ( +
    + { + this.state.telemetry.map(this.renderTelemetryEntry) + } +
    + ); + } + render() { return (
    @@ -174,17 +216,7 @@ class MapPageComponent extends React.Component { | Island Communication
    - { - /* -
    -
    - 2017-10-16 16:00:05 - monkey-elastic - bla bla -
    -
    - */ - } + { this.renderTelemetryConsole() }
    diff --git a/monkey_island/cc/ui/src/styles/App.css b/monkey_island/cc/ui/src/styles/App.css index 9ecf08cbb..9a0d248a5 100644 --- a/monkey_island/cc/ui/src/styles/App.css +++ b/monkey_island/cc/ui/src/styles/App.css @@ -276,13 +276,14 @@ body { bottom: 0; left: 0; right: 0; - height: 70px; + height: 130px; background: rgba(0,0,0,0.7); border-radius: 5px; border: 3px solid #aaa; padding: 0.5em; color: white; font-family: Consolas, "Courier New", monospace; + overflow: auto; } .telemetry-console .date { From 1935d2d6a191cd11ca249cfd1f292c979ba12220 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 9 Jan 2018 19:25:18 +0200 Subject: [PATCH 84/92] Fix temporary hack --- monkey_island/cc/resources/telemetry_feed.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey_island/cc/resources/telemetry_feed.py b/monkey_island/cc/resources/telemetry_feed.py index 4c5e3b354..24e0dcc51 100644 --- a/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey_island/cc/resources/telemetry_feed.py @@ -14,9 +14,9 @@ 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... - timestamp = datetime(2010,1,1).isoformat() - - telemetries = mongo.db.telemetry.find({'timestamp': {'$gt': dateutil.parser.parse(timestamp)}}) + telemetries = mongo.db.telemetry.find({}) + else: + telemetries = mongo.db.telemetry.find({'timestamp': {'$gt': dateutil.parser.parse(timestamp)}}) return \ { From 4ef0a5302627e934c8f4d3ec259af4d81fc65e30 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 16 Jan 2018 10:50:05 +0200 Subject: [PATCH 85/92] Content fixes --- .../cc/ui/src/components/pages/ReportPage.js | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index 2c6551f53..e6becee6d 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -334,8 +334,7 @@ class ReportPageComponent extends React.Component {
  • Weak segmentation - Machines from different segments are able to communicate.
  • : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
  • Lack of machine hardening, machines successfully tunneled monkey traffic using unused - ports.
  • : null} +
  • Weak segmentation - machines were able to communicate over unused ports.
  • : null}
    : @@ -441,7 +440,7 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a SMB attack.
    - The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password. @@ -458,7 +457,7 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a SMB attack.
    - The attack succeeded by using a pass-the-hash attack over SMB protocol with user {issue.username}. @@ -475,7 +474,7 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a WMI attack.
    - The attack succeeded by authenticating over WMI protocol with user {issue.username} and its password. @@ -492,7 +491,7 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a WMI attack.
    - The attack succeeded by using a pass-the-hash attack over WMI protocol with user {issue.username}. @@ -509,7 +508,7 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a SSH attack.
    - The attack succeeded by authenticating over SSH protocol with user {issue.username} and its password. @@ -526,7 +525,7 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a RDP attack.
    - The attack succeeded by authenticating over RDP protocol with user {issue.username} and its password. @@ -545,8 +544,8 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a SambaCry attack.
    - The attack succeeded by authenticating over SMB protocol with user {issue.username} and its password, and by using the SambaCry + The Monkey authenticated over the SMB protocol with user {issue.username} and its password, and used the SambaCry vulnerability. @@ -562,7 +561,7 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to an Elastic Groovy attack.
    - The attack succeeded because the Elastic Search server is not patched against CVE-2015-1427. + The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427. ); @@ -577,8 +576,8 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a ShellShock attack.
    - The attack succeeded because the HTTP server running on TCP port {issue.port} is vulnerable to a shell injection attack on the + The attack was made possible because the HTTP server running on TCP port {issue.port} was vulnerable to a shell injection attack on the paths: {this.generateShellshockPathListBadges(issue.paths)}. @@ -594,8 +593,8 @@ class ReportPageComponent extends React.Component { className="label label-info" style={{margin: '2px'}}>{issue.ip_address}) is vulnerable to a Conficker attack.
    - The attack succeeded because the target machine uses an outdated and unpatched operating system vulnerable to - Conficker. + The attack was made possible because the target machine used an outdated and unpatched operating system + vulnerable to Conficker. ); From e1803a7ff9abb6fde8d02b50c81978e612072d07 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Tue, 16 Jan 2018 17:23:17 +0200 Subject: [PATCH 86/92] Fixed CR --- monkey_island/cc/resources/root.py | 40 ++++++----- monkey_island/cc/resources/telemetry.py | 4 +- monkey_island/cc/services/report.py | 68 +++++++++---------- monkey_island/cc/utils.py | 2 - .../monkey_island_pip_requirements.txt | 2 + monkey_island/requirements.txt | 3 +- 6 files changed, 61 insertions(+), 58 deletions(-) diff --git a/monkey_island/cc/resources/root.py b/monkey_island/cc/resources/root.py index d553e8727..25d7dfed7 100644 --- a/monkey_island/cc/resources/root.py +++ b/monkey_island/cc/resources/root.py @@ -1,13 +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' @@ -19,25 +18,34 @@ 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() - mongo.db.report.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() infection_done = NodeService.is_monkey_finished_running() report_done = ReportService.is_report_generated() diff --git a/monkey_island/cc/resources/telemetry.py b/monkey_island/cc/resources/telemetry.py index ade4f6734..030f10af2 100644 --- a/monkey_island/cc/resources/telemetry.py +++ b/monkey_island/cc/resources/telemetry.py @@ -124,9 +124,7 @@ class Telemetry(flask_restful.Resource): for attempt in telemetry_json['data']['attempts']: if attempt['result']: attempt.pop('result') - for field in ['password', 'lm_hash', 'ntlm_hash']: - if len(attempt[field]) == 0: - attempt.pop(field) + [attempt.pop(field) for field in ['password', 'lm_hash', 'ntlm_hash'] if len(attempt[field]) == 0] NodeService.add_credentials_to_node(edge['to'], attempt) @staticmethod diff --git a/monkey_island/cc/services/report.py b/monkey_island/cc/services/report.py index 0fcb71990..c197c55f3 100644 --- a/monkey_island/cc/services/report.py +++ b/monkey_island/cc/services/report.py @@ -1,4 +1,5 @@ import ipaddress +from enum import Enum from cc.database import mongo from cc.services.config import ConfigService @@ -25,7 +26,7 @@ class ReportService: 'ShellShockExploiter': 'ShellShock Exploiter', } - class ISSUES_DICT: + class ISSUES_DICT(Enum): WEAK_PASSWORD = 0 STOLEN_CREDS = 1 ELASTIC = 2 @@ -33,7 +34,7 @@ class ReportService: SHELLSHOCK = 4 CONFICKER = 5 - class WARNINGS_DICT: + class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 TUNNEL = 1 @@ -49,18 +50,15 @@ class ReportService: 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 - total = delta.seconds - seconds = total % 60 - total = (total - seconds) / 60 - minutes = total % 60 - total = (total - minutes) / 60 - hours = total if hours > 0: st += "%d hours, " % hours - st += "%d minutes and %d seconds" % (minutes, seconds) + return st @staticmethod @@ -77,7 +75,8 @@ class ReportService: 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})] + + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in + mongo.db.monkey.find({}, {'_id': 1})] nodes = [ { 'label': node['label'], @@ -95,7 +94,8 @@ class ReportService: @staticmethod def get_exploited(): exploited = \ - [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in mongo.db.monkey.find({}, {'_id': 1}) + [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})] @@ -215,22 +215,18 @@ class ReportService: @staticmethod def process_exploit(exploit): exploiter_type = exploit['data']['exploiter'] - if exploiter_type == 'SmbExploiter': - return ReportService.process_smb_exploit(exploit) - if exploiter_type == 'WmiExploiter': - return ReportService.process_wmi_exploit(exploit) - if exploiter_type == 'SSHExploiter': - return ReportService.process_ssh_exploit(exploit) - if exploiter_type == 'RdpExploiter': - return ReportService.process_rdp_exploit(exploit) - if exploiter_type == 'SambaCryExploiter': - return ReportService.process_sambacry_exploit(exploit) - if exploiter_type == 'ElasticGroovyExploiter': - return ReportService.process_elastic_exploit(exploit) - if exploiter_type == 'Ms08_067_Exploiter': - return ReportService.process_conficker_exploit(exploit) - if exploiter_type == 'ShellShockExploiter': - return ReportService.process_shellshock_exploit(exploit) + 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(): @@ -334,18 +330,18 @@ class ReportService: for machine in issues: for issue in issues[machine]: if issue['type'] == 'elastic': - issues_byte_array[ReportService.ISSUES_DICT.ELASTIC] = True + issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True elif issue['type'] == 'sambacry': - issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY] = True + issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True elif issue['type'] == 'shellshock': - issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK] = True + issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True elif issue['type'] == 'conficker': - issues_byte_array[ReportService.ISSUES_DICT.CONFICKER] = True + 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] = True + 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] = True + issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True return issues_byte_array @@ -356,9 +352,9 @@ class ReportService: for machine in issues: for issue in issues[machine]: if issue['type'] == 'cross_segment': - warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT] = True + warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True elif issue['type'] == 'tunnel': - warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL] = True + warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True return warnings_byte_array diff --git a/monkey_island/cc/utils.py b/monkey_island/cc/utils.py index d59c23825..9c49eba2c 100644 --- a/monkey_island/cc/utils.py +++ b/monkey_island/cc/utils.py @@ -7,8 +7,6 @@ import struct import ipaddress from netifaces import interfaces, ifaddresses, AF_INET -from cc.database import mongo - __author__ = 'Barak' diff --git a/monkey_island/deb-package/monkey_island_pip_requirements.txt b/monkey_island/deb-package/monkey_island_pip_requirements.txt index 26f0f9ee2..404aad8b0 100644 --- a/monkey_island/deb-package/monkey_island_pip_requirements.txt +++ b/monkey_island/deb-package/monkey_island_pip_requirements.txt @@ -10,4 +10,6 @@ Flask-Pymongo Flask-Restful jsonschema netifaces +ipaddress +enum34 virtualenv \ No newline at end of file diff --git a/monkey_island/requirements.txt b/monkey_island/requirements.txt index 1aa7288c5..9d8bfbfb8 100644 --- a/monkey_island/requirements.txt +++ b/monkey_island/requirements.txt @@ -10,4 +10,5 @@ Flask-Pymongo Flask-Restful jsonschema netifaces -ipaddress \ No newline at end of file +ipaddress +enum34 \ No newline at end of file From 56035df461295b2fae374ef9c0301995012262a0 Mon Sep 17 00:00:00 2001 From: Itay Mizeretz Date: Thu, 18 Jan 2018 18:33:02 +0200 Subject: [PATCH 87/92] Most theme updates --- monkey_island/cc/ui/src/components/Main.js | 2 +- .../cc/ui/src/components/pages/ReportPage.js | 22 ++++++++++--------- monkey_island/cc/ui/src/styles/App.css | 21 ++++++++++-------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/monkey_island/cc/ui/src/components/Main.js b/monkey_island/cc/ui/src/components/Main.js index cee9bc494..881c3a2ec 100644 --- a/monkey_island/cc/ui/src/components/Main.js +++ b/monkey_island/cc/ui/src/components/Main.js @@ -69,7 +69,7 @@ class AppComponent extends React.Component {
    - + Infection Monkey
    diff --git a/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey_island/cc/ui/src/components/pages/ReportPage.js index e6becee6d..53a9d0322 100644 --- a/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -122,6 +122,7 @@ class ReportPageComponent extends React.Component {
    {this.generateReportHeader()} +
    {this.generateReportOverviewSection()} {this.generateReportFindingsSection()} {this.generateReportRecommendationsSection()} @@ -139,19 +140,20 @@ class ReportPageComponent extends React.Component { generateReportHeader() { return ( -